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