      1 /*
      2     Copyright 2008-2024
      3         Matthias Ehmann,
      4         Michael Gerhaeuser,
      5         Carsten Miller,
      6         Bianca Valentin,
      7         Alfred Wassermann,
      8         Peter Wilfahrt
      9
     10     This file is part of JSXGraph.
     11
     12     JSXGraph is free software dual licensed under the GNU LGPL or MIT License.
     13
     14     You can redistribute it and/or modify it under the terms of the
     15
     16       * GNU Lesser General Public License as published by
     17         the Free Software Foundation, either version 3 of the License, or
     18         (at your option) any later version
     19       OR
     20       * MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT
     21
     22     JSXGraph is distributed in the hope that it will be useful,
     23     but WITHOUT ANY WARRANTY; without even the implied warranty of
     24     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     25     GNU Lesser General Public License for more details.
     26
     27     You should have received a copy of the GNU Lesser General Public License and
     28     the MIT License along with JSXGraph. If not, see <https://www.gnu.org/licenses/>
     29     and <https://opensource.org/licenses/MIT/>.
     30  */
     31
     32 /*global JXG: true, define: true*/
     33 /*jslint nomen: true, plusplus: true*/
     34
     35 import JXG from "../jxg.js";
     36 import Const from "../base/constants.js";
     37 import Type from "../utils/type.js";
     38
     39 /**
     40  * Parser helper routines. The methods in here are for parsing expressions in Geonext Syntax.
     41  * @namespace
     42  */
     43 JXG.GeonextParser = {
     44     /**
     45      * Converts expression of the form <i>leftop^rightop</i> into <i>Math.pow(leftop,rightop)</i>.
     46      * @param {String} te Expression of the form <i>leftop^rightop</i>
     47      * @returns {String} Converted expression.
     48      */
     49     replacePow: function (te) {
     50         var count, pos, c, previousIndex, leftop, rightop, pre, p, left, i, right, expr;
     51
     52         // delete all whitespace immediately before and after all ^ operators
     53         te = te.replace(/(\s*)\^(\s*)/g, "^");
     54
     55         //  Loop over all ^ operators
     56         i = te.indexOf("^");
     57         previousIndex = -1;
     58
     59         while (i >= 0 && i < te.length - 1) {
     60             if (previousIndex === i) {
     61                 throw new Error("JSXGraph: Error while parsing expression '" + te + "'");
     62             }
     63             previousIndex = i;
     64
     65             // left and right are the substrings before, resp. after the ^ character
     66             left = te.slice(0, i);
     67             right = te.slice(i + 1);
     68
     69             // If there is a ")" immediately before the ^ operator, it can be the end of a
     70             // (i) term in parenthesis
     71             // (ii) function call
     72             // (iii) method  call
     73             // In either case, first the corresponding opening parenthesis is searched.
     74             // This is the case, when count==0
     75             if (left.charAt(left.length - 1) === ")") {
     76                 count = 1;
     77                 pos = left.length - 2;
     78
     79                 while (pos >= 0 && count > 0) {
     80                     c = left.charAt(pos);
     81                     if (c === ")") {
     82                         count++;
     83                     } else if (c === "(") {
     84                         count -= 1;
     85                     }
     86                     pos -= 1;
     87                 }
     88
     89                 if (count === 0) {
     90                     // Now, we have found the opning parenthesis and we have to look
     91                     // if it is (i), or (ii), (iii).
     92                     leftop = "";
     93                     // Search for F or p.M before (...)^
     94                     pre = left.substring(0, pos + 1);
     95                     p = pos;
     96                     while (p >= 0 && pre.slice(p, p + 1).match(/([\w.]+)/)) {
     97                         leftop = RegExp.$1 + leftop;
     98                         p -= 1;
     99                     }
    100                     leftop += left.substring(pos + 1, left.length);
    101                     leftop = leftop.replace(/([()+*%^\-/\][])/g, "\\$1");
    102                 } else {
    103                     throw new Error("JSXGraph: Missing '(' in expression");
    104                 }
    105             } else {
    106                 // Otherwise, the operand has to be a constant (or variable).
    107                 leftop = "[\\w\\.]+"; // former: \\w\\.
    108             }
    109
    110             // To the right of the ^ operator there also may be a function or method call
    111             // or a term in parenthesis. Alos, ere we search for the closing
    112             // parenthesis.
    113             if (right.match(/^([\w.]*\()/)) {
    114                 count = 1;
    115                 pos = RegExp.$1.length;
    116
    117                 while (pos < right.length && count > 0) {
    118                     c = right.charAt(pos);
    119
    120                     if (c === ")") {
    121                         count -= 1;
    122                     } else if (c === "(") {
    123                         count += 1;
    124                     }
    125                     pos += 1;
    126                 }
    127
    128                 if (count === 0) {
    129                     rightop = right.substring(0, pos);
    130                     rightop = rightop.replace(/([()+*%^\-/[\]])/g, "\\$1");
    131                 } else {
    132                     throw new Error("JSXGraph: Missing ')' in expression");
    133                 }
    134             } else {
    135                 // Otherwise, the operand has to be a constant (or variable).
    136                 rightop = "[\\w\\.]+";
    137             }
    138             // Now, we have the two operands and replace ^ by JXG.Math.pow
    139             expr = new RegExp("(" + leftop + ")\\^(" + rightop + ")");
    140             //te = te.replace(expr, 'JXG.Math.pow($1,$2)');
    141             te = te.replace(expr, "pow($1,$2)");
    142             i = te.indexOf("^");
    143         }
    144
    145         return te;
    146     },
    147
    148     /**
    149      * Converts expression of the form <i>If(a,b,c)</i> into <i>(a)?(b):(c)/i>.
    150      * @param {String} te Expression of the form <i>If(a,b,c)</i>
    151      * @returns {String} Converted expression.
    152      */
    153     replaceIf: function (te) {
    154         var left,
    155             right,
    156             i,
    157             pos,
    158             count,
    159             k1,
    160             k2,
    161             c,
    162             meat,
    163             s = "",
    164             first = null,
    165             second = null,
    166             third = null;
    167
    168         i = te.indexOf("If(");
    169         if (i < 0) {
    170             return te;
    171         }
    172
    173         // "" means not defined. Here, we replace it by 0
    174         te = te.replace(/""/g, "0");
    175         while (i >= 0) {
    176             left = te.slice(0, i);
    177             right = te.slice(i + 3);
    178
    179             // Search the end of the If() command and take out the meat
    180             count = 1;
    181             pos = 0;
    182             k1 = -1;
    183             k2 = -1;
    184
    185             while (pos < right.length && count > 0) {
    186                 c = right.charAt(pos);
    187
    188                 if (c === ")") {
    189                     count -= 1;
    190                 } else if (c === "(") {
    191                     count += 1;
    192                 } else if (c === "," && count === 1) {
    193                     if (k1 < 0) {
    194                         // first komma
    195                         k1 = pos;
    196                     } else {
    197                         // second komma
    198                         k2 = pos;
    199                     }
    200                 }
    201                 pos += 1;
    202             }
    203             meat = right.slice(0, pos - 1);
    204             right = right.slice(pos);
    205
    206             // Test the two kommas
    207             if (k1 < 0) {
    208                 // , missing
    209                 return "";
    210             }
    211
    212             if (k2 < 0) {
    213                 // , missing
    214                 return "";
    215             }
    216
    217             first = meat.slice(0, k1);
    218             second = meat.slice(k1 + 1, k2);
    219             third = meat.slice(k2 + 1);
    220
    221             // Recurse
    222             first = this.replaceIf(first);
    223             second = this.replaceIf(second);
    224             third = this.replaceIf(third);
    225
    226             s += left + "((" + first + ")?" + "(" + second + "):(" + third + "))";
    227             te = right;
    228             first = null;
    229             second = null;
    230             i = te.indexOf("If(");
    231         }
    232         s += right;
    233         return s;
    234     },
    235
    236     /**
    237      * Replace an element's name in terms by an element's id.
    238      * @param {String} term Term containing names of elements.
    239      * @param {JXG.Board} board Reference to the board the elements are on.
    240      * @param {Boolean} [jc=false] If true, all id's will be surrounded by <tt>$('</tt> and <tt>')</tt>.
    241      * @returns {String} The same string with names replaced by ids.
    242      **/
    243     replaceNameById: function (term, board, jc) {
    244         var end,
    245             elName,
    246             el,
    247             i,
    248             pos = 0,
    249             funcs = ["X", "Y", "L", "V"],
    250             printId = function (id) {
    251                 if (jc) {
    252                     return "$('" + id + "')";
    253                 }
    254
    255                 return id;
    256             };
    257
    258         // Find X(el), Y(el), ...
    259         // All functions declared in funcs
    260         for (i = 0; i < funcs.length; i++) {
    261             pos = term.indexOf(funcs[i] + "(");
    262
    263             while (pos >= 0) {
    264                 if (pos >= 0) {
    265                     end = term.indexOf(")", pos + 2);
    266                     if (end >= 0) {
    267                         elName = term.slice(pos + 2, end);
    268                         elName = elName.replace(/\\(['"])?/g, "$1");
    269                         el = board.elementsByName[elName];
    270
    271                         if (el) {
    272                             term =
    273                                 term.slice(0, pos + 2) +
    274                                 (jc ? "$('" : "") +
    275                                 printId(el.id) +
    276                                 term.slice(end);
    277                         }
    278                     }
    279                 }
    280                 end = term.indexOf(")", pos + 2);
    281                 pos = term.indexOf(funcs[i] + "(", end);
    282             }
    283         }
    284
    285         pos = term.indexOf("Dist(");
    286         while (pos >= 0) {
    287             if (pos >= 0) {
    288                 end = term.indexOf(",", pos + 5);
    289                 if (end >= 0) {
    290                     elName = term.slice(pos + 5, end);
    291                     elName = elName.replace(/\\(['"])?/g, "$1");
    292                     el = board.elementsByName[elName];
    293
    294                     if (el) {
    295                         term = term.slice(0, pos + 5) + printId(el.id) + term.slice(end);
    296                     }
    297                 }
    298             }
    299             end = term.indexOf(",", pos + 5);
    300             pos = term.indexOf(",", end);
    301             end = term.indexOf(")", pos + 1);
    302
    303             if (end >= 0) {
    304                 elName = term.slice(pos + 1, end);
    305                 elName = elName.replace(/\\(['"])?/g, "$1");
    306                 el = board.elementsByName[elName];
    307
    308                 if (el) {
    309                     term = term.slice(0, pos + 1) + printId(el.id) + term.slice(end);
    310                 }
    311             }
    312             end = term.indexOf(")", pos + 1);
    313             pos = term.indexOf("Dist(", end);
    314         }
    315
    316         funcs = ["Deg", "Rad"];
    317         for (i = 0; i < funcs.length; i++) {
    318             pos = term.indexOf(funcs[i] + "(");
    319             while (pos >= 0) {
    320                 if (pos >= 0) {
    321                     end = term.indexOf(",", pos + 4);
    322                     if (end >= 0) {
    323                         elName = term.slice(pos + 4, end);
    324                         elName = elName.replace(/\\(['"])?/g, "$1");
    325                         el = board.elementsByName[elName];
    326
    327                         if (el) {
    328                             term = term.slice(0, pos + 4) + printId(el.id) + term.slice(end);
    329                         }
    330                     }
    331                 }
    332
    333                 end = term.indexOf(",", pos + 4);
    334                 pos = term.indexOf(",", end);
    335                 end = term.indexOf(",", pos + 1);
    336
    337                 if (end >= 0) {
    338                     elName = term.slice(pos + 1, end);
    339                     elName = elName.replace(/\\(['"])?/g, "$1");
    340                     el = board.elementsByName[elName];
    341
    342                     if (el) {
    343                         term = term.slice(0, pos + 1) + printId(el.id) + term.slice(end);
    344                     }
    345                 }
    346
    347                 end = term.indexOf(",", pos + 1);
    348                 pos = term.indexOf(",", end);
    349                 end = term.indexOf(")", pos + 1);
    350
    351                 if (end >= 0) {
    352                     elName = term.slice(pos + 1, end);
    353                     elName = elName.replace(/\\(['"])?/g, "$1");
    354                     el = board.elementsByName[elName];
    355                     if (el) {
    356                         term = term.slice(0, pos + 1) + printId(el.id) + term.slice(end);
    357                     }
    358                 }
    359
    360                 end = term.indexOf(")", pos + 1);
    361                 pos = term.indexOf(funcs[i] + "(", end);
    362             }
    363         }
    364
    365         return term;
    366     },
    367
    368     /**
    369      * Replaces element ids in terms by element this.board.objects['id'].
    370      * @param {String} term A GEONE<sub>x</sub>T function string with JSXGraph ids in it.
    371      * @returns {String} The input string with element ids replaced by this.board.objects["id"].
    372      **/
    373     replaceIdByObj: function (term) {
    374         // Search for expressions like "X(gi23)" or "Y(gi23A)" and convert them to objects['gi23'].X().
    375         var expr = /(X|Y|L)\(([\w_]+)\)/g;
    376         term = term.replace(expr, "$('$2').$1()");
    377
    378         expr = /(V)\(([\w_]+)\)/g;
    379         term = term.replace(expr, "$('$2').Value()");
    380
    381         expr = /(Dist)\(([\w_]+),([\w_]+)\)/g;
    382         term = term.replace(expr, "dist($('$2'), $('$3'))");
    383
    384         expr = /(Deg)\(([\w_]+),([ \w[\w_]+),([\w_]+)\)/g;
    385         term = term.replace(expr, "deg($('$2'),$('$3'),$('$4'))");
    386
    387         // Search for Rad('gi23','gi24','gi25')
    388         expr = /Rad\(([\w_]+),([\w_]+),([\w_]+)\)/g;
    389         term = term.replace(expr, "rad($('$1'),$('$2'),$('$3'))");
    390
    391         // it's ok, it will run through the jessiecode parser afterwards...
    392         /*jslint regexp: true*/
    393         expr = /N\((.+)\)/g;
    394         term = term.replace(expr, "($1)");
    395
    396         return term;
    397     },
    398
    399     /**
    400      * Converts the given algebraic expression in GEONE<sub>x</sub>T syntax into an equivalent expression in JavaScript syntax.
    401      * @param {String} term Expression in GEONExT syntax
    402      * @param {JXG.Board} board
    403      * @returns {String} Given expression translated to JavaScript.
    404      */
    405     geonext2JS: function (term, board) {
    406         var expr,
    407             newterm,
    408             i,
    409             from = [
    410                 "Abs",
    411                 "ACos",
    412                 "ASin",
    413                 "ATan",
    414                 "Ceil",
    415                 "Cos",
    416                 "Exp",
    417                 "Factorial",
    418                 "Floor",
    419                 "Log",
    420                 "Max",
    421                 "Min",
    422                 "Random",
    423                 "Round",
    424                 "Sin",
    425                 "Sqrt",
    426                 "Tan",
    427                 "Trunc"
    428             ],
    429             to = [
    430                 "abs",
    431                 "acos",
    432                 "asin",
    433                 "atan",
    434                 "ceil",
    435                 "cos",
    436                 "exp",
    437                 "factorial",
    438                 "floor",
    439                 "log",
    440                 "max",
    441                 "min",
    442                 "random",
    443                 "round",
    444                 "sin",
    445                 "sqrt",
    446                 "tan",
    447                 "ceil"
    448             ];
    449
    450         // Hacks, to enable not well formed XML, @see JXG.GeonextReader#replaceLessThan
    451         term = term.replace(/</g, "<");
    452         term = term.replace(/>/g, ">");
    453         term = term.replace(/&/g, "&");
    454
    455         // Convert GEONExT syntax to JavaScript syntax
    456         newterm = term;
    457         newterm = this.replaceNameById(newterm, board);
    458         newterm = this.replaceIf(newterm);
    459         // Exponentiations-Problem x^y -> Math(exp(x,y).
    460         newterm = this.replacePow(newterm);
    461         newterm = this.replaceIdByObj(newterm);
    462
    463         for (i = 0; i < from.length; i++) {
    464             // sin -> Math.sin and asin -> Math.asin
    465             expr = new RegExp(["(\\W|^)(", from[i], ")"].join(""), "ig");
    466             newterm = newterm.replace(expr, ["$1", to[i]].join(""));
    467         }
    468         newterm = newterm.replace(/True/g, "true");
    469         newterm = newterm.replace(/False/g, "false");
    470         newterm = newterm.replace(/fasle/g, "false");
    471         newterm = newterm.replace(/Pi/g, "PI");
    472         newterm = newterm.replace(/"/g, "'");
    473
    474         return newterm;
    475     },
    476
    477     /**
    478      * Finds dependencies in a given term and resolves them by adding the
    479      * dependent object to the found objects child elements.
    480      * @param {JXG.GeometryElement} me Object depending on objects in given term.
    481      * @param {String} term String containing dependencies for the given object.
    482      * @param {JXG.Board} [board=me.board] Reference to a board
    483      */
    484     findDependencies: function (me, term, board) {
    485         var elements, el, expr, elmask;
    486
    487         if (!Type.exists(board)) {
    488             board = me.board;
    489         }
    490
    491         elements = board.elementsByName;
    492
    493         for (el in elements) {
    494             if (elements.hasOwnProperty(el)) {
    495                 if (el !== me.name) {
    496                     if (elements[el].elementClass === Const.OBJECT_CLASS_TEXT) {
    497                         if (!Type.evaluate(elements[el].visProp.islabel)) {
    498                             elmask = el.replace(/\[/g, "\\[");
    499                             elmask = elmask.replace(/\]/g, "\\]");
    500
    501                             // Searches (A), (A,B),(A,B,C)
    502                             expr = new RegExp(
    503                                 "\\(([\\w\\[\\]'_ ]+,)*(" + elmask + ")(,[\\w\\[\\]'_ ]+)*\\)",
    504                                 "g"
    505                             );
    506
    507                             if (term.search(expr) >= 0) {
    508                                 elements[el].addChild(me);
    509                             }
    510                         }
    511                     } else {
    512                         elmask = el.replace(/\[/g, "\\[");
    513                         elmask = elmask.replace(/\]/g, "\\]");
    514
    515                         // Searches (A), (A,B),(A,B,C)
    516                         expr = new RegExp(
    517                             "\\(([\\w\\[\\]'_ ]+,)*(" + elmask + ")(,[\\w\\[\\]'_ ]+)*\\)",
    518                             "g"
    519                         );
    520
    521                         if (term.search(expr) >= 0) {
    522                             elements[el].addChild(me);
    523                         }
    524                     }
    525                 }
    526             }
    527         }
    528     },
    529
    530     /**
    531      * Converts the given algebraic expression in GEONE<sub>x</sub>T syntax into an equivalent expression in JessieCode syntax.
    532      * @param {String} term Expression in GEONExT syntax
    533      * @param {JXG.Board} board
    534      * @returns {String} Given expression translated to JavaScript.
    535      */
    536     gxt2jc: function (term, board) {
    537         var newterm;
    538             // from = ["Sqrt"],
    539             // to = ["sqrt"];
    540
    541         // Hacks, to enable not well formed XML, @see JXG.GeonextReader#replaceLessThan
    542         term = term.replace(/</g, "<");
    543         term = term.replace(/>/g, ">");
    544         term = term.replace(/&/g, "&");
    545         newterm = term;
    546         newterm = this.replaceNameById(newterm, board, true);
    547         newterm = newterm.replace(/True/g, "true");
    548         newterm = newterm.replace(/False/g, "false");
    549         newterm = newterm.replace(/fasle/g, "false");
    550
    551         return newterm;
    552     }
    553 };
    554
    555 export default JXG.GeonextParser;
    556