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