1 /*
  2     Copyright 2008-2021
  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