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