1 /*
  2     Copyright 2008-2022
  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 /*global JXG: true, define: true, window: true*/
 33 /*jslint nomen: true, plusplus: true*/
 34 
 35 /* depends:
 36  jxg
 37  base/constants
 38  base/coords
 39  base/element
 40  parser/geonext
 41  math/statistics
 42  utils/env
 43  utils/type
 44  */
 45 
 46 /**
 47  * @fileoverview In this file the Text element is defined.
 48  */
 49 
 50 define([
 51     'jxg', 'base/constants', 'base/element', 'parser/geonext',
 52     'utils/env', 'utils/type', 'math/math', 'base/coordselement'
 53 ], function (JXG, Const, GeometryElement, GeonextParser, Env, Type, Mat, CoordsElement) {
 54 
 55     "use strict";
 56 
 57     var priv = {
 58         HTMLSliderInputEventHandler: function () {
 59             this._val = parseFloat(this.rendNodeRange.value);
 60             this.rendNodeOut.value = this.rendNodeRange.value;
 61             this.board.update();
 62         }
 63     };
 64 
 65     /**
 66      * Construct and handle texts.
 67      *
 68      * The coordinates can be relative to the coordinates of an element
 69      * given in {@link JXG.Options#text.anchor}.
 70      *
 71      * MathJax, HTML and GEONExT syntax can be handled.
 72      * @class Creates a new text object. Do not use this constructor to create a text. Use {@link JXG.Board#create} with
 73      * type {@link Text} instead.
 74      * @augments JXG.GeometryElement
 75      * @augments JXG.CoordsElement
 76      * @param {string|JXG.Board} board The board the new text is drawn on.
 77      * @param {Array} coordinates An array with the user coordinates of the text.
 78      * @param {Object} attributes An object containing visual properties and optional a name and a id.
 79      * @param {string|function} content A string or a function returning a string.
 80      *
 81      */
 82     JXG.Text = function (board, coords, attributes, content) {
 83         this.constructor(board, attributes, Const.OBJECT_TYPE_TEXT, Const.OBJECT_CLASS_TEXT);
 84 
 85         this.element = this.board.select(attributes.anchor);
 86         this.coordsConstructor(coords, Type.evaluate(this.visProp.islabel));
 87 
 88         this.content = '';
 89         this.plaintext = '';
 90         this.plaintextOld = null;
 91         this.orgText = '';
 92 
 93         this.needsSizeUpdate = false;
 94         // Only used by infobox anymore
 95         this.hiddenByParent = false;
 96 
 97         /**
 98          * Width and height of the the text element in pixel.
 99          *
100          * @private
101          * @type Array
102          */
103         this.size = [1.0, 1.0];
104         this.id = this.board.setId(this, 'T');
105 
106         this.board.renderer.drawText(this);
107         this.board.finalizeAdding(this);
108 
109         // Set text before drawing
110         // this._createFctUpdateText(content);
111         // this.updateText();
112 
113         this.setText(content);
114 
115         if (Type.isString(this.content)) {
116             this.notifyParents(this.content);
117         }
118         this.elType = 'text';
119 
120         this.methodMap = Type.deepCopy(this.methodMap, {
121             setText: 'setTextJessieCode',
122             // free: 'free',
123             move: 'setCoords'
124         });
125     };
126 
127     JXG.Text.prototype = new GeometryElement();
128     Type.copyPrototypeMethods(JXG.Text, CoordsElement, 'coordsConstructor');
129 
130     JXG.extend(JXG.Text.prototype, /** @lends JXG.Text.prototype */ {
131         /**
132          * @private
133          * Test if the the screen coordinates (x,y) are in a small stripe
134          * at the left side or at the right side of the text.
135          * Sensitivity is set in this.board.options.precision.hasPoint.
136          * If dragarea is set to 'all' (default), tests if the the screen
137          * coordinates (x,y) are in within the text boundary.
138          * @param {Number} x
139          * @param {Number} y
140          * @returns {Boolean}
141          */
142         hasPoint: function (x, y) {
143             var lft, rt, top, bot, ax, ay, type, r;
144 
145             if (Type.isObject(Type.evaluate(this.visProp.precision))) {
146                 type = this.board._inputDevice;
147                 r = Type.evaluate(this.visProp.precision[type]);
148             } else {
149                 // 'inherit'
150                 r = this.board.options.precision.hasPoint;
151             }
152             if (this.transformations.length > 0) {
153                 //Transform the mouse/touch coordinates
154                 // back to the original position of the text.
155                 lft = Mat.matVecMult(Mat.inverse(this.board.renderer.joinTransforms(this, this.transformations)), [1, x, y]);
156                 x = lft[1];
157                 y = lft[2];
158             }
159 
160             ax = this.getAnchorX();
161             if (ax === 'right') {
162                 lft = this.coords.scrCoords[1] - this.size[0];
163             } else if (ax === 'middle') {
164                 lft = this.coords.scrCoords[1] - 0.5 * this.size[0];
165             } else {
166                 lft = this.coords.scrCoords[1];
167             }
168             rt = lft + this.size[0];
169 
170             ay = this.getAnchorY();
171             if (ay === 'top') {
172                 bot = this.coords.scrCoords[2] + this.size[1];
173             } else if (ay === 'middle') {
174                 bot = this.coords.scrCoords[2] + 0.5 * this.size[1];
175             } else {
176                 bot = this.coords.scrCoords[2];
177             }
178             top = bot - this.size[1];
179 
180             if (Type.evaluate(this.visProp.dragarea) === 'all') {
181                 return x >= lft - r && x < rt + r && y >= top - r && y <= bot + r;
182             }
183             // e.g. 'small'
184             return (y >= top - r && y <= bot + r) &&
185                 ((x >= lft - r && x <= lft + 2 * r) ||
186                     (x >= rt - 2 * r && x <= rt + r));
187         },
188 
189         /**
190          * This sets the updateText function of this element depending on the type of text content passed.
191          * Used by {@link JXG.Text#_setText} and {@link JXG.Text} constructor.
192          * @param {String|Function|Number} text
193          * @private
194          */
195         _createFctUpdateText: function (text) {
196             var updateText, resolvedText,
197                 ev_p = Type.evaluate(this.visProp.parse),
198                 ev_um = Type.evaluate(this.visProp.usemathjax),
199                 ev_uk = Type.evaluate(this.visProp.usekatex),
200                 convertJessieCode = false;
201 
202             this.orgText = text;
203 
204             if (Type.isFunction(text)) {
205                 // <value> tags will not be evaluated if text is provided by a function
206                 this.updateText = function () {
207                     resolvedText = text().toString();  // Evaluate function
208                     if (ev_p && !ev_um && !ev_uk) {
209                         this.plaintext = this.replaceSub(this.replaceSup(this.convertGeonextAndSketchometry2CSS(resolvedText)));
210                     } else {
211                         this.plaintext = resolvedText;
212                     }
213                 };
214 
215             } else {
216 
217                 if (Type.isNumber(text)) {
218                     this.content = Type.toFixed(text, Type.evaluate(this.visProp.digits));
219                 } else if (Type.isString(text) && ev_p) {
220 
221                     if (Type.evaluate(this.visProp.useasciimathml)) {   // ASCIIMathML
222                         this.content = "'`" + text + "`'";
223 
224                     } else if (ev_um || ev_uk) {                        // MathJax or KaTeX
225                         // Replace value-tags by functions
226                         this.content = this.valueTagToJessieCode(text);
227                         this.content = this.content.replace(/\\/g, "\\\\"); // Replace single backshlash by double
228 
229                     } else {
230                         // No TeX involved.
231                         // Converts GEONExT syntax into JavaScript string
232                         // Short math is allowed
233                         // Replace value-tags by functions
234                         // Avoid geonext2JS calls
235                         this.content = this.poorMansTeX(this.valueTagToJessieCode(text));
236                     }
237                     convertJessieCode = true;
238                 }
239 
240                 // Generate function which returns the text to be displayed
241                 if (convertJessieCode) {
242 
243                     // Convert JessieCode to JS function
244                     updateText = this.board.jc.snippet(this.content, true, '', false);
245 
246                     // Ticks have been esacped in valueTagToJessieCode
247                     this.updateText = function () {
248                         this.plaintext = this.unescapeTicks(updateText());
249                     };
250                 } else {
251                     this.updateText = function () {
252                         this.plaintext = text;
253                     };
254 
255                 }
256             }
257         },
258 
259         /**
260          * Defines new content. This is used by {@link JXG.Text#setTextJessieCode} and {@link JXG.Text#setText}. This is required because
261          * JessieCode needs to filter all Texts inserted into the DOM and thus has to replace setText by setTextJessieCode.
262          * @param {String|Function|Number} text
263          * @returns {JXG.Text}
264          * @private
265          */
266         _setText: function (text) {
267             this._createFctUpdateText(text);
268 
269             // First evaluation of the string.
270             // We need this for display='internal' and Canvas
271             this.updateText();
272             this.fullUpdate();
273 
274             // We do not call updateSize for the infobox to speed up rendering
275             if (!this.board.infobox || this.id !== this.board.infobox.id) {
276                 this.updateSize();    // updateSize() is called at least once.
277             }
278 
279             // This may slow down canvas renderer
280             // if (this.board.renderer.type === 'canvas') {
281             //     this.board.fullUpdate();
282             // }
283 
284             return this;
285         },
286 
287         /**
288          * Defines new content but converts < and > to HTML entities before updating the DOM.
289          * @param {String|function} text
290          */
291         setTextJessieCode: function (text) {
292             var s;
293 
294             this.visProp.castext = text;
295             if (Type.isFunction(text)) {
296                 s = function () {
297                     return Type.sanitizeHTML(text());
298                 };
299             } else {
300                 if (Type.isNumber(text)) {
301                     s = text;
302                 } else {
303                     s = Type.sanitizeHTML(text);
304                 }
305             }
306 
307             return this._setText(s);
308         },
309 
310         /**
311          * Defines new content.
312          * @param {String|function} text
313          * @returns {JXG.Text} Reference to the text object.
314          */
315         setText: function (text) {
316             return this._setText(text);
317         },
318 
319         /**
320          * Recompute the width and the height of the text box.
321          * Updates the array {@link JXG.Text#size} with pixel values.
322          * The result may differ from browser to browser
323          * by some pixels.
324          * In canvas an old IEs we use a very crude estimation of the dimensions of
325          * the textbox.
326          * JSXGraph needs {@link JXG.Text#size} for applying rotations in IE and
327          * for aligning text.
328          *
329          * @return {[type]} [description]
330          */
331         updateSize: function () {
332             var tmp, that, node,
333                 ev_d = Type.evaluate(this.visProp.display);
334 
335             if (!Env.isBrowser || this.board.renderer.type === 'no') {
336                 return this;
337             }
338             node = this.rendNode;
339 
340             /**
341              * offsetWidth and offsetHeight seem to be supported for internal vml elements by IE10+ in IE8 mode.
342              */
343             if (ev_d === 'html' || this.board.renderer.type === 'vml') {
344                 if (Type.exists(node.offsetWidth)) {
345                     that = this;
346                     window.setTimeout(function () {
347                         that.size = [node.offsetWidth, node.offsetHeight];
348                         that.needsUpdate = true;
349                         that.updateRenderer();
350                     }, 0);
351                     // In case, there is non-zero padding or borders
352                     // the following approach does not longer work.
353                     // s = [node.offsetWidth, node.offsetHeight];
354                     // if (s[0] === 0 && s[1] === 0) { // Some browsers need some time to set offsetWidth and offsetHeight
355                     //     that = this;
356                     //     window.setTimeout(function () {
357                     //         that.size = [node.offsetWidth, node.offsetHeight];
358                     //         that.needsUpdate = true;
359                     //         that.updateRenderer();
360                     //     }, 0);
361                     // } else {
362                     //     this.size = s;
363                     // }
364                 } else {
365                     this.size = this.crudeSizeEstimate();
366                 }
367             } else if (ev_d === 'internal') {
368                 if (this.board.renderer.type === 'svg') {
369                     that = this;
370                     window.setTimeout(function () {
371                         try {
372                             tmp = node.getBBox();
373                             that.size = [tmp.width, tmp.height];
374                             that.needsUpdate = true;
375                             that.updateRenderer();
376                         } catch (e) {
377                         }
378                     }, 0);
379                 } else if (this.board.renderer.type === 'canvas') {
380                     this.size = this.crudeSizeEstimate();
381                 }
382             }
383 
384             return this;
385         },
386 
387         /**
388          * A very crude estimation of the dimensions of the textbox in case nothing else is available.
389          * @returns {Array}
390          */
391         crudeSizeEstimate: function () {
392             var ev_fs = parseFloat(Type.evaluate(this.visProp.fontsize));
393             return [ev_fs * this.plaintext.length * 0.45, ev_fs * 0.9];
394         },
395 
396         /**
397          * Decode unicode entities into characters.
398          * @param {String} string
399          * @returns {String}
400          */
401         utf8_decode: function (string) {
402             return string.replace(/&#x(\w+);/g, function (m, p1) {
403                 return String.fromCharCode(parseInt(p1, 16));
404             });
405         },
406 
407         /**
408          * Replace _{} by <sub>
409          * @param {String} te String containing _{}.
410          * @returns {String} Given string with _{} replaced by <sub>.
411          */
412         replaceSub: function (te) {
413             if (!te.indexOf) {
414                 return te;
415             }
416 
417             var j,
418                 i = te.indexOf('_{');
419 
420             // the regexp in here are not used for filtering but to provide some kind of sugar for label creation,
421             // i.e. replacing _{...} with <sub>...</sub>. What is passed would get out anyway.
422             /*jslint regexp: true*/
423 
424             while (i >= 0) {
425                 te = te.substr(0, i) + te.substr(i).replace(/_\{/, '<sub>');
426                 j = te.substr(i).indexOf('}');
427                 if (j >= 0) {
428                     te = te.substr(0, j) + te.substr(j).replace(/\}/, '</sub>');
429                 }
430                 i = te.indexOf('_{');
431             }
432 
433             i = te.indexOf('_');
434             while (i >= 0) {
435                 te = te.substr(0, i) + te.substr(i).replace(/_(.?)/, '<sub>$1</sub>');
436                 i = te.indexOf('_');
437             }
438 
439             return te;
440         },
441 
442         /**
443          * Replace ^{} by <sup>
444          * @param {String} te String containing ^{}.
445          * @returns {String} Given string with ^{} replaced by <sup>.
446          */
447         replaceSup: function (te) {
448             if (!te.indexOf) {
449                 return te;
450             }
451 
452             var j,
453                 i = te.indexOf('^{');
454 
455             // the regexp in here are not used for filtering but to provide some kind of sugar for label creation,
456             // i.e. replacing ^{...} with <sup>...</sup>. What is passed would get out anyway.
457             /*jslint regexp: true*/
458 
459             while (i >= 0) {
460                 te = te.substr(0, i) + te.substr(i).replace(/\^\{/, '<sup>');
461                 j = te.substr(i).indexOf('}');
462                 if (j >= 0) {
463                     te = te.substr(0, j) + te.substr(j).replace(/\}/, '</sup>');
464                 }
465                 i = te.indexOf('^{');
466             }
467 
468             i = te.indexOf('^');
469             while (i >= 0) {
470                 te = te.substr(0, i) + te.substr(i).replace(/\^(.?)/, '<sup>$1</sup>');
471                 i = te.indexOf('^');
472             }
473 
474             return te;
475         },
476 
477         /**
478          * Return the width of the text element.
479          * @returns {Array} [width, height] in pixel
480          */
481         getSize: function () {
482             return this.size;
483         },
484 
485         /**
486          * Move the text to new coordinates.
487          * @param {number} x
488          * @param {number} y
489          * @returns {object} reference to the text object.
490          */
491         setCoords: function (x, y) {
492             var coordsAnchor, dx, dy;
493             if (Type.isArray(x) && x.length > 1) {
494                 y = x[1];
495                 x = x[0];
496             }
497 
498             if (Type.evaluate(this.visProp.islabel) && Type.exists(this.element)) {
499                 coordsAnchor = this.element.getLabelAnchor();
500                 dx = (x - coordsAnchor.usrCoords[1]) * this.board.unitX;
501                 dy = -(y - coordsAnchor.usrCoords[2]) * this.board.unitY;
502 
503                 this.relativeCoords.setCoordinates(Const.COORDS_BY_SCREEN, [dx, dy]);
504             } else {
505                 /*
506                 this.X = function () {
507                     return x;
508                 };
509 
510                 this.Y = function () {
511                     return y;
512                 };
513                 */
514                 this.coords.setCoordinates(Const.COORDS_BY_USER, [x, y]);
515             }
516 
517             // this should be a local update, otherwise there might be problems
518             // with the tick update routine resulting in orphaned tick labels
519             this.fullUpdate();
520 
521             return this;
522         },
523 
524         /**
525          * Evaluates the text.
526          * Then, the update function of the renderer
527          * is called.
528          */
529         update: function (fromParent) {
530             if (!this.needsUpdate) {
531                 return this;
532             }
533 
534             this.updateCoords(fromParent);
535             this.updateText();
536 
537             if (Type.evaluate(this.visProp.display) === 'internal') {
538                 if (Type.isString(this.plaintext)) {
539                     this.plaintext = this.utf8_decode(this.plaintext);
540                 }
541             }
542 
543             this.checkForSizeUpdate();
544             if (this.needsSizeUpdate) {
545                 this.updateSize();
546             }
547 
548             return this;
549         },
550 
551         /**
552          * Used to save updateSize() calls.
553          * Called in JXG.Text.update
554          * That means this.update() has been called.
555          * More tests are in JXG.Renderer.updateTextStyle. The latter tests
556          * are one update off. But this should pose not too many problems, since
557          * it affects fontSize and cssClass changes.
558          *
559          * @private
560          */
561         checkForSizeUpdate: function () {
562             if (this.board.infobox && this.id === this.board.infobox.id) {
563                 this.needsSizeUpdate = false;
564             } else {
565                 // For some magic reason it is more efficient on the iPad to
566                 // call updateSize() for EVERY text element EVERY time.
567                 this.needsSizeUpdate = (this.plaintextOld !== this.plaintext);
568 
569                 if (this.needsSizeUpdate) {
570                     this.plaintextOld = this.plaintext;
571                 }
572             }
573 
574         },
575 
576         /**
577          * The update function of the renderert
578          * is called.
579          * @private
580          */
581         updateRenderer: function () {
582             if (//this.board.updateQuality === this.board.BOARD_QUALITY_HIGH &&
583                 Type.evaluate(this.visProp.autoposition)) {
584 
585                 this.setAutoPosition()
586                     .updateConstraint();
587             }
588             return this.updateRendererGeneric('updateText');
589         },
590 
591         /**
592          * Converts shortened math syntax into correct syntax:  3x instead of 3*x or
593          * (a+b)(3+1) instead of (a+b)*(3+1).
594          *
595          * @private
596          * @param{String} expr Math term
597          * @returns {string} expanded String
598          */
599         expandShortMath: function (expr) {
600             var re = /([)0-9.])\s*([(a-zA-Z_])/g;
601             return expr.replace(re, '$1*$2');
602         },
603 
604         /**
605          * Converts the GEONExT syntax of the <value> terms into JavaScript.
606          * Also, all Objects whose name appears in the term are searched and
607          * the text is added as child to these objects.
608          * This method is called if the attribute parse==true is set.
609          *
610          * @param{String} contentStr String to be parsed
611          * @param{Boolean} [expand] Optional flag if shortened math syntax is allowed (e.g. 3x instead of 3*x).
612          * @param{Boolean} [avoidGeonext2JS] Optional flag if geonext2JS should be called. For backwards compatibility
613          * this has to be set explicitely to true.
614          * @param{Boolean} [outputTeX] Optional flag which has to be true if the resulting term will be sent to MathJax or KaTeX.
615          * If true, "_" and "^" are NOT replaced by HTML tags sub and sup. Default: false, i.e. the replacement is done.
616          * This flag allows the combination of <value> tag containing calculations with TeX output.
617          *
618          * @private
619          * @see JXG.GeonextParser.geonext2JS
620          */
621         generateTerm: function (contentStr, expand, avoidGeonext2JS) {
622             var res, term, i, j,
623                 plaintext = '""';
624 
625             // Revert possible jc replacement
626             contentStr = contentStr || '';
627             contentStr = contentStr.replace(/\r/g, '');
628             contentStr = contentStr.replace(/\n/g, '');
629             contentStr = contentStr.replace(/"/g, '\'');
630             contentStr = contentStr.replace(/'/g, "\\'");
631 
632             // Old GEONExT syntax, not (yet) supported as TeX output.
633             // Otherwise, the else clause should be used.
634             // That means, i.e. the <arc> tag and <sqrt> tag are not
635             // converted into TeX syntax.
636             contentStr = contentStr.replace(/&arc;/g, '∠');
637             contentStr = contentStr.replace(/<arc\s*\/>/g, '∠');
638             contentStr = contentStr.replace(/<arc\s*\/>/g, '∠');
639             contentStr = contentStr.replace(/<sqrt\s*\/>/g, '√');
640 
641             contentStr = contentStr.replace(/<value>/g, '<value>');
642             contentStr = contentStr.replace(/<\/value>/g, '</value>');
643 
644             // Convert GEONExT syntax into  JavaScript syntax
645             i = contentStr.indexOf('<value>');
646             j = contentStr.indexOf('</value>');
647             if (i >= 0) {
648                 while (i >= 0) {
649                     plaintext += ' + "' + this.replaceSub(this.replaceSup(contentStr.slice(0, i))) + '"';
650                     // plaintext += ' + "' + this.replaceSub(contentStr.slice(0, i)) + '"';
651 
652                     term = contentStr.slice(i + 7, j);
653                     term = term.replace(/\s+/g, ''); // Remove all whitespace
654                     if (expand === true) {
655                         term = this.expandShortMath(term);
656                     }
657                     if (avoidGeonext2JS) {
658                         res = term;
659                     } else {
660                         res = GeonextParser.geonext2JS(term, this.board);
661                     }
662                     res = res.replace(/\\"/g, "'");
663                     res = res.replace(/\\'/g, "'");
664 
665                     // GEONExT-Hack: apply rounding once only.
666                     if (res.indexOf('toFixed') < 0) {
667                         // output of a value tag
668                         if (Type.isNumber((Type.bind(this.board.jc.snippet(res, true, '', false), this))())) {
669                             // may also be a string
670                             plaintext += '+(' + res + ').toFixed(' + (Type.evaluate(this.visProp.digits)) + ')';
671                         } else {
672                             plaintext += '+(' + res + ')';
673                         }
674                     } else {
675                         plaintext += '+(' + res + ')';
676                     }
677 
678                     contentStr = contentStr.slice(j + 8);
679                     i = contentStr.indexOf('<value>');
680                     j = contentStr.indexOf('</value>');
681                 }
682             }
683 
684             plaintext += ' + "' + this.replaceSub(this.replaceSup(contentStr)) + '"';
685             plaintext = this.convertGeonextAndSketchometry2CSS(plaintext);
686 
687             // This should replace e.g. &pi; by π
688             plaintext = plaintext.replace(/&/g, '&');
689             plaintext = plaintext.replace(/"/g, "'");
690 
691             return plaintext;
692         },
693 
694         valueTagToJessieCode: function(contentStr) {
695             var res, term, i, j,
696                 expandShortMath = true,
697                 textComps = [],
698                 tick = '"';
699 
700             contentStr = contentStr || '';
701             contentStr = contentStr.replace(/\r/g, '');
702             contentStr = contentStr.replace(/\n/g, '');
703 
704             contentStr = contentStr.replace(/<value>/g, '<value>');
705             contentStr = contentStr.replace(/<\/value>/g, '</value>');
706 
707             // Convert content of value tag (GEONExT/JessieCode) syntax into JavaScript syntax
708             i = contentStr.indexOf('<value>');
709             j = contentStr.indexOf('</value>');
710             if (i >= 0) {
711                 while (i >= 0) {
712                     // Add string fragment before <value> tag
713                     textComps.push(tick + this.escapeTicks(contentStr.slice(0, i)) + tick);
714 
715                     term = contentStr.slice(i + 7, j);
716                     term = term.replace(/\s+/g, ''); // Remove all whitespace
717                     if (expandShortMath === true) {
718                         term = this.expandShortMath(term);
719                     }
720                     res = term;
721                     res = res.replace(/\\"/g, "'").replace(/\\'/g, "'");
722 
723                     // Hack: apply rounding once only.
724                     if (res.indexOf('toFixed') < 0) {
725                         // Output of a value tag
726                         // Run the JessieCode parser
727                         if (Type.isNumber((Type.bind(this.board.jc.snippet(res, true, '', false), this))())) {
728                             // Output is number
729                             textComps.push('(' + res + ').toFixed(' + (Type.evaluate(this.visProp.digits)) + ')');
730                         } else {
731                             // Output is a string
732                             textComps.push('(' + res + ')');
733                         }
734                     } else {
735                         textComps.push('(' + res + ')');
736                     }
737                     contentStr = contentStr.slice(j + 8);
738                     i = contentStr.indexOf('<value>');
739                     j = contentStr.indexOf('</value>');
740                 }
741             }
742             // Add trailing string fragment
743             textComps.push(tick + this.escapeTicks(contentStr) + tick);
744 
745             return textComps.join(' + ').replace(/&/g, '&');
746         },
747 
748         poorMansTeX: function(s) {
749             s = s.replace(/<arc\s*\/*>/g, '∠')
750                 .replace(/<arc\s*\/*>/g, '∠')
751                 .replace(/<sqrt\s*\/*>/g, '√')
752                 .replace(/<sqrt\s*\/*>/g, '√');
753 
754             return this.convertGeonextAndSketchometry2CSS(this.replaceSub(this.replaceSup(s)));
755         },
756 
757         escapeTicks: function(s) {
758             return s.replace(/"/g, '%22').replace(/'/g, '%27');
759         },
760 
761         unescapeTicks: function(s) {
762             return s.replace(/%22/g, '"').replace(/%27/g, "'");
763         },
764 
765         /**
766          * Converts the GEONExT tags <overline> and <arrow> to
767          * HTML span tags with proper CSS formatting.
768          * @private
769          * @see JXG.Text.generateTerm
770          * @see JXG.Text._setText
771          */
772         convertGeonext2CSS: function (s) {
773             if (Type.isString(s)) {
774                 s = s.replace(
775                     /(<|<)overline(>|>)/g,
776                     '<span style=text-decoration:overline;>'
777                 );
778                 s = s.replace(
779                     /(<|<)\/overline(>|>)/g,
780                     '</span>'
781                 );
782                 s = s.replace(
783                     /(<|<)arrow(>|>)/g,
784                     '<span style=text-decoration:overline;>'
785                 );
786                 s = s.replace(
787                     /(<|<)\/arrow(>|>)/g,
788                     '</span>'
789                 );
790             }
791 
792             return s;
793         },
794 
795         /**
796          * Converts the sketchometry tag <sketchofont> to
797          * HTML span tags with proper CSS formatting.
798          * @private
799          * @see JXG.Text.generateTerm
800          * @see JXG.Text._setText
801          */
802         convertSketchometry2CSS: function (s) {
803             if (Type.isString(s)) {
804                 s = s.replace(
805                     /(<|<)sketchofont(>|>)/g,
806                     '<span style=font-family:sketchometry-light;font-weight:500;>'
807                 );
808                 s = s.replace(
809                     /(<|<)\/sketchofont(>|>)/g,
810                     '</span>'
811                 );
812                 s = s.replace(
813                     /(<|<)sketchometry-light(>|>)/g,
814                     '<span style=font-family:sketchometry-light;font-weight:500;>'
815                 );
816                 s = s.replace(
817                     /(<|<)\/sketchometry-light(>|>)/g,
818                     '</span>'
819                 );
820             }
821 
822             return s;
823         },
824 
825         /**
826          * Alias for convertGeonext2CSS and convertSketchometry2CSS
827          * @private
828          * @see JXG.Text.convertGeonext2CSS
829          * @see JXG.Text.convertSketchometry2CSS
830          */
831         convertGeonextAndSketchometry2CSS: function (s){
832             s = this.convertGeonext2CSS(s);
833             s = this.convertSketchometry2CSS(s);
834             return s;
835         },
836 
837         /**
838          * Finds dependencies in a given term and notifies the parents by adding the
839          * dependent object to the found objects child elements.
840          * @param {String} content String containing dependencies for the given object.
841          * @private
842          */
843         notifyParents: function (content) {
844             var search,
845                 res = null;
846 
847             // revert possible jc replacement
848             content = content.replace(/<value>/g, '<value>');
849             content = content.replace(/<\/value>/g, '</value>');
850 
851             do {
852                 search = /<value>([\w\s*/^\-+()[\],<>=!]+)<\/value>/;
853                 res = search.exec(content);
854 
855                 if (res !== null) {
856                     GeonextParser.findDependencies(this, res[1], this.board);
857                     content = content.substr(res.index);
858                     content = content.replace(search, '');
859                 }
860             } while (res !== null);
861 
862             return this;
863         },
864 
865         // documented in element.js
866         getParents: function () {
867             var p;
868             if (this.relativeCoords !== undefined) { // Texts with anchor elements, excluding labels
869                 p = [this.relativeCoords.usrCoords[1], this.relativeCoords.usrCoords[2], this.orgText];
870             } else {                                 // Other texts
871                 p = [this.Z(), this.X(), this.Y(), this.orgText];
872             }
873 
874             if (this.parents.length !== 0) {
875                 p = this.parents;
876             }
877 
878             return p;
879         },
880 
881         bounds: function () {
882             var c = this.coords.usrCoords;
883 
884             if (Type.evaluate(this.visProp.islabel) || this.board.unitY === 0 || this.board.unitX === 0) {
885                 return [0, 0, 0, 0];
886             }
887             return [c[1], c[2] + this.size[1] / this.board.unitY, c[1] + this.size[0] / this.board.unitX, c[2]];
888         },
889 
890         getAnchorX: function () {
891             var a = Type.evaluate(this.visProp.anchorx);
892             if (a === 'auto') {
893                 switch (this.visProp.position) {
894                     case 'top':
895                     case 'bot':
896                         return 'middle';
897                     case 'rt':
898                     case 'lrt':
899                     case 'urt':
900                         return 'left';
901                     case 'lft':
902                     case 'llft':
903                     case 'ulft':
904                     default:
905                         return 'right';
906                 }
907             }
908             return a;
909         },
910 
911         getAnchorY: function () {
912             var a = Type.evaluate(this.visProp.anchory);
913             if (a === 'auto') {
914                 switch (this.visProp.position) {
915                     case 'top':
916                     case 'ulft':
917                     case 'urt':
918                         return 'bottom';
919                     case 'bot':
920                     case 'lrt':
921                     case 'llft':
922                         return 'top';
923                     case 'rt':
924                     case 'lft':
925                     default:
926                         return 'middle';
927                 }
928             }
929             return a;
930         },
931 
932         /**
933          * Computes the number of overlaps of a box of w pixels width, h pixels height
934          * and center (x, y)
935          *
936          * @private
937          * @param  {Number} x x-coordinate of the center (screen coordinates)
938          * @param  {Number} y y-coordinate of the center (screen coordinates)
939          * @param  {Number} w width of the box in pixel
940          * @param  {Number} h width of the box in pixel
941          * @return {Number}   Number of overlapping elements
942          */
943         getNumberofConflicts: function (x, y, w, h) {
944             var count = 0,
945                 i, obj, le,
946                 savePointPrecision;
947 
948             // Set the precision of hasPoint to half the max if label isn't too long
949             savePointPrecision = this.board.options.precision.hasPoint;
950             // this.board.options.precision.hasPoint = Math.max(w, h) * 0.5;
951             this.board.options.precision.hasPoint = (w + h) * 0.25;
952             // TODO:
953             // Make it compatible with the objects' visProp.precision attribute
954             for (i = 0, le = this.board.objectsList.length; i < le; i++) {
955                 obj = this.board.objectsList[i];
956                 if (obj.visPropCalc.visible &&
957                     obj.elType !== 'axis' &&
958                     obj.elType !== 'ticks' &&
959                     obj !== this.board.infobox &&
960                     obj !== this &&
961                     obj.hasPoint(x, y)) {
962 
963                     count++;
964                 }
965             }
966             this.board.options.precision.hasPoint = savePointPrecision;
967 
968             return count;
969         },
970 
971         /**
972          * Sets the offset of a label element to the position with the least number
973          * of overlaps with other elements, while retaining the distance to its
974          * anchor element. Twelve different angles are possible.
975          *
976          * @returns {JXG.Text} Reference to the text object.
977          */
978         setAutoPosition: function () {
979             var x, y, cx, cy,
980                 anchorCoords,
981                 // anchorX, anchorY,
982                 w = this.size[0],
983                 h = this.size[1],
984                 start_angle, angle,
985                 optimum = {
986                     conflicts: Infinity,
987                     angle: 0,
988                     r: 0
989                 },
990                 max_r, delta_r,
991                 conflicts, offset, r,
992                 num_positions = 12,
993                 step = 2 * Math.PI / num_positions,
994                 j, dx, dy, co, si;
995 
996             if (this === this.board.infobox ||
997                 !this.visPropCalc.visible ||
998                 !Type.evaluate(this.visProp.islabel) ||
999                 !this.element) {
1000                 return this;
1001             }
1002 
1003             // anchorX = Type.evaluate(this.visProp.anchorx);
1004             // anchorY = Type.evaluate(this.visProp.anchory);
1005             offset = Type.evaluate(this.visProp.offset);
1006             anchorCoords = this.element.getLabelAnchor();
1007             cx = anchorCoords.scrCoords[1];
1008             cy = anchorCoords.scrCoords[2];
1009 
1010             // Set dx, dy as the relative position of the center of the label
1011             // to its anchor element ignoring anchorx and anchory.
1012             dx = offset[0];
1013             dy = offset[1];
1014 
1015             conflicts = this.getNumberofConflicts(cx + dx, cy - dy, w, h);
1016             if (conflicts === 0) {
1017                 return this;
1018             }
1019             // console.log(this.id, conflicts, w, h);
1020             // r = Geometry.distance([0, 0], offset, 2);
1021 
1022             r = 12;
1023             max_r = 28;
1024             delta_r = 0.2 * r;
1025 
1026             start_angle = Math.atan2(dy, dx);
1027 
1028             optimum.conflicts = conflicts;
1029             optimum.angle = start_angle;
1030             optimum.r = r;
1031 
1032             while (optimum.conflicts > 0 && r < max_r) {
1033                 for (j = 1, angle = start_angle + step; j < num_positions && optimum.conflicts > 0; j++) {
1034                     co = Math.cos(angle);
1035                     si = Math.sin(angle);
1036 
1037                     x = cx + r * co;
1038                     y = cy - r * si;
1039 
1040                     conflicts = this.getNumberofConflicts(x, y, w, h);
1041                     if (conflicts < optimum.conflicts) {
1042                         optimum.conflicts = conflicts;
1043                         optimum.angle = angle;
1044                         optimum.r = r;
1045                     }
1046                     if (optimum.conflicts === 0) {
1047                         break;
1048                     }
1049                     angle += step;
1050                 }
1051                 r += delta_r;
1052             }
1053             // console.log(this.id, "after", optimum)
1054             r = optimum.r;
1055             co = Math.cos(optimum.angle);
1056             si = Math.sin(optimum.angle);
1057             this.visProp.offset = [r * co, r * si];
1058 
1059             if (co < -0.2) {
1060                 this.visProp.anchorx = 'right';
1061             } else if (co > 0.2) {
1062                 this.visProp.anchorx = 'left';
1063             } else {
1064                 this.visProp.anchorx = 'middle';
1065             }
1066 
1067             return this;
1068         }
1069     });
1070 
1071     /**
1072      * @class Construct and handle texts.
1073      *
1074      * The coordinates can be relative to the coordinates of an element
1075      * given in {@link JXG.Options#text.anchor}.
1076      *
1077      * MathJaX, HTML and GEONExT syntax can be handled.
1078      * @pseudo
1079      * @description
1080      * @name Text
1081      * @augments JXG.Text
1082      * @constructor
1083      * @type JXG.Text
1084      *
1085      * @param {number,function_number,function_number,function_String,function} z_,x,y,str Parent elements for text elements.
1086      *                     <p>
1087      *   Parent elements can be two or three elements of type number, a string containing a GEONE<sub>x</sub>T
1088      *   constraint, or a function which takes no parameter and returns a number. Every parent element determines one coordinate. If a coordinate is
1089      *   given by a number, the number determines the initial position of a free text. If given by a string or a function that coordinate will be constrained
1090      *   that means the user won't be able to change the texts's position directly by mouse because it will be calculated automatically depending on the string
1091      *   or the function's return value. If two parent elements are given the coordinates will be interpreted as 2D affine Euclidean coordinates, if three such
1092      *   parent elements are given they will be interpreted as homogeneous coordinates.
1093      *                     <p>
1094      *                     The text to display may be given as string or as function returning a string.
1095      *
1096      * There is the attribute 'display' which takes the values 'html' or 'internal'. In case of 'html' a HTML division tag is created to display
1097      * the text. In this case it is also possible to use ASCIIMathML. Incase of 'internal', a SVG or VML text element is used to display the text.
1098      * @see JXG.Text
1099      * @example
1100      * // Create a fixed text at position [0,1].
1101      *   var t1 = board.create('text',[0,1,"Hello World"]);
1102      * </pre><div class="jxgbox" id="JXG896013aa-f24e-4e83-ad50-7bc7df23f6b7" style="width: 300px; height: 300px;"></div>
1103      * <script type="text/javascript">
1104      *   var t1_board = JXG.JSXGraph.initBoard('JXG896013aa-f24e-4e83-ad50-7bc7df23f6b7', {boundingbox: [-3, 6, 5, -3], axis: true, showcopyright: false, shownavigation: false});
1105      *   var t1 = t1_board.create('text',[0,1,"Hello World"]);
1106      * </script><pre>
1107      * @example
1108      * // Create a variable text at a variable position.
1109      *   var s = board.create('slider',[[0,4],[3,4],[-2,0,2]]);
1110      *   var graph = board.create('text',
1111      *                        [function(x){ return s.Value();}, 1,
1112      *                         function(){return "The value of s is"+JXG.toFixed(s.Value(), 2);}
1113      *                        ]
1114      *                     );
1115      * </pre><div class="jxgbox" id="JXG5441da79-a48d-48e8-9e53-75594c384a1c" style="width: 300px; height: 300px;"></div>
1116      * <script type="text/javascript">
1117      *   var t2_board = JXG.JSXGraph.initBoard('JXG5441da79-a48d-48e8-9e53-75594c384a1c', {boundingbox: [-3, 6, 5, -3], axis: true, showcopyright: false, shownavigation: false});
1118      *   var s = t2_board.create('slider',[[0,4],[3,4],[-2,0,2]]);
1119      *   var t2 = t2_board.create('text',[function(x){ return s.Value();}, 1, function(){return "The value of s is "+JXG.toFixed(s.Value(), 2);}]);
1120      * </script><pre>
1121      * @example
1122      * // Create a text bound to the point A
1123      * var p = board.create('point',[0, 1]),
1124      *     t = board.create('text',[0, -1,"Hello World"], {anchor: p});
1125      *
1126      * </pre><div class="jxgbox" id="JXGff5a64b2-2b9a-11e5-8dd9-901b0e1b8723" style="width: 300px; height: 300px;"></div>
1127      * <script type="text/javascript">
1128      *     (function() {
1129      *         var board = JXG.JSXGraph.initBoard('JXGff5a64b2-2b9a-11e5-8dd9-901b0e1b8723',
1130      *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
1131      *     var p = board.create('point',[0, 1]),
1132      *         t = board.create('text',[0, -1,"Hello World"], {anchor: p});
1133      *
1134      *     })();
1135      *
1136      * </script><pre>
1137      *
1138      */
1139     JXG.createText = function (board, parents, attributes) {
1140         var t,
1141             attr = Type.copyAttributes(attributes, board.options, 'text'),
1142             coords = parents.slice(0, -1),
1143             content = parents[parents.length - 1];
1144 
1145         // downwards compatibility
1146         attr.anchor = attr.parent || attr.anchor;
1147         t = CoordsElement.create(JXG.Text, board, coords, attr, content);
1148 
1149         if (!t) {
1150             throw new Error("JSXGraph: Can't create text with parent types '" +
1151                 (typeof parents[0]) + "' and '" + (typeof parents[1]) + "'." +
1152                 "\nPossible parent types: [x,y], [z,x,y], [element,transformation]");
1153         }
1154 
1155         if (attr.rotate !== 0 && attr.display === 'internal') { // This is the default value, i.e. no rotation
1156             t.addRotation(attr.rotate);
1157         }
1158 
1159         return t;
1160     };
1161 
1162     JXG.registerElement('text', JXG.createText);
1163 
1164     /**
1165      * @class Labels are text objects tied to other elements like points, lines and curves.
1166      * Labels are handled internally by JSXGraph, only. There is NO constructor "board.create('label', ...)".
1167      *
1168      * @pseudo
1169      * @description
1170      * @name Label
1171      * @augments JXG.Text
1172      * @constructor
1173      * @type JXG.Text
1174      */
1175     //  See element.js#createLabel
1176 
1177     /**
1178      * [[x,y], [w px, h px], [range]
1179      */
1180     JXG.createHTMLSlider = function (board, parents, attributes) {
1181         var t, par,
1182             attr = Type.copyAttributes(attributes, board.options, 'htmlslider');
1183 
1184         if (parents.length !== 2 || parents[0].length !== 2 || parents[1].length !== 3) {
1185             throw new Error("JSXGraph: Can't create htmlslider with parent types '" +
1186                 (typeof parents[0]) + "' and '" + (typeof parents[1]) + "'." +
1187                 "\nPossible parents are: [[x,y], [min, start, max]]");
1188         }
1189 
1190         // backwards compatibility
1191         attr.anchor = attr.parent || attr.anchor;
1192         attr.fixed = attr.fixed || true;
1193 
1194         par = [parents[0][0], parents[0][1],
1195             '<form style="display:inline">' +
1196             '<input type="range" /><span></span><input type="text" />' +
1197             '</form>'];
1198 
1199         t = JXG.createText(board, par, attr);
1200         t.type = Type.OBJECT_TYPE_HTMLSLIDER;
1201 
1202         t.rendNodeForm = t.rendNode.childNodes[0];
1203 
1204         t.rendNodeRange = t.rendNodeForm.childNodes[0];
1205         t.rendNodeRange.min = parents[1][0];
1206         t.rendNodeRange.max = parents[1][2];
1207         t.rendNodeRange.step = attr.step;
1208         t.rendNodeRange.value = parents[1][1];
1209 
1210         t.rendNodeLabel = t.rendNodeForm.childNodes[1];
1211         t.rendNodeLabel.id = t.rendNode.id + '_label';
1212 
1213         if (attr.withlabel) {
1214             t.rendNodeLabel.innerHTML = t.name + '=';
1215         }
1216 
1217         t.rendNodeOut = t.rendNodeForm.childNodes[2];
1218         t.rendNodeOut.value = parents[1][1];
1219 
1220         try {
1221             t.rendNodeForm.id = t.rendNode.id + '_form';
1222             t.rendNodeRange.id = t.rendNode.id + '_range';
1223             t.rendNodeOut.id = t.rendNode.id + '_out';
1224         } catch (e) {
1225             JXG.debug(e);
1226         }
1227 
1228         t.rendNodeRange.style.width = attr.widthrange + 'px';
1229         t.rendNodeRange.style.verticalAlign = 'middle';
1230         t.rendNodeOut.style.width = attr.widthout + 'px';
1231 
1232         t._val = parents[1][1];
1233 
1234         if (JXG.supportsVML()) {
1235             /*
1236             * OnChange event is used for IE browsers
1237             * The range element is supported since IE10
1238             */
1239             Env.addEvent(t.rendNodeForm, 'change', priv.HTMLSliderInputEventHandler, t);
1240         } else {
1241             /*
1242             * OnInput event is used for non-IE browsers
1243             */
1244             Env.addEvent(t.rendNodeForm, 'input', priv.HTMLSliderInputEventHandler, t);
1245         }
1246 
1247         t.Value = function () {
1248             return this._val;
1249         };
1250 
1251         return t;
1252     };
1253 
1254     JXG.registerElement('htmlslider', JXG.createHTMLSlider);
1255 
1256     return {
1257         Text: JXG.Text,
1258         createText: JXG.createText,
1259         createHTMLSlider: JXG.createHTMLSlider
1260     };
1261 });
1262