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 (!elements[el].evalVisProp('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