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