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