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