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