1 /* See license.txt for terms of usage */
  2 
  3 define([
  4     "firebug/lib/trace",
  5     "firebug/lib/options",
  6     "firebug/lib/deprecated",
  7     "firebug/lib/xpcom"
  8 ],
  9 function(FBTrace, Options, Deprecated, Xpcom) {
 10 
 11 // ********************************************************************************************* //
 12 // Constants
 13 
 14 const Ci = Components.interfaces;
 15 const Cc = Components.classes;
 16 const Cu = Components.utils;
 17 
 18 const entityConverter = Xpcom.CCSV("@mozilla.org/intl/entityconverter;1", "nsIEntityConverter");
 19 
 20 const reNotWhitespace = /[^\s]/;
 21 
 22 
 23 var Str = {};
 24 
 25 // ********************************************************************************************* //
 26 // Whitespace and Entity conversions
 27 
 28 var entityConversionLists = Str.entityConversionLists =
 29 {
 30     normal : {
 31         whitespace : {
 32             "\t" : "\u200c\u2192",
 33             "\n" : "\u200c\u00b6",
 34             "\r" : "\u200c\u00ac",
 35             " "  : "\u200c\u00b7"
 36         }
 37     },
 38     reverse : {
 39         whitespace : {
 40             "	" : "\t",
 41             "
" : "\n",
 42             "\u200c\u2192" : "\t",
 43             "\u200c\u00b6" : "\n",
 44             "\u200c\u00ac" : "\r",
 45             "\u200c\u00b7" : " "
 46         }
 47     }
 48 };
 49 
 50 var normal = entityConversionLists.normal,
 51     reverse = entityConversionLists.reverse;
 52 
 53 function addEntityMapToList(ccode, entity)
 54 {
 55     var lists = Array.slice(arguments, 2),
 56         len = lists.length,
 57         ch = String.fromCharCode(ccode);
 58 
 59     for (var i = 0; i < len; i++)
 60     {
 61         var list = lists[i];
 62         normal[list]=normal[list] || {};
 63         normal[list][ch] = "&" + entity + ";";
 64         reverse[list]=reverse[list] || {};
 65         reverse[list]["&" + entity + ";"] = ch;
 66     }
 67 }
 68 
 69 var e = addEntityMapToList,
 70     white = "whitespace",
 71     text = "text",
 72     attr = "attributes",
 73     css = "css",
 74     editor = "editor";
 75 
 76 e(0x0000, "#0", text, attr, css, editor);
 77 e(0x0022, "quot", attr, css);
 78 e(0x0026, "amp", attr, text, css);
 79 e(0x0027, "apos", css);
 80 e(0x003c, "lt", attr, text, css);
 81 e(0x003e, "gt", attr, text, css);
 82 e(0xa9, "copy", text, editor);
 83 e(0xae, "reg", text, editor);
 84 e(0x2122, "trade", text, editor);
 85 
 86 // See http://en.wikipedia.org/wiki/Dash
 87 e(0x2012, "#8210", attr, text, editor); // figure dash
 88 e(0x2013, "ndash", attr, text, editor); // en dash
 89 e(0x2014, "mdash", attr, text, editor); // em dash
 90 e(0x2015, "#8213", attr, text, editor); // horizontal bar
 91 
 92 // See http://www.cs.tut.fi/~jkorpela/chars/spaces.html
 93 e(0x00a0, "nbsp", attr, text, white, editor);
 94 e(0x2002, "ensp", attr, text, white, editor);
 95 e(0x2003, "emsp", attr, text, white, editor);
 96 e(0x2004, "emsp13", attr, text, white, editor);
 97 e(0x2005, "emsp14", attr, text, white, editor);
 98 e(0x2007, "numsp", attr, text, white, editor);
 99 e(0x2008, "puncsp", attr, text, white, editor);
100 e(0x2009, "thinsp", attr, text, white, editor);
101 e(0x200a, "hairsp", attr, text, white, editor);
102 e(0x200b, "#8203", attr, text, white, editor); // zero-width space (ZWSP)
103 e(0x200c, "zwnj", attr, text, white, editor);
104 
105 e(0x202f, "#8239", attr, text, white, editor); // NARROW NO-BREAK SPACE
106 e(0x205f, "#8287", attr, text, white, editor); // MEDIUM MATHEMATICAL SPACE
107 e(0x3000, "#12288", attr, text, white, editor); // IDEOGRAPHIC SPACE
108 e(0xfeff, "#65279", attr, text, white, editor); // ZERO WIDTH NO-BREAK SPACE
109 
110 e(0x200d, "zwj", attr, text, white, editor);
111 e(0x200e, "lrm", attr, text, white, editor);
112 e(0x200f, "rlm", attr, text, white, editor);
113 e(0x202d, "#8237", attr, text, white, editor); // left-to-right override
114 e(0x202e, "#8238", attr, text, white, editor); // right-to-left override
115 
116 // ********************************************************************************************* //
117 // Entity escaping
118 
119 var entityConversionRegexes =
120 {
121     normal : {},
122     reverse : {}
123 };
124 
125 var escapeEntitiesRegEx =
126 {
127     normal : function(list)
128     {
129         var chars = [];
130         for (var ch in list)
131             chars.push(ch);
132         return new RegExp("([" + chars.join("") + "])", "gm");
133     },
134     reverse : function(list)
135     {
136         var chars = [];
137         for (var ch in list)
138             chars.push(ch);
139         return new RegExp("(" + chars.join("|") + ")", "gm");
140     }
141 };
142 
143 function getEscapeRegexp(direction, lists)
144 {
145     var name = "";
146     var re;
147     var groups = [].concat(lists);
148     for (i = 0; i < groups.length; i++)
149         name += groups[i].group;
150     re = entityConversionRegexes[direction][name];
151     if (!re)
152     {
153         var list = {};
154         if (groups.length > 1)
155         {
156             for ( var i = 0; i < groups.length; i++)
157             {
158                 var aList = entityConversionLists[direction][groups[i].group];
159                 for ( var item in aList)
160                     list[item] = aList[item];
161             }
162         }
163         else if (groups.length==1)
164         {
165             list = entityConversionLists[direction][groups[0].group]; // faster for special case
166         }
167         else
168         {
169             list = {}; // perhaps should print out an error here?
170         }
171         re = entityConversionRegexes[direction][name] = escapeEntitiesRegEx[direction](list);
172     }
173     return re;
174 }
175 
176 function createSimpleEscape(name, direction)
177 {
178     return function(value)
179     {
180         var list = entityConversionLists[direction][name];
181         return String(value).replace(
182                 getEscapeRegexp(direction, {
183                     group : name,
184                     list : list
185                 }),
186                 function(ch)
187                 {
188                     return list[ch];
189                 }
190             );
191     }
192 }
193 
194 function escapeEntityAsName(char)
195 {
196     try
197     {
198         return entityConverter.ConvertToEntity(char, entityConverter.entityW3C);
199     }
200     catch(e)
201     {
202         return char;
203     }
204 }
205 
206 function escapeEntityAsUnicode(char)
207 {
208     var charCode = char.charCodeAt(0);
209 
210     if (charCode == 34)
211         return """;
212     else if (charCode == 38)
213         return "&";
214     else if (charCode < 32 || charCode >= 127)
215         return "&#" + charCode + ";";
216 
217     return char;
218 }
219 
220 function escapeGroupsForEntities(str, lists, type)
221 {
222     var results = [];
223     var noEntityString = "";
224     var textListIndex = -1;
225 
226     if (!type)
227         type = "names";
228 
229     for (var i = 0, listsLen = lists.length; i < listsLen; i++)
230     {
231         if (lists[i].group == "text")
232         {
233             textListIndex = i;
234             break;
235         }
236     }
237 
238     for (var i = 0, strLen = str.length; i < strLen; i++)
239     {
240         var result = str.charAt(i);
241 
242         // If there's "text" in the list groups, use a different
243         // method for converting the characters
244         if (textListIndex != -1)
245         {
246             if (type == "unicode")
247                 result = escapeEntityAsUnicode(str.charAt(i));
248             else if (type == "names")
249                 result = escapeEntityAsName(str.charAt(i));
250         }
251 
252         if (result != str.charAt(i))
253         {
254             if (noEntityString != "")
255             {
256                 results.push({
257                     "str": noEntityString,
258                     "class": "",
259                     "extra": ""
260                 });
261                 noEntityString = "";
262             }
263 
264             results.push({
265                 "str": result,
266                 "class": lists[textListIndex].class,
267                 "extra": lists[textListIndex].extra[result] ? lists[textListIndex].class
268                         + lists[textListIndex].extra[result] : ""
269             });
270         }
271         else
272         {
273             var listEntity;
274             for (var j = 0, listsLen = lists.length; j < listsLen; j++)
275             {
276                 var list = lists[j];
277                 if (list.group != "text")
278                 {
279                     listEntity = entityConversionLists.normal[list.group][result];
280                     if (listEntity)
281                     {
282                         result = listEntity;
283 
284                         if (noEntityString != "")
285                         {
286                             results.push({
287                                 "str": noEntityString,
288                                 "class": "",
289                                 "extra": ""
290                             });
291                             noEntityString = "";
292                         }
293 
294                         results.push({
295                             "str": result,
296                             "class": list.class,
297                             "extra": list.extra[result] ? list.class + list.extra[result] : ""
298                         });
299                         break;
300                     }
301                 }
302             }
303 
304             if (result == str.charAt(i))
305             {
306                 noEntityString += result;
307             }
308         }
309     }
310 
311     if (noEntityString != "")
312     {
313         results.push({
314             "str": noEntityString,
315             "class": "",
316             "extra": ""
317         });
318     }
319 
320     return results;
321 }
322 
323 Str.escapeGroupsForEntities = escapeGroupsForEntities;
324 
325 function unescapeEntities(str, lists)
326 {
327     var re = getEscapeRegexp("reverse", lists),
328         split = String(str).split(re),
329         len = split.length,
330         results = [],
331         cur, r, i, ri = 0, l, list;
332 
333     if (!len)
334         return str;
335 
336     lists = [].concat(lists);
337     for (i = 0; i < len; i++)
338     {
339         cur = split[i];
340         if (cur == '')
341             continue;
342 
343         for (l = 0; l < lists.length; l++)
344         {
345             list = lists[l];
346             r = entityConversionLists.reverse[list.group][cur];
347             if (r)
348             {
349                 results[ri] = r;
350                 break;
351             }
352         }
353 
354         if (!r)
355             results[ri] = cur;
356         ri++;
357     }
358     return results.join('') || '';
359 }
360 
361 // ********************************************************************************************* //
362 // String escaping
363 
364 var escapeForTextNode = Str.escapeForTextNode = createSimpleEscape("text", "normal");
365 var escapeForElementAttribute = Str.escapeForElementAttribute = createSimpleEscape("attributes", "normal");
366 Str.escapeForHtmlEditor = createSimpleEscape("editor", "normal");
367 Str.escapeForCss = createSimpleEscape("css", "normal");
368 
369 // deprecated compatibility functions
370 Str.deprecateEscapeHTML = createSimpleEscape("text", "normal");
371 Str.deprecatedUnescapeHTML = createSimpleEscape("text", "reverse");
372 
373 Str.escapeHTML = Deprecated.deprecated("use appropriate escapeFor... function",
374     Str.deprecateEscapeHTML);
375 Str.unescapeHTML = Deprecated.deprecated("use appropriate unescapeFor... function",
376     Str.deprecatedUnescapeHTML);
377 
378 var escapeForSourceLine = Str.escapeForSourceLine = createSimpleEscape("text", "normal");
379 
380 var unescapeWhitespace = createSimpleEscape("whitespace", "reverse");
381 
382 Str.unescapeForTextNode = function(str)
383 {
384     if (Options.get("showTextNodesWithWhitespace"))
385         str = unescapeWhitespace(str);
386 
387     if (Options.get("entityDisplay") == "names")
388         str = escapeForElementAttribute(str);
389 
390     return str;
391 };
392 
393 Str.unescapeForURL = createSimpleEscape('text', 'reverse');
394 
395 Str.escapeNewLines = function(value)
396 {
397     return value.replace(/\r/gm, "\\r").replace(/\n/gm, "\\n");
398 };
399 
400 Str.stripNewLines = function(value)
401 {
402     return typeof(value) == "string" ? value.replace(/[\r\n]/gm, " ") : value;
403 };
404 
405 Str.escapeSingleQuoteJS = function(value)
406 {
407     return value.replace("\\", "\\\\", "g").replace(/\r/gm, "\\r")
408                 .replace(/\n/gm, "\\n").replace("'", "\\'", "g");
409 };
410 
411 Str.reverseString = function(value)
412 {
413     return value.split("").reverse().join("");
414 };
415 
416 Str.escapeJS = function(value)
417 {
418     return value.replace("\\", "\\\\", "g").replace(/\r/gm, "\\r")
419         .replace(/\n/gm, "\\n").replace('"', '\\"', "g");
420 };
421 
422 Str.cropString = function(text, limit, alterText)
423 {
424     if (!alterText)
425         alterText = "...";
426 
427     // Make sure it's a string.
428     text = text + "";
429 
430     // Use default limit if necessary.
431     if (!limit)
432         limit = Options.get("stringCropLength");
433 
434     // Crop the string only if a limit is actually specified.
435     if (limit <= 0)
436         return text;
437 
438     var halfLimit = (limit / 2);
439     halfLimit -= 2; // adjustment for alterText's increase in size
440 
441     if (text.length > limit)
442         return text.substr(0, halfLimit) + alterText + text.substr(text.length-halfLimit);
443 
444     return text;
445 };
446 
447 Str.cropStringEx = function(text, limit, alterText, pivot)
448 {
449     if (!alterText)
450         alterText = "...";
451 
452     // Make sure it's a string.
453     text = text + "";
454 
455     // Use default limit if necessary.
456     if (!limit)
457         limit = Options.get("stringCropLength");
458 
459     // Crop the string only if a limit is actually specified.
460     if (limit <= 0)
461         return text;
462 
463     if (text.length < limit)
464         return text;
465 
466     if (typeof(pivot) == "undefined")
467         pivot = text.length / 2;
468 
469     var halfLimit = (limit / 2);
470 
471     // Adjust the pivot to the real center in case it's at an edge.
472     if (pivot < halfLimit)
473         pivot = halfLimit;
474 
475     if (pivot > text.length - halfLimit)
476         pivot = text.length - halfLimit;
477 
478     // Get substring around the pivot
479     var begin = Math.max(0, pivot - halfLimit);
480     var end = Math.min(text.length - 1, pivot + halfLimit);
481     var result = text.substring(begin, end);
482 
483     // Add alterText to the beginning or end of the result as necessary.
484     if (begin > 0)
485         result = alterText + result;
486 
487     if (end < text.length - 1)
488         result += alterText;
489 
490     return result;
491 };
492 
493 Str.lineBreak = function()
494 {
495     if (navigator.appVersion.indexOf("Win") != -1)
496         return "\r\n";
497 
498     if (navigator.appVersion.indexOf("Mac") != -1)
499         return "\r";
500 
501     return "\n";
502 };
503 
504 Str.cropMultipleLines = function(text, limit)
505 {
506     return this.escapeNewLines(this.cropString(text, limit));
507 };
508 
509 Str.isWhitespace = function(text)
510 {
511     return !reNotWhitespace.exec(text);
512 };
513 
514 Str.splitLines = function(text)
515 {
516     if (!text)
517         return [];
518 
519     const reSplitLines2 = /.*(:?\r\n|\n|\r)?/mg;
520     var lines;
521     if (text.match)
522     {
523         lines = text.match(reSplitLines2);
524     }
525     else
526     {
527         var str = text+"";
528         lines = str.match(reSplitLines2);
529     }
530     lines.pop();
531     return lines;
532 };
533 
534 Str.trim = function(text)
535 {
536     return text.replace(/^\s*|\s*$/g, "");
537 };
538 
539 Str.trimLeft = function(text)
540 {
541     return text.replace(/^\s+/, "");
542 };
543 
544 Str.trimRight = function(text)
545 {
546     return text.replace(/\s+$/, "");
547 };
548 
549 Str.hasPrefix = function(hay, needle)
550 {
551     // Passing empty string is ok, but null or undefined is not.
552     if (hay == null)
553     {
554         if (FBTrace.DBG_ERRORS)
555             FBTrace.sysout("Str.hasPrefix; string must not be null", {hay: hay, needle: needle});
556 
557         return false;
558     }
559 
560     // This is the fastest way of testing for prefixes - (hay.indexOf(needle) === 0)
561     // can be O(|hay|) in the worst case, and (hay.substr(0, needle.length) === needle)
562     // unnecessarily creates a new string and might be O(|needle|) in some JavaScript
563     // implementations. See the discussion in issue 3071.
564     return hay.lastIndexOf(needle, 0) === 0;
565 };
566 
567 Str.endsWith = function(str, suffix)
568 {
569     return str.indexOf(suffix, str.length - suffix.length) !== -1;
570 }
571 
572 // ********************************************************************************************* //
573 // HTML Wrap
574 
575 Str.wrapText = function(text, noEscapeHTML)
576 {
577     var reNonAlphaNumeric = /[^A-Za-z_$0-9'"-]/;
578 
579     var html = [];
580     var wrapWidth = Options.get("textWrapWidth");
581 
582     // Split long text into lines and put every line into a <code> element (only in case
583     // if noEscapeHTML is false). This is useful for automatic scrolling when searching
584     // within response body (in order to scroll we need an element).
585     // Don't use <pre> elements since this adds additional new line endings when copying
586     // selected source code using Firefox->Edit->Copy (Ctrl+C) (issue 2093).
587     var lines = Str.splitLines(text);
588     for (var i = 0; i < lines.length; ++i)
589     {
590         var line = lines[i];
591 
592         if (wrapWidth > 0)
593         {
594             while (line.length > wrapWidth)
595             {
596                 var m = reNonAlphaNumeric.exec(line.substr(wrapWidth, 100));
597                 var wrapIndex = wrapWidth + (m ? m.index : 0);
598                 var subLine = line.substr(0, wrapIndex);
599                 line = line.substr(wrapIndex);
600 
601                 if (!noEscapeHTML) html.push("<code class=\"wrappedText focusRow\" role=\"listitem\">");
602                 html.push(noEscapeHTML ? subLine : escapeForTextNode(subLine));
603                 if (!noEscapeHTML) html.push("</code>");
604             }
605         }
606 
607         if (!noEscapeHTML) html.push("<code class=\"wrappedText focusRow\" role=\"listitem\">");
608         html.push(noEscapeHTML ? line : escapeForTextNode(line));
609         if (!noEscapeHTML) html.push("</code>");
610     }
611 
612     return html;
613 };
614 
615 Str.insertWrappedText = function(text, textBox, noEscapeHTML)
616 {
617     var html = Str.wrapText(text, noEscapeHTML);
618     textBox.innerHTML = "<pre role=\"list\">" + html.join("") + "</pre>";
619 };
620 
621 // ********************************************************************************************* //
622 // Indent
623 
624 const reIndent = /^(\s+)/;
625 
626 function getIndent(line)
627 {
628     var m = reIndent.exec(line);
629     return m ? m[0].length : 0;
630 }
631 
632 Str.cleanIndentation = function(text)
633 {
634     var lines = Str.splitLines(text);
635 
636     var minIndent = -1;
637     for (var i = 0; i < lines.length; ++i)
638     {
639         var line = lines[i];
640         var indent = getIndent(line);
641         if (minIndent == -1 && line && !Str.isWhitespace(line))
642             minIndent = indent;
643         if (indent >= minIndent)
644             lines[i] = line.substr(minIndent);
645     }
646     return lines.join("");
647 };
648 
649 // ********************************************************************************************* //
650 // Formatting
651 
652 //deprecated compatibility functions
653 Str.deprecateEscapeHTML = createSimpleEscape("text", "normal");
654 
655 Str.formatNumber = Deprecated.deprecated("use <number>.toLocaleString() instead",
656     function(number) { return number.toLocaleString(); });
657 
658 Str.formatSize = function(bytes)
659 {
660     var negative = (bytes < 0);
661     bytes = Math.abs(bytes);
662 
663     // xxxHonza, XXXjjb: Why Firebug.sizePrecision is not set in Chromebug?
664     var sizePrecision = Options.get("sizePrecision");
665     if (typeof(sizePrecision) == "undefined")
666     {
667         Options.set("sizePrecision", 2);
668         sizePrecision = 2;
669     }
670 
671     // Get size precision (number of decimal places from the preferences)
672     // and make sure it's within limits.
673     sizePrecision = (sizePrecision > 2) ? 2 : sizePrecision;
674     sizePrecision = (sizePrecision < -1) ? -1 : sizePrecision;
675 
676     var result;
677 
678     if (sizePrecision == -1)
679         result = bytes + " B";
680 
681     var a = Math.pow(10, sizePrecision);
682 
683     if (bytes == -1 || bytes == undefined)
684         return "?";
685     else if (bytes == 0)
686         return "0 B";
687     else if (bytes < 1024)
688         result = bytes.toLocaleString() + " B";
689     else if (bytes < (1024*1024))
690         result = (Math.round((bytes/1024)*a)/a).toLocaleString() + " KB";
691     else
692         result = (Math.round((bytes/(1024*1024))*a)/a).toLocaleString() + " MB";
693 
694     return negative ? "-" + result : result;
695 };
696 
697 Str.formatTime = function(elapsed)
698 {
699     if (elapsed == -1)
700         return "";
701     else if (elapsed == 0)
702         return "0";
703     else if (elapsed < 1000)
704         return elapsed + "ms";
705     else if (elapsed < 60000)
706         return (Math.round(elapsed/10) / 100) + "s";
707     else
708     {
709         var min = Math.floor(elapsed/60000);
710         var sec = (elapsed % 60000);
711         return min + "m " + (Math.round((elapsed/1000)%60)) + "s";
712     }
713 };
714 
715 /**
716  * Formats an IPv4 or IPv6 address incl. port
717  * @param {String} address IP address to format
718  * @param {String} [port] IP port to format
719  * @returns {String} Formatted IP address
720  */
721 Str.formatIP = function(address, port)
722 {
723     if (!address || address == "")
724         return "";
725 
726     var result = address;
727     var isIPv6Address = address.indexOf(":") != -1;
728     if (isIPv6Address)
729         result = "["+result+"]";
730 
731     if (port && port != "")
732         result += ":"+port;
733 
734     return result;
735 };
736 
737 // ********************************************************************************************* //
738 // Conversions
739 
740 Str.convertToUnicode = function(text, charset)
741 {
742     if (!text)
743         return "";
744 
745     try
746     {
747         var conv = Cc["@mozilla.org/intl/scriptableunicodeconverter"].getService(
748             Ci.nsIScriptableUnicodeConverter);
749         conv.charset = charset ? charset : "UTF-8";
750         return conv.ConvertToUnicode(text);
751     }
752     catch (exc)
753     {
754         if (FBTrace.DBG_ERRORS)
755         {
756             FBTrace.sysout("Str.convertToUnicode: fails: for charset "+charset+" conv.charset:"+
757                 conv.charset+" exc: "+exc, exc);
758         }
759 
760         // the exception is worthless, make up a new one
761         throw new Error("Firebug failed to convert to unicode using charset: "+conv.charset+
762             " in @mozilla.org/intl/scriptableunicodeconverter");
763     }
764 };
765 
766 Str.convertFromUnicode = function(text, charset)
767 {
768     if (!text)
769         return "";
770 
771     try
772     {
773         var conv = Cc["@mozilla.org/intl/scriptableunicodeconverter"].createInstance(
774             Ci.nsIScriptableUnicodeConverter);
775         conv.charset = charset ? charset : "UTF-8";
776         return conv.ConvertFromUnicode(text);
777     }
778     catch (exc)
779     {
780         if (FBTrace.DBG_ERRORS)
781         {
782             FBTrace.sysout("Str.convertFromUnicode: fails: for charset "+charset+" conv.charset:"+
783                 conv.charset+" exc: "+exc, exc);
784         }
785     }
786 };
787 
788 // ********************************************************************************************* //
789 
790 Str.safeToString = function(ob)
791 {
792     try
793     {
794         if (!ob)
795         {
796             if (ob == undefined)
797                 return "undefined";
798             if (ob == null)
799                 return "null";
800             if (ob == false)
801                 return "false";
802             return "";
803         }
804         if (ob && (typeof (ob["toString"]) == "function") )
805             return ob.toString();
806         if (ob && typeof (ob["toSource"]) == "function")
807             return ob.toSource();
808        /* https://bugzilla.mozilla.org/show_bug.cgi?id=522590 */
809         var str = "[";
810         for (var p in ob)
811             str += p + ",";
812         return str + "]";
813 
814     }
815     catch (exc)
816     {
817         if (FBTrace.DBG_ERRORS)
818             FBTrace.sysout("Str.safeToString FAILS "+exc, exc);
819     }
820     return "[unsupported: no toString() function in type "+typeof(ob)+"]";
821 };
822 
823 // ********************************************************************************************* //
824 
825 return Str;
826 
827 // ********************************************************************************************* //
828 });
829