1 /* 2 * ***** BEGIN LICENSE BLOCK ***** 3 * Zimbra Collaboration Suite Web Client 4 * Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. 5 * 6 * The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at: https://www.zimbra.com/license 9 * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15 10 * have been added to cover use of software over a computer network and provide for limited attribution 11 * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B. 12 * 13 * Software distributed under the License is distributed on an "AS IS" basis, 14 * WITHOUT WARRANTY OF ANY KIND, either express or implied. 15 * See the License for the specific language governing rights and limitations under the License. 16 * The Original Code is Zimbra Open Source Web Client. 17 * The Initial Developer of the Original Code is Zimbra, Inc. All rights to the Original Code were 18 * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015. 19 * 20 * All portions of the code are Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. All Rights Reserved. 21 * ***** END LICENSE BLOCK ***** 22 */ 23 24 /** 25 * Default constructor does nothing (static class). 26 * @constructor 27 * @class 28 * This class provides static methods to perform miscellaneous string-related utility functions. 29 * 30 * @author Ross Dargahi 31 * @author Roland Schemers 32 * @author Conrad Damon 33 */ 34 AjxStringUtil = function() {}; 35 36 AjxStringUtil.TRIM_RE = /^\s+|\s+$/g; 37 AjxStringUtil.COMPRESS_RE = /\s+/g; 38 AjxStringUtil.ELLIPSIS = " ... "; 39 AjxStringUtil.ELLIPSIS_NO_SPACE = "..."; 40 AjxStringUtil.LIST_SEP = ", "; 41 42 AjxStringUtil.CRLF = "\r\n"; 43 AjxStringUtil.CRLF2 = "\r\n\r\n"; 44 AjxStringUtil.CRLF_HTML = "<br>"; 45 AjxStringUtil.CRLF2_HTML = "<div><br></div><div><br></div>"; 46 47 //Regex for image tag having src starting with cid: 48 AjxStringUtil.IMG_SRC_CID_REGEX = /<img([^>]*)\ssrc=["']cid:/gi; 49 50 AjxStringUtil.makeString = 51 function(val) { 52 return val ? String(val) : ""; 53 }; 54 55 /** 56 * Capitalizes the specified string by upper-casing the first character 57 * and lower-casing the rest of the string. 58 * 59 * @param {string} str The string to capitalize. 60 */ 61 AjxStringUtil.capitalize = function(str) { 62 return str && str.length > 0 ? str.charAt(0).toUpperCase() + str.substr(1).toLowerCase() : ""; 63 }; 64 65 /** 66 * Capitalizes the specified string by upper-casing the first character. 67 * Unlike AjxStringUtil.capitalize - don't change the rest of the letters. 68 * 69 * @param {string} str The string to capitalize. 70 */ 71 AjxStringUtil.capitalizeFirstLetter = function(str) { 72 return str && str.length > 0 ? str.charAt(0).toUpperCase() + str.substr(1) : ""; 73 }; 74 75 76 /** 77 * Capitalizes all the words in the specified string by upper-casing the first 78 * character of each word (does not change following characters, so something like MTV stays MTV 79 * 80 * @param {string} str The string to capitalize. 81 */ 82 AjxStringUtil.capitalizeWords = function(str) { 83 return str ? AjxUtil.map(str.split(/\s+/g), AjxStringUtil.capitalizeFirstLetter).join(" ") : ""; 84 }; 85 86 /** 87 * Converts the given text to mixed-case. The input text is one or more words 88 * separated by spaces. The output is a single word in mixed (or camel) case. 89 * 90 * @param {string} text text to convert 91 * @param {string|RegEx} sep text separator (defaults to any space) 92 * @param {boolean} camel if <code>true</code>, first character of result is lower-case 93 * @return {string} the resulting string 94 */ 95 AjxStringUtil.toMixed = 96 function(text, sep, camel) { 97 if (!text || (typeof text != "string")) { return ""; } 98 sep = sep || /\s+/; 99 var wds = text.split(sep); 100 var newText = []; 101 newText.push(camel ? wds[0].toLowerCase() : wds[0].substring(0, 1).toUpperCase() + wds[0].substring(1).toLowerCase()); 102 for (var i = 1; i < wds.length; i++) { 103 newText.push(wds[i].substring(0, 1).toUpperCase() + wds[i].substring(1).toLowerCase()); 104 } 105 return newText.join(""); 106 }; 107 108 /** 109 * Converts the given mixed-case text to a string of one or more words 110 * separated by spaces. 111 * 112 * @param {string} text The mixed-case text. 113 * @param {string} sep (Optional) The separator between words. Default 114 * is a single space. 115 */ 116 AjxStringUtil.fromMixed = function(text, sep) { 117 sep = ["$1", sep || " ", "$2"].join(""); 118 return AjxStringUtil.trim(text.replace(/([a-z])([A-Z]+)/g, sep)); 119 }; 120 121 /** 122 * Removes white space from the beginning and end of a string, optionally compressing internal white space. By default, white 123 * space is defined as a sequence of Unicode whitespace characters (\s in regexes). Optionally, the user can define what 124 * white space is by passing it as an argument. 125 * 126 * <p>TODO: add left/right options</p> 127 * 128 * @param {string} str the string to trim 129 * @param {boolean} compress whether to compress internal white space to one space 130 * @param {string} space a string that represents a user definition of white space 131 * @return {string} a trimmed string 132 */ 133 AjxStringUtil.trim = 134 function(str, compress, space) { 135 136 if (!str) {return "";} 137 138 var trim_re = AjxStringUtil.TRIM_RE; 139 140 var compress_re = AjxStringUtil.COMPRESS_RE; 141 if (space) { 142 trim_re = new RegExp("^" + space + "+|" + space + "+$", "g"); 143 compress_re = new RegExp(space + "+", "g"); 144 } else { 145 space = " "; 146 } 147 str = str.replace(trim_re, ''); 148 if (compress) { 149 str = str.replace(compress_re, space); 150 } 151 152 return str; 153 }; 154 155 /** 156 * Returns the string repeated the given number of times. 157 * 158 * @param {string} str a string 159 * @param {number} num number of times to repeat the string 160 * @return {string} the string 161 */ 162 AjxStringUtil.repeat = 163 function(str, num) { 164 var text = ""; 165 for (var i = 0; i < num; i++) { 166 text += str; 167 } 168 return text; 169 }; 170 171 /** 172 * Gets the units from size string. 173 * 174 * @param {string} sizeString the size string 175 * @return {string} the units 176 */ 177 AjxStringUtil.getUnitsFromSizeString = 178 function(sizeString) { 179 var units = "px"; 180 if (typeof(sizeString) == "string") { 181 var digitString = Number(parseInt(sizeString,10)).toString(); 182 if (sizeString.length > digitString.length) { 183 units = sizeString.substr(digitString.length, (sizeString.length-digitString.length)); 184 if (!(units=="em" || units=="ex" || units=="px" || units=="in" || units=="cm" == units=="mm" || units=="pt" || units=="pc" || units=="%")) { 185 units = "px"; 186 } 187 } 188 } 189 return units; 190 }; 191 192 /** 193 * Splits a string, ignoring delimiters that are in quotes or parentheses. Comma 194 * is the default split character, but the user can pass in a string of multiple 195 * delimiters. It can handle nested parentheses, but not nested quotes. 196 * 197 * <p>TODO: handle escaped quotes</p> 198 * 199 * @param {string} str the string to split 200 * @param {string} [dels] an optional string of delimiter characters 201 * @return {array} an array of strings 202 */ 203 AjxStringUtil.split = 204 function(str, dels) { 205 206 if (!str) {return [];} 207 var i = 0; 208 dels = dels ? dels : ','; 209 var isDel = new Object(); 210 if (typeof dels == 'string') { 211 isDel[dels] = 1; 212 } else { 213 for (i = 0; i < dels.length; i++) { 214 isDel[dels[i]] = 1; 215 } 216 } 217 218 var q = false; 219 var p = 0; 220 var start = 0; 221 var chunk; 222 var chunks = []; 223 var j = 0; 224 for (i = 0; i < str.length; i++) { 225 var c = str.charAt(i); 226 if (c == '"') { 227 q = !q; 228 } else if (c == '(') { 229 p++; 230 } else if (c == ')') { 231 p--; 232 } else if (isDel[c]) { 233 if (!q && !p) { 234 chunk = str.substring(start, i); 235 chunks[j++] = chunk; 236 start = i + 1; 237 } 238 } 239 } 240 chunk = str.substring(start, str.length); 241 chunks[j++] = chunk; 242 243 return chunks; 244 }; 245 246 AjxStringUtil.SPACE_WORD_RE = new RegExp("\\s*\\S+", "g"); 247 /** 248 * Splits the line into words, keeping leading whitespace with each word. 249 * 250 * @param {string} line the text to split 251 * 252 * @return {array} the array of words 253 */ 254 AjxStringUtil.splitKeepLeadingWhitespace = 255 function(line) { 256 var words = [], result; 257 while (result = AjxStringUtil.SPACE_WORD_RE.exec(line)) { 258 words.push(result[0]); 259 } 260 return words; 261 }; 262 263 AjxStringUtil.WRAP_LENGTH = 80; 264 AjxStringUtil.HDR_WRAP_LENGTH = 120; 265 AjxStringUtil.MAX_HTMLNODE_COUNT = 250; 266 267 // ID for a BLOCKQUOTE to mark it as ours 268 AjxStringUtil.HTML_QUOTE_COLOR = "#1010FF"; 269 AjxStringUtil.HTML_QUOTE_STYLE = "color:#000;font-weight:normal;font-style:normal;text-decoration:none;font-family:Helvetica,Arial,sans-serif;font-size:12pt;"; 270 AjxStringUtil.HTML_QUOTE_PREFIX_PRE = '<blockquote style="border-left:2px solid ' + 271 AjxStringUtil.HTML_QUOTE_COLOR + 272 ';margin-left:5px;padding-left:5px;'+ 273 AjxStringUtil.HTML_QUOTE_STYLE + 274 '">'; 275 AjxStringUtil.HTML_QUOTE_PREFIX_POST = '</blockquote>'; 276 AjxStringUtil.HTML_QUOTE_NONPREFIX_PRE = '<div style="' + 277 AjxStringUtil.HTML_QUOTE_STYLE + 278 '">'; 279 AjxStringUtil.HTML_QUOTE_NONPREFIX_POST = '</div><br/>'; 280 281 /** 282 * Wraps text to the given length and optionally quotes it. The level of quoting in the 283 * source text is preserved based on the prefixes. Special lines such as email headers 284 * always start a new line. 285 * 286 * @param {hash} params a hash of parameters 287 * @param {string} text the text to be wrapped 288 * @param {number} len the desired line length of the wrapped text, defaults to 80 289 * @param {string} prefix an optional string to prepend to each line (useful for quoting) 290 * @param {string} before text to prepend to final result 291 * @param {string} after text to append to final result 292 * @param {boolean} preserveReturns if true, don't combine small lines 293 * @param {boolean} isHeaders if true, we are wrapping a block of email headers 294 * @param {boolean} isFlowed format text for display as flowed (RFC 3676) 295 * @param {boolean} htmlMode if true, surround the content with the before and after 296 * 297 * @return {string} the wrapped/quoted text 298 */ 299 AjxStringUtil.wordWrap = 300 function(params) { 301 302 if (!(params && params.text)) { return ""; } 303 304 var text = params.text; 305 var before = params.before || ""; 306 var after = params.after || ""; 307 var isFlowed = params.isFlowed; 308 309 // For HTML, just surround the content with the before and after, which is 310 // typically a block-level element that puts a border on the left 311 if (params.htmlMode) { 312 before = params.before || (params.prefix ? AjxStringUtil.HTML_QUOTE_PREFIX_PRE : AjxStringUtil.HTML_QUOTE_NONPREFIX_PRE); 313 after = params.after || (params.prefix ? AjxStringUtil.HTML_QUOTE_PREFIX_POST : AjxStringUtil.HTML_QUOTE_NONPREFIX_POST); 314 return [before, text, after].join(""); 315 } 316 317 var max = params.len || (params.isHeaders ? AjxStringUtil.HDR_WRAP_LENGTH : AjxStringUtil.WRAP_LENGTH); 318 var prefixChar = params.prefix || ""; 319 var eol = "\n"; 320 321 var lines = text.split(AjxStringUtil.SPLIT_RE); 322 var words = []; 323 324 // Divides lines into words. Each word is part of a hash that also has 325 // the word's prefix, whether it's a paragraph break, and whether it 326 // needs to be preserved at the start or end of a line. 327 for (var l = 0, llen = lines.length; l < llen; l++) { 328 var line = lines[l]; 329 // get this line's prefix 330 var m = line.match(/^([\s>\|]+)/); 331 var prefix = m ? m[1] : ""; 332 if (prefix) { 333 line = line.substr(prefix.length); 334 } 335 if (AjxStringUtil._NON_WHITESPACE.test(line)) { 336 var wds = AjxStringUtil.splitKeepLeadingWhitespace(line); 337 if (wds && wds[0] && wds[0].length) { 338 var mustStart = AjxStringUtil.MSG_SEP_RE.test(line) || AjxStringUtil.COLON_RE.test(line) || 339 AjxStringUtil.HDR_RE.test(line) || params.isHeaders || AjxStringUtil.SIG_RE.test(line); 340 var mustEnd = params.preserveReturns; 341 if (isFlowed) { 342 var m = line.match(/( +)$/); 343 if (m) { 344 wds[wds.length - 1] += m[1]; // preserve trailing space at end of line 345 mustEnd = false; 346 } 347 else { 348 mustEnd = true; 349 } 350 } 351 for (var w = 0, wlen = wds.length; w < wlen; w++) { 352 words.push({ 353 w: wds[w], 354 prefix: prefix, 355 mustStart: (w === 0) && mustStart, 356 mustEnd: (w === wlen - 1) && mustEnd 357 }); 358 } 359 } 360 } else { 361 // paragraph marker 362 words.push({ 363 para: true, 364 prefix: prefix 365 }); 366 } 367 } 368 369 // Take the array of words and put them back together. We break for a new line 370 // when we hit the max line length, change prefixes, or hit a word that must start a new line. 371 var result = "", curLen = 0, wds = [], curPrefix = null; 372 for (var i = 0, len = words.length; i < len; i++) { 373 var word = words[i]; 374 var w = word.w, prefix = word.prefix; 375 var addPrefix = !prefixChar ? "" : curPrefix ? prefixChar : prefixChar + " "; 376 var pl = (curPrefix === null) ? 0 : curPrefix.length; 377 pl = 0; 378 var newPrefix = addPrefix + (curPrefix || ""); 379 if (word.para) { 380 // paragraph break - output what we have, then add a blank line 381 if (wds.length) { 382 result += newPrefix + wds.join("").replace(/^ +/, "") + eol; 383 } 384 if (i < words.length - 1) { 385 curPrefix = prefix; 386 addPrefix = !prefixChar ? "" : curPrefix ? prefixChar : prefixChar + " "; 387 newPrefix = addPrefix + (curPrefix || ""); 388 result += newPrefix + eol; 389 } 390 wds = []; 391 curLen = 0; 392 curPrefix = null; 393 } else if ((pl + curLen + w.length <= max) && (prefix === curPrefix || curPrefix === null) && !word.mustStart) { 394 // still room left on the current line, add the word 395 wds.push(w); 396 curLen += w.length; 397 curPrefix = prefix; 398 if (word.mustEnd && words[i + 1]) { 399 words[i + 1].mustStart = true; 400 } 401 } else { 402 // no more room - output what we have and start a new line 403 if (wds.length) { 404 result += newPrefix + wds.join("").replace(/^ +/, "") + eol; 405 } 406 wds = [w]; 407 curLen = w.length; 408 curPrefix = prefix; 409 if (word.mustEnd && words[i + 1]) { 410 words[i + 1].mustStart = true; 411 } 412 } 413 } 414 415 // handle last line 416 if (wds.length) { 417 var addPrefix = !prefixChar ? "" : wds[0].prefix ? prefixChar : prefixChar + " "; 418 var newPrefix = addPrefix + (curPrefix || ""); 419 result += newPrefix + wds.join("").replace(/^ /, "") + eol; 420 } 421 422 return [before, result, after].join(""); 423 }; 424 425 /** 426 * Quotes text with the given quote character. For HTML, surrounds the text with the 427 * given strings. Does no wrapping. 428 * 429 * @param {hash} params a hash of parameters 430 * @param {string} params.text the text to be wrapped 431 * @param {string} [params.pre] prefix for quoting 432 * @param {string} [params.before] text to prepend to final result 433 * @param {string} [params.after] text to append to final result 434 * 435 * @return {string} the quoted text 436 */ 437 AjxStringUtil.quoteText = 438 function(params) { 439 440 if (!(params && params.text)) { return ""; } 441 442 var text = params.text; 443 var before = params.before || "", after = params.after || ""; 444 445 // For HTML, just surround the content with the before and after, which is 446 // typically a block-level element that puts a border on the left 447 if (params.htmlMode || !params.pre) { 448 return [before, text, after].join(""); 449 } 450 451 var len = params.len || 80; 452 var pre = params.pre || ""; 453 var eol = "\n"; 454 455 text = AjxStringUtil.trim(text); 456 text = text.replace(/\n\r/g, eol); 457 var lines = text.split(eol); 458 var result = []; 459 460 for (var l = 0, llen = lines.length; l < llen; l++) { 461 var line = AjxStringUtil.trim(lines[l]); 462 result.push(pre + line + eol); 463 } 464 465 return before + result.join("") + after; 466 }; 467 468 AjxStringUtil.SHIFT_CHAR = { 48:')', 49:'!', 50:'@', 51:'#', 52:'$', 53:'%', 54:'^', 55:'&', 56:'*', 57:'(', 469 59:':', 186:':', 187:'+', 188:'<', 189:'_', 190:'>', 191:'?', 192:'~', 470 219:'{', 220:'|', 221:'}', 222:'"' }; 471 472 /** 473 * Returns the character for the given key, taking the shift key into consideration. 474 * 475 * @param {number} keycode a numeric keycode (not a character code) 476 * @param {boolean} shifted whether the shift key is down 477 * @return {char} a character 478 */ 479 AjxStringUtil.shiftChar = 480 function(keycode, shifted) { 481 return shifted ? AjxStringUtil.SHIFT_CHAR[keycode] || String.fromCharCode(keycode) : String.fromCharCode(keycode); 482 }; 483 484 /** 485 * Does a diff between two strings, returning the index of the first differing character. 486 * 487 * @param {string} str1 a string 488 * @param {string} str2 another string 489 * @return {number} the index at which they first differ 490 */ 491 AjxStringUtil.diffPoint = 492 function(str1, str2) { 493 if (!(str1 && str2)) { 494 return 0; 495 } 496 var len = Math.min(str1.length, str2.length); 497 var i = 0; 498 while (i < len && (str1.charAt(i) == str2.charAt(i))) { 499 i++; 500 } 501 return i; 502 }; 503 504 /* 505 * DEPRECATED 506 * 507 * Replaces variables in a string with values from a list. The variables are 508 * denoted by a '$' followed by a number, starting from 0. For example, a string 509 * of "Hello $0, meet $1" with a list of ["Harry", "Sally"] would result in the 510 * string "Hello Harry, meet Sally". 511 * 512 * @param str the string to resolve 513 * @param values an array of values to interpolate 514 * @returns a string with the variables replaced 515 * 516 * @deprecated 517 */ 518 AjxStringUtil.resolve = 519 function(str, values) { 520 DBG.println(AjxDebug.DBG1, "Call to deprecated function AjxStringUtil.resolve"); 521 return AjxMessageFormat.format(str, values); 522 }; 523 524 /** 525 * Encodes a complete URL. Leaves delimiters alone. 526 * 527 * @param {string} str the string to encode 528 * @return {string} the encoded string 529 */ 530 AjxStringUtil.urlEncode = 531 function(str) { 532 if (!str) return ""; 533 var func = window.encodeURL || window.encodeURI; 534 return func(str); 535 }; 536 537 /** 538 * Encodes a string as if it were a <em>part</em> of a URL. The 539 * difference between this function and {@link AjxStringUtil.urlEncode} 540 * is that this will also encode the following delimiters: 541 * 542 * <pre> 543 * : / ? & = 544 * </pre> 545 * 546 * @param {string} str the string to encode 547 * @return {string} the resulting string 548 */ 549 AjxStringUtil.urlComponentEncode = 550 function(str) { 551 if (!str) return ""; 552 var func = window.encodeURLComponent || window.encodeURIComponent; 553 return func(str); 554 }; 555 556 /** 557 * Decodes a complete URL. 558 * 559 * @param {string} str the string to decode 560 * @return {string} the decoded string 561 */ 562 AjxStringUtil.urlDecode = 563 function(str) { 564 if (!str) return ""; 565 var func = window.decodeURL || window.decodeURI; 566 try { 567 return func(str); 568 } 569 catch(e) { 570 return ""; 571 } 572 }; 573 574 /** 575 * Decodes a string as if it were a <em>part</em> of a URL. Falls back 576 * to unescape() if necessary. 577 * 578 * @param {string} str the string to decode 579 * @return {string} the decoded string 580 */ 581 AjxStringUtil.urlComponentDecode = 582 function(str) { 583 if (!str) return ""; 584 var func = window.decodeURLComponent || window.decodeURIComponent; 585 var result; 586 try { 587 result = func(str); 588 } catch(e) { 589 result = unescape(str); 590 } 591 592 return result || str; 593 }; 594 595 AjxStringUtil.ENCODE_MAP = { '>' : '>', '<' : '<', '&' : '&' }; 596 597 /** 598 * HTML-encodes a string. 599 * 600 * @param {string} str the string to encode 601 * @param {boolean} includeSpaces if <code>true</code>, to include encoding spaces 602 * @return {string} the encoded string 603 */ 604 AjxStringUtil.htmlEncode = 605 function(str, includeSpaces) { 606 607 if (!str) {return "";} 608 if (typeof(str) != "string") { 609 str = str.toString ? str.toString() : ""; 610 } 611 612 if (!AjxEnv.isSafari || AjxEnv.isSafariNightly) { 613 if (includeSpaces) { 614 return str.replace(/[<>&]/g, function(htmlChar) { return AjxStringUtil.ENCODE_MAP[htmlChar]; }).replace(/ /g, ' '); 615 } else { 616 return str.replace(/[<>&]/g, function(htmlChar) { return AjxStringUtil.ENCODE_MAP[htmlChar]; }); 617 } 618 } else { 619 if (includeSpaces) { 620 return str.replace(/[&]/g, '&').replace(/ /g, ' ').replace(/[<]/g, '<').replace(/[>]/g, '>'); 621 } else { 622 return str.replace(/[&]/g, '&').replace(/[<]/g, '<').replace(/[>]/g, '>'); 623 } 624 } 625 }; 626 627 /** 628 * encode quotes for using in inline JS code, so the text does not end a quoted param prematurely. 629 * @param str 630 */ 631 AjxStringUtil.encodeQuotes = 632 function(str) { 633 return str.replace(/"/g, '"').replace(/'/g, "'"); 634 }; 635 636 637 /** 638 * Decodes the string. 639 * 640 * @param {string} str the string to decode 641 * @param {boolean} decodeSpaces if <code>true</code>, decode spaces 642 * @return {string} the string 643 */ 644 AjxStringUtil.htmlDecode = 645 function(str, decodeSpaces) { 646 647 if(decodeSpaces) 648 str = str.replace(/ /g," "); 649 650 return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); 651 }; 652 653 AjxStringUtil.__jsEscapeChar = function(c) { 654 var codestr = c.charCodeAt(0).toString(16); 655 656 if (codestr.length == 1) 657 return '\\u000' + codestr; 658 else if (codestr.length == 2) 659 return '\\u00' + codestr; 660 else if (codestr.length == 3) 661 return '\\u0' + codestr; 662 else if (codestr.length == 4) 663 return '\\u' + codestr; 664 665 // shouldn't happen -- ECMAscript proscribes that strings are 666 // UTF-16 internally 667 DBG.println(AjxDebug.NONE, "unexpected condition in " + 668 "AjxStringUtil.__jsEscapeChar -- code point 0x" + 669 codestr + " doesn't fit in 16 bits"); 670 }; 671 672 /** 673 * Encodes non-ASCII and non-printable characters as \uXXXX, suitable 674 * for JSON. 675 * 676 * @param {string} str the string 677 * @return {string} the encoded string 678 */ 679 AjxStringUtil.jsEncode = 680 function(str) { 681 return str.replace(/[^\u0020-\u007e]/g, 682 AjxStringUtil.__jsEscapeChar); 683 }; 684 685 /** 686 * Removes HTML tags from the given string. 687 * 688 * @param {string} str text from which to strip tags 689 * @param {boolean} removeContent if <code>true</code>, also remove content within tags 690 * @return {string} the resulting HTML string 691 */ 692 AjxStringUtil.stripTags = 693 function(str, removeContent) { 694 if (typeof str !== 'string') { 695 return ""; 696 } 697 if (removeContent) { 698 str = str.replace(/(<(\w+)[^>]*>).*(<\/\2[^>]*>)/, "$1$3"); 699 } 700 return str.replace(/<\/?[^>]+>/gi, ''); 701 }; 702 703 /** 704 * Converts the string to HTML. 705 * 706 * @param {string} str the string 707 * @return {string} the resulting string 708 */ 709 AjxStringUtil.convertToHtml = 710 function(str, quotePrefix, openTag, closeTag) { 711 712 openTag = openTag || "<blockquote>"; 713 closeTag = closeTag || "</blockquote>"; 714 715 if (!str) {return "";} 716 717 str = AjxStringUtil.htmlEncode(str); 718 if (quotePrefix) { 719 // Convert a section of lines prefixed with > or | 720 // to a section encapsuled in <blockquote> tags 721 var prefix_re = /^(>|>|\|\s+)/; 722 var lines = str.split(/\r?\n/); 723 var level = 0; 724 for (var i = 0; i < lines.length; i++) { 725 var line = lines[i]; 726 if (line.length > 0) { 727 var lineLevel = 0; 728 // Remove prefixes while counting how many there are on the line 729 while (line.match(prefix_re)) { 730 line = line.replace(prefix_re, ""); 731 lineLevel++; 732 } 733 // If the lineLevel has changed since the last line, add blockquote start or end tags, and adjust level accordingly 734 while (lineLevel > level) { 735 line = openTag + line; 736 level++; 737 } 738 while (lineLevel < level) { 739 lines[i - 1] = lines[i - 1] + closeTag; 740 level--; 741 } 742 } 743 lines[i] = line; 744 } 745 while (level > 0) { 746 lines.push(closeTag); 747 level--; 748 } 749 750 str = lines.join("\n"); 751 } 752 753 str = str 754 .replace(/ /mg, ' ') 755 .replace(/^ /mg, ' ') 756 .replace(/\t/mg, " ") 757 .replace(/\r?\n/mg, "<br>"); 758 return str; 759 }; 760 761 AjxStringUtil.SPACE_ENCODE_MAP = { ' ' : ' ', '>' : '>', '<' : '<', '&' : '&' , '\n': '<br>'}; 762 763 /** 764 * HTML-encodes a string. 765 * 766 * @param {string} str the string to encode 767 * 768 * @private 769 */ 770 AjxStringUtil.htmlEncodeSpace = 771 function(str) { 772 if (!str) { return ""; } 773 return str.replace(/[&]/g, '&').replace(/ /g, ' ').replace(/[<]/g, '<').replace(/[>]/g, '>'); 774 }; 775 776 /** 777 * Encode 778 * @param base {string} Ruby base. 779 * @param text {string} Ruby text (aka furigana). 780 */ 781 AjxStringUtil.htmlRubyEncode = function(base, text) { 782 if (base && text) { 783 return [ 784 "<ruby>", 785 "<rb>",AjxStringUtil.htmlEncode(base),"</rb> ", 786 "<rp>(</rp><rt>",AjxStringUtil.htmlEncode(text),"</rt><rp>)</rp>", 787 "</ruby>" 788 ].join(""); 789 } 790 return AjxStringUtil.htmlEncode(base || text || ""); 791 }; 792 793 // this function makes sure a leading space is preservered, takes care of tabs, 794 // then finally takes replaces newlines with <br>'s 795 AjxStringUtil.nl2br = 796 function(str) { 797 if (!str) return ""; 798 return str.replace(/^ /mg, " "). 799 // replace(/\t/g, "<pre style='display:inline;'>\t</pre>"). 800 // replace(/\t/mg, " "). 801 replace(/\t/mg, "<span style='white-space:pre'>\t</span>"). 802 replace(/\n/g, "<br>"); 803 }; 804 805 AjxStringUtil.xmlEncode = 806 function(str) { 807 if (str) { 808 // bug fix #8779 - safari barfs if "str" is not a String type 809 str = "" + str; 810 return str.replace(/&/g,"&").replace(/</g,"<"); 811 } 812 return ""; 813 }; 814 815 AjxStringUtil.xmlDecode = 816 function(str) { 817 return str ? str.replace(/&/g,"&").replace(/</g,"<") : ""; 818 }; 819 820 AjxStringUtil.xmlAttrEncode = 821 function(str) { 822 return str ? str.replace(/&/g,"&").replace(/</g,"<").replace(/\x22/g, '"').replace(/\x27/g,"'") : ""; 823 }; 824 825 AjxStringUtil.xmlAttrDecode = 826 function(str) { 827 return str ? str.replace(/&/g,"&").replace(/</g,"<").replace(/"/g, '"').replace(/'/g,"'") : ""; 828 }; 829 830 AjxStringUtil.__RE_META = { " ":" ", "\n":"\\n", "\r":"\\r", "\t":"\\t" }; 831 AjxStringUtil.__reMetaEscape = function($0, $1) { 832 return AjxStringUtil.__RE_META[$1] || "\\"+$1; 833 }; 834 AjxStringUtil.regExEscape = 835 function(str) { 836 return str.replace(/(\W)/g, AjxStringUtil.__reMetaEscape); 837 }; 838 839 AjxStringUtil._calcDIV = null; // used by 'clip()' and 'wrap()' functions 840 841 AjxStringUtil.calcDIV = 842 function() { 843 if (AjxStringUtil._calcDIV == null) { 844 AjxStringUtil._calcDIV = document.createElement("div"); 845 AjxStringUtil._calcDIV.style.zIndex = 0; 846 AjxStringUtil._calcDIV.style.position = DwtControl.ABSOLUTE_STYLE; 847 AjxStringUtil._calcDIV.style.visibility = "hidden"; 848 document.body.appendChild(AjxStringUtil._calcDIV); 849 } 850 return AjxStringUtil._calcDIV; 851 }; 852 853 /** 854 * Clips a string at "pixelWidth" using using "className" on hidden 'AjxStringUtil._calcDIV'. 855 * Returns "origString" with "..." appended if clipped. 856 * 857 * NOTE: The same CSS style ("className") must be assigned to both the intended 858 * display area and the hidden 'AjxStringUtil._calcDIV'. "className" is 859 * optional; if supplied, it will be assigned to 'AjxStringUtil._calcDIV' to 860 * handle different CSS styles ("className"s) on same page. 861 * 862 * NOTE2: MSIE Benchmark - clipping an average of 17 characters each over 190 863 * iterations averaged 27ms each (5.1 seconds total for 190) 864 * 865 * @private 866 */ 867 AjxStringUtil.clip = 868 function(origString, pixelWidth, className) { 869 var calcDIV = AjxStringUtil.calcDIV(); 870 if (arguments.length == 3) calcDIV.className = className; 871 //calcDIV.innerHTML = "<div>" + origString + "</div>"; // prevents screen flash in IE? 872 calcDIV.innerHTML = origString; 873 if (calcDIV.offsetWidth <= pixelWidth) return origString; 874 875 for (var i=origString.length-1; i>0; i--) { 876 var newString = origString.substr(0,i); 877 calcDIV.innerHTML = newString + AjxStringUtil.ELLIPSIS; 878 if (calcDIV.offsetWidth <= pixelWidth) return newString + AjxStringUtil.ELLIPSIS; 879 } 880 return origString; 881 }; 882 883 AjxStringUtil.clipByLength = 884 function(str,clipLen) { 885 var len = str.length; 886 return (len <= clipLen) 887 ? str 888 : [str.substr(0,clipLen/2), '...', str.substring(len - ((clipLen/2) - 3),len)].join(""); 889 }; 890 891 /** 892 * Forces a string to wrap at "pixelWidth" using "className" on hidden 'AjxStringUtil._calcDIV'. 893 * Returns "origString" with "<br>" tags inserted to force wrapping. 894 * Breaks string on embedded space characters, EOL ("/n") and "<br>" tags when possible. 895 * 896 * @returns "origString" with "<br>" tags inserted to force wrapping. 897 * 898 * @private 899 */ 900 AjxStringUtil.wrap = 901 function(origString, pixelWidth, className) { 902 var calcDIV = AjxStringUtil.calcDIV(); 903 if (arguments.length == 3) calcDIV.className = className; 904 905 var newString = ""; 906 var newLine = ""; 907 var textRows = origString.split("/n"); 908 for (var trCount = 0; trCount < textRows.length; trCount++) { 909 if (trCount != 0) { 910 newString += newLine + "<br>"; 911 newLine = ""; 912 } 913 htmlRows = textRows[trCount].split("<br>"); 914 for (var hrCount=0; hrCount<htmlRows.length; hrCount++) { 915 if (hrCount != 0) { 916 newString += newLine + "<br>"; 917 newLine = ""; 918 } 919 words = htmlRows[hrCount].split(" "); 920 var wCount=0; 921 while (wCount<words.length) { 922 calcDIV.innerHTML = newLine + " " + words[wCount]; 923 var newLinePixels = calcDIV.offsetWidth; 924 if (newLinePixels > pixelWidth) { 925 // whole "words[wCount]" won't fit on current "newLine" - insert line break, avoid incrementing "wCount" 926 calcDIV.innerHTML = words[wCount]; 927 newLinePixels = newLinePixels - calcDIV.offsetWidth; 928 if ( (newLinePixels >= pixelWidth) || (calcDIV.offsetWidth <= pixelWidth) ) { 929 // either a) excess caused by <space> character or b) will fit completely on next line 930 // so just break without incrementing "wCount" and append next time 931 newString += newLine + "<br>"; 932 newLine = ""; 933 } 934 else { // must break "words[wCount]" 935 var keepLooping = true; 936 var atPos = 0; 937 while (keepLooping) { 938 atPos++; 939 calcDIV.innerHTML = newLine + " " + words[wCount].substring(0,atPos); 940 keepLooping = (calcDIV.offsetWidth <= pixelWidth); 941 } 942 atPos--; 943 newString += newLine + words[wCount].substring(0,atPos) + "<br>"; 944 words[wCount] = words[wCount].substr(atPos); 945 newLine = ""; 946 } 947 } else { // doesn't exceed pixelWidth, append to "newLine" and increment "wCount" 948 newLine += " " + words[wCount]; 949 wCount++; 950 } 951 } 952 } 953 } 954 newString += newLine; 955 return newString; 956 }; 957 958 // Regexes for finding stuff in msg content 959 AjxStringUtil.MSG_SEP_RE = new RegExp("^\\s*--+\\s*(" + AjxMsg.origMsg + "|" + AjxMsg.forwardedMessage + ")\\s*--+", "i"); 960 AjxStringUtil.SIG_RE = /^(- ?-+)|(__+)\r?$/; 961 AjxStringUtil.SPLIT_RE = /\r\n|\r|\n/; 962 AjxStringUtil.HDR_RE = /^\s*\w+:/; 963 AjxStringUtil.COLON_RE = /\S+:$/; 964 965 // Converts a HTML document represented by a DOM tree to text 966 // XXX: There has got to be a better way of doing this! 967 AjxStringUtil._NO_LIST = 0; 968 AjxStringUtil._ORDERED_LIST = 1; 969 AjxStringUtil._UNORDERED_LIST = 2; 970 AjxStringUtil._INDENT = " "; 971 AjxStringUtil._NON_WHITESPACE = /\S+/; 972 AjxStringUtil._LF = /\n/; 973 974 AjxStringUtil.convertHtml2Text = 975 function(domRoot, convertor, onlyOneNewLinePerP) { 976 977 if (!domRoot) { return ""; } 978 979 if (convertor && AjxUtil.isFunction(convertor._before)) { 980 domRoot = convertor._before(domRoot); 981 } 982 983 if (typeof domRoot == "string") { 984 var domNode = document.createElement("SPAN"); 985 domNode.innerHTML = domRoot; 986 domRoot = domNode; 987 } 988 var text = []; 989 var idx = 0; 990 var ctxt = {}; 991 AjxStringUtil._traverse(domRoot, text, idx, AjxStringUtil._NO_LIST, 0, 0, ctxt, convertor, onlyOneNewLinePerP); 992 993 var result = text.join(""); 994 995 if (convertor && AjxUtil.isFunction(convertor._after)) { 996 result = convertor._after(result); 997 } 998 999 return result; 1000 }; 1001 1002 AjxStringUtil._traverse = 1003 function(el, text, idx, listType, listLevel, bulletNum, ctxt, convertor, onlyOneNewLinePerP) { 1004 1005 var nodeName = el.nodeName.toLowerCase(); 1006 1007 var result = null; 1008 if (convertor && convertor[nodeName]) { 1009 result = convertor[nodeName](el, ctxt); 1010 } 1011 1012 if (result != null) { 1013 text[idx++] = result; 1014 } else if (nodeName == "#text") { 1015 if (el.nodeValue.search(AjxStringUtil._NON_WHITESPACE) != -1) { 1016 if (ctxt.lastNode == "ol" || ctxt.lastNode == "ul") { 1017 text[idx++] = "\n"; 1018 } 1019 if (ctxt.isPreformatted) { 1020 text[idx++] = AjxStringUtil.trim(el.nodeValue) + " "; 1021 } else { 1022 text[idx++] = AjxStringUtil.trim(el.nodeValue.replace(AjxStringUtil._LF, " "), true) + " "; 1023 } 1024 } 1025 } else if (nodeName == "p") { 1026 text[idx++] = onlyOneNewLinePerP ? "\n" : "\n\n"; 1027 } else if (nodeName === "a") { 1028 if (el.href) { 1029 //format as [ href | text ] (if no text, format as [ href ] 1030 text[idx++] = "[ "; 1031 text[idx++] = el.href; 1032 if (el.textContent) { 1033 text[idx++] = " | "; 1034 text[idx++] = el.textContent; 1035 } 1036 text[idx++] = " ] "; 1037 return idx; // returning since we take care of all the child nodes via the "textContent" above. No need to parse further. 1038 } 1039 } else if (listType == AjxStringUtil._NO_LIST && (nodeName == "br" || nodeName == "hr")) { 1040 text[idx++] = "\n"; 1041 } else if (nodeName == "ol" || nodeName == "ul") { 1042 text[idx++] = "\n"; 1043 if (el.parentNode.nodeName.toLowerCase() != "li" && ctxt.lastNode != "br" && ctxt.lastNode != "hr") { 1044 text[idx++] = "\n"; 1045 } 1046 listType = (nodeName == "ol") ? AjxStringUtil._ORDERED_LIST : AjxStringUtil._UNORDERED_LIST; 1047 listLevel++; 1048 bulletNum = 0; 1049 } else if (nodeName == "li") { 1050 for (var i = 0; i < listLevel; i++) { 1051 text[idx++] = AjxStringUtil._INDENT; 1052 } 1053 if (listType == AjxStringUtil._ORDERED_LIST) { 1054 text[idx++] = bulletNum + ". "; 1055 } else { 1056 text[idx++] = "\u002A "; // TODO AjxMsg.bullet 1057 } 1058 } else if (nodeName == "tr" && el.parentNode.firstChild != el) { 1059 text[idx++] = "\n"; 1060 } else if (nodeName == "td" && el.parentNode.firstChild != el) { 1061 text[idx++] = "\t"; 1062 } else if (nodeName == "div" || nodeName == "address") { 1063 if (idx && text[idx - 1] !== "\n") { 1064 text[idx++] = "\n"; 1065 } 1066 } else if (nodeName == "blockquote") { 1067 text[idx++] = "\n\n"; 1068 } else if (nodeName == "pre") { 1069 if (idx && text[idx - 1] !== "\n") { 1070 text[idx++] = "\n"; 1071 } 1072 ctxt.isPreformatted = true; 1073 } else if (nodeName == "#comment" || 1074 nodeName == "script" || 1075 nodeName == "select" || 1076 nodeName == "style") { 1077 return idx; 1078 } 1079 1080 var childNodes = el.childNodes; 1081 var len = childNodes.length; 1082 for (var i = 0; i < len; i++) { 1083 var tmp = childNodes[i]; 1084 if (tmp.nodeType == 1 && tmp.tagName.toLowerCase() == "li") { 1085 bulletNum++; 1086 } 1087 idx = AjxStringUtil._traverse(tmp, text, idx, listType, listLevel, bulletNum, ctxt, convertor, onlyOneNewLinePerP); 1088 } 1089 1090 if (convertor && convertor["/"+nodeName]) { 1091 text[idx++] = convertor["/"+nodeName](el); 1092 } 1093 1094 if (nodeName == "h1" || nodeName == "h2" || nodeName == "h3" || nodeName == "h4" 1095 || nodeName == "h5" || nodeName == "h6" || nodeName == "div" || nodeName == "address") { 1096 if (idx && text[idx - 1] !== "\n") { 1097 text[idx++] = "\n"; 1098 } 1099 ctxt.list = false; 1100 } else if (nodeName == "pre") { 1101 if (idx && text[idx - 1] !== "\n") { 1102 text[idx++] = "\n"; 1103 } 1104 ctxt.isPreformatted = false; 1105 } else if (nodeName == "li") { 1106 if (!ctxt.list) { 1107 text[idx++] = "\n"; 1108 } 1109 ctxt.list = false; 1110 } else if (nodeName == "ol" || nodeName == "ul") { 1111 ctxt.list = true; 1112 } else if (nodeName != "#text") { 1113 ctxt.list = false; 1114 } 1115 1116 ctxt.lastNode = nodeName; 1117 return idx; 1118 }; 1119 1120 /** 1121 * Sets the given name/value pairs into the given query string. Args that appear 1122 * in both will get the new value. The order of args in the returned query string 1123 * is indeterminate. 1124 * 1125 * @param args [hash] name/value pairs to add to query string 1126 * @param qsReset [boolean] if true, start with empty query string 1127 * 1128 * @private 1129 */ 1130 AjxStringUtil.queryStringSet = 1131 function(args, qsReset) { 1132 var qs = qsReset ? "" : location.search; 1133 if (qs.indexOf("?") == 0) { 1134 qs = qs.substr(1); 1135 } 1136 var qsArgs = qs.split("&"); 1137 var newArgs = {}; 1138 for (var i = 0; i < qsArgs.length; i++) { 1139 var f = qsArgs[i].split("="); 1140 newArgs[f[0]] = f[1]; 1141 } 1142 for (var name in args) { 1143 newArgs[name] = args[name]; 1144 } 1145 var pairs = []; 1146 var i = 0; 1147 for (var name in newArgs) { 1148 if (name) { 1149 pairs[i++] = [name, newArgs[name]].join("="); 1150 } 1151 } 1152 1153 return "?" + pairs.join("&"); 1154 }; 1155 1156 /** 1157 * Removes the given arg from the query string. 1158 * 1159 * @param {String} qs a query string 1160 * @param {String} name the arg name 1161 * 1162 * @return {String} the resulting query string 1163 */ 1164 AjxStringUtil.queryStringRemove = 1165 function(qs, name) { 1166 qs = qs ? qs : ""; 1167 if (qs.indexOf("?") == 0) { 1168 qs = qs.substr(1); 1169 } 1170 var pairs = qs.split("&"); 1171 var pairs1 = []; 1172 for (var i = 0; i < pairs.length; i++) { 1173 if (pairs[i].indexOf(name) != 0) { 1174 pairs1.push(pairs[i]); 1175 } 1176 } 1177 1178 return "?" + pairs1.join("&"); 1179 }; 1180 1181 /** 1182 * Returns the given object/primitive as a string. 1183 * 1184 * @param {primitive|Object} o an object or primitive 1185 * @return {String} the string 1186 */ 1187 AjxStringUtil.getAsString = 1188 function(o) { 1189 return !o ? "" : (typeof(o) == 'object') ? o.toString() : o; 1190 }; 1191 1192 AjxStringUtil.isWhitespace = 1193 function(str) { 1194 return (str.charCodeAt(0) <= 32); 1195 }; 1196 1197 AjxStringUtil.isDigit = 1198 function(str) { 1199 var charCode = str.charCodeAt(0); 1200 return (charCode >= 48 && charCode <= 57); 1201 }; 1202 1203 AjxStringUtil.compareRight = 1204 function(a,b) { 1205 var bias = 0; 1206 var idxa = 0; 1207 var idxb = 0; 1208 var ca; 1209 var cb; 1210 1211 for (; (idxa < a.length || idxb < b.length); idxa++, idxb++) { 1212 ca = a.charAt(idxa); 1213 cb = b.charAt(idxb); 1214 1215 if (!AjxStringUtil.isDigit(ca) && 1216 !AjxStringUtil.isDigit(cb)) 1217 { 1218 return bias; 1219 } 1220 else if (!AjxStringUtil.isDigit(ca)) 1221 { 1222 return -1; 1223 } 1224 else if (!AjxStringUtil.isDigit(cb)) 1225 { 1226 return +1; 1227 } 1228 else if (ca < cb) 1229 { 1230 if (bias == 0) bias = -1; 1231 } 1232 else if (ca > cb) 1233 { 1234 if (bias == 0) bias = +1; 1235 } 1236 } 1237 }; 1238 1239 AjxStringUtil.natCompare = 1240 function(a, b) { 1241 var idxa = 0, idxb = 0; 1242 var nza = 0, nzb = 0; 1243 var ca, cb; 1244 1245 while (idxa < a.length || idxb < b.length) 1246 { 1247 // number of zeroes leading the last number compared 1248 nza = nzb = 0; 1249 1250 ca = a.charAt(idxa); 1251 cb = b.charAt(idxb); 1252 1253 // ignore overleading spaces/zeros and move the index accordingly 1254 while (AjxStringUtil.isWhitespace(ca) || ca =='0') { 1255 nza = (ca == '0') ? (nza+1) : 0; 1256 ca = a.charAt(++idxa); 1257 } 1258 while (AjxStringUtil.isWhitespace(cb) || cb == '0') { 1259 nzb = (cb == '0') ? (nzb+1) : 0; 1260 cb = b.charAt(++idxb); 1261 } 1262 1263 // current index points to digit in both str 1264 if (AjxStringUtil.isDigit(ca) && AjxStringUtil.isDigit(cb)) { 1265 var result = AjxStringUtil.compareRight(a.substring(idxa), b.substring(idxb)); 1266 if (result && result!=0) { 1267 return result; 1268 } 1269 } 1270 1271 if (ca == 0 && cb == 0) { 1272 return nza - nzb; 1273 } 1274 1275 if (ca < cb) { 1276 return -1; 1277 } else if (ca > cb) { 1278 return +1; 1279 } 1280 1281 ++idxa; ++idxb; 1282 } 1283 }; 1284 1285 AjxStringUtil.clipFile = 1286 function(fileName, limit) { 1287 var index = fileName.lastIndexOf('.'); 1288 1289 // fallback - either not found or starts with delimiter 1290 if (index <= 0) { 1291 index = fileName.length; 1292 } 1293 1294 if (index <= limit) { 1295 return fileName; 1296 } 1297 1298 var fName = fileName.slice(0, index); 1299 var ext = fileName.slice(index); 1300 1301 return [ 1302 fName.slice(0, limit/2), 1303 AjxMsg.ellipsis, 1304 fName.slice(-Math.ceil(limit/2) + AjxMsg.ellipsis.length), 1305 ext 1306 ].join("") 1307 }; 1308 1309 1310 AjxStringUtil.URL_PARSE_RE = new RegExp("^(?:([^:/?#.]+):)?(?://)?(([^:/?#]*)(?::(\\d*))?)?((/(?:[^?#](?![^?#/]*\\.[^?#/.]+(?:[\\?#]|$)))*/?)?([^?#/]*))?(?:\\?([^#]*))?(?:#(.*))?"); 1311 1312 AjxStringUtil.parseURL = 1313 function(sourceUri) { 1314 1315 var names = ["source","protocol","authority","domain","port","path","directoryPath","fileName","query","anchor"]; 1316 var parts = AjxStringUtil.URL_PARSE_RE.exec(sourceUri); 1317 var uri = {}; 1318 1319 for (var i = 0; i < names.length; i++) { 1320 uri[names[i]] = (parts[i] ? parts[i] : ""); 1321 } 1322 1323 if (uri.directoryPath.length > 0) { 1324 uri.directoryPath = uri.directoryPath.replace(/\/?$/, "/"); 1325 } 1326 1327 return uri; 1328 }; 1329 1330 /** 1331 * Parses a mailto: link into components. If the string is not a mailto: link, the object returned will 1332 * have a "to" property set to the string. 1333 * 1334 * @param {String} str email address, possibly within a "mailto:" link 1335 * @returns {Object} object with at least a 'to' property, and possibly 'subject' and 'body' 1336 */ 1337 AjxStringUtil.parseMailtoLink = function(str) { 1338 1339 var parts = {}; 1340 1341 if (!str) { 1342 return parts; 1343 } 1344 1345 if (str.toLowerCase().indexOf('mailto:') === -1) { 1346 parts.to = str; 1347 return parts; 1348 } 1349 1350 var match = str.match(/\bsubject=([^&]+)/i); 1351 parts.subject = match ? decodeURIComponent(match[1]) : null; 1352 1353 match = str.match(/\bto\:([^&]+)/); 1354 if (!match) { 1355 match = str.match(/\bmailto\:([^\?]+)/i); 1356 } 1357 parts.to = match ? decodeURIComponent(match[1]) : null; 1358 1359 match = str.match(/\bbody=([^&]+)/i); 1360 parts.body = match ? decodeURIComponent(match[1]) : null; 1361 1362 return parts; 1363 }; 1364 1365 /** 1366 * Parse the query string (part after the "?") and return it as a hash of key/value pairs. 1367 * 1368 * @param {String} sourceUri the source location or query string 1369 * @return {Object} a hash of query string params 1370 */ 1371 AjxStringUtil.parseQueryString = 1372 function(sourceUri) { 1373 1374 var location = sourceUri || ("" + window.location); 1375 var idx = location.indexOf("?"); 1376 var qs = (idx === -1) ? location : location.substring(idx + 1); 1377 qs = qs.replace(/#.*$/, ''); // strip anchor 1378 var list = qs.split("&"); 1379 var params = {}, pair, key, value; 1380 for (var i = 0; i < list.length; i++) { 1381 pair = list[i].split("="); 1382 key = decodeURIComponent(pair[0]), 1383 value = pair[1] ? decodeURIComponent(pair[1]) : true; // if no value given, set to true so we know it's there 1384 params[key] = value; 1385 } 1386 return params; 1387 }; 1388 1389 /** 1390 * Pretty-prints a JS object. Preferred over JSON.stringify for the debug-related dumping 1391 * of an object for several reasons: 1392 * - doesn't have an enclosing object, which shifts everything over one level 1393 * - doesn't put quotes around keys 1394 * - shows indexes for arrays (downside is that prevents output from being eval-able) 1395 * 1396 * @param obj 1397 * @param recurse 1398 * @param showFuncs 1399 * @param omit 1400 */ 1401 AjxStringUtil.prettyPrint = 1402 function(obj, recurse, showFuncs, omit) { 1403 1404 AjxStringUtil._visited = new AjxVector(); 1405 var text = AjxStringUtil._prettyPrint(obj, recurse, showFuncs, omit); 1406 AjxStringUtil._visited = null; 1407 1408 return text; 1409 }; 1410 1411 AjxStringUtil._visited = null; 1412 1413 AjxStringUtil._prettyPrint = 1414 function(obj, recurse, showFuncs, omit) { 1415 1416 var indentLevel = 0; 1417 var showBraces = false; 1418 var stopRecursion = false; 1419 if (arguments.length > 4) { 1420 indentLevel = arguments[4]; 1421 showBraces = arguments[5]; 1422 stopRecursion = arguments[6]; 1423 } 1424 1425 if (AjxUtil.isObject(obj)) { 1426 var objStr = obj.toString ? obj.toString() : ""; 1427 if (omit && objStr && omit[objStr]) { 1428 return "[" + objStr + "]"; 1429 } 1430 if (AjxStringUtil._visited.contains(obj)) { 1431 return "[visited object]"; 1432 } else { 1433 AjxStringUtil._visited.add(obj); 1434 } 1435 } 1436 1437 var indent = AjxStringUtil.repeat(" ", indentLevel); 1438 var text = ""; 1439 1440 if (obj === undefined) { 1441 text += "[undefined]"; 1442 } else if (obj === null) { 1443 text += "[null]"; 1444 } else if (AjxUtil.isBoolean(obj)) { 1445 text += obj ? "true" : "false"; 1446 } else if (AjxUtil.isString(obj)) { 1447 text += '"' + AjxStringUtil._escapeForHTML(obj) + '"'; 1448 } else if (AjxUtil.isNumber(obj)) { 1449 text += obj; 1450 } else if (AjxUtil.isObject(obj)) { 1451 var isArray = AjxUtil.isArray(obj) || AjxUtil.isArray1(obj); 1452 if (stopRecursion) { 1453 text += isArray ? "[Array]" : obj.toString(); 1454 } else { 1455 stopRecursion = !recurse; 1456 var keys = new Array(); 1457 for (var i in obj) { 1458 if (obj.hasOwnProperty(i)) { 1459 keys.push(i); 1460 } 1461 } 1462 1463 if (isArray) { 1464 keys.sort(function(a,b) {return a - b;}); 1465 } else { 1466 keys.sort(); 1467 } 1468 1469 if (showBraces) { 1470 text += isArray ? "[" : "{"; 1471 } 1472 var len = keys.length; 1473 for (var i = 0; i < len; i++) { 1474 var key = keys[i]; 1475 var nextObj = obj[key]; 1476 var value = null; 1477 // For dumping events, and dom elements, though I may not want to 1478 // traverse the node, I do want to know what the attribute is. 1479 if (nextObj == window || nextObj == document || (!AjxEnv.isIE && nextObj instanceof Node)){ 1480 value = nextObj.toString(); 1481 } 1482 if ((typeof(nextObj) == "function")) { 1483 if (showFuncs) { 1484 value = "[function]"; 1485 } else { 1486 continue; 1487 } 1488 } 1489 1490 if (i > 0) { 1491 text += ","; 1492 } 1493 text += "\n" + indent; 1494 var keyString; 1495 if (isArray) { 1496 keyString = "// [" + key + "]:\n" + indent; 1497 } else { 1498 keyString = key + ": "; 1499 } 1500 if (omit && omit[key]) { 1501 text += keyString + "[" + key + "]"; 1502 } else if (value != null) { 1503 text += keyString + value; 1504 } else { 1505 text += keyString + AjxStringUtil._prettyPrint(nextObj, recurse, showFuncs, omit, indentLevel + 2, true, stopRecursion); 1506 } 1507 } 1508 if (i > 0) { 1509 text += "\n" + AjxStringUtil.repeat(" ", indentLevel - 1); 1510 } 1511 if (showBraces) { 1512 text += isArray ? "]" : "}"; 1513 } 1514 } 1515 } 1516 return text; 1517 }; 1518 1519 AjxStringUtil._escapeForHTML = 1520 function(str){ 1521 1522 if (typeof(str) != 'string') { return str; } 1523 1524 var s = str; 1525 s = s.replace(/\&/g, '&'); 1526 s = s.replace(/\</g, '<'); 1527 s = s.replace(/\>/g, '>'); 1528 s = s.replace(/\"/g, '"'); 1529 s = s.replace(/\xA0/g, ' '); 1530 1531 return s; 1532 }; 1533 1534 // hidden SPANs for measuring regular and bold strings 1535 AjxStringUtil._testSpan = null; 1536 AjxStringUtil._testSpanBold = null; 1537 1538 // cached string measurements 1539 AjxStringUtil.WIDTH = {}; // regular strings 1540 AjxStringUtil.WIDTH_BOLD = {}; // bold strings 1541 AjxStringUtil.MAX_CACHE = 1000; // max total number of cached strings 1542 AjxStringUtil._cacheSize = 0; // current number of cached strings 1543 1544 /** 1545 * Returns the width in pixels of the given string. 1546 * 1547 * @param {string} str string to measure 1548 * @param {boolean} bold if true, string should be measured in bold font 1549 * @param {string|number} font size to measure string in. If unset, use default font size 1550 */ 1551 AjxStringUtil.getWidth = 1552 function(str, bold, fontSize) { 1553 1554 if (!AjxStringUtil._testSpan) { 1555 var span1 = AjxStringUtil._testSpan = document.createElement("SPAN"); 1556 var span2 = AjxStringUtil._testSpanBold = document.createElement("SPAN"); 1557 span1.style.position = span2.style.position = Dwt.ABSOLUTE_STYLE; 1558 var shellEl = DwtShell.getShell(window).getHtmlElement(); 1559 shellEl.appendChild(span1); 1560 shellEl.appendChild(span2); 1561 Dwt.setLocation(span1, Dwt.LOC_NOWHERE, Dwt.LOC_NOWHERE); 1562 Dwt.setLocation(span2, Dwt.LOC_NOWHERE, Dwt.LOC_NOWHERE); 1563 span2.style.fontWeight = "bold"; 1564 } 1565 1566 if (AjxUtil.isString(fontSize)) { 1567 fontSize = DwtCssStyle.asPixelCount(fontSize); 1568 } 1569 var sz = "" + (fontSize || 0); // 0 means "default"; 1570 1571 var cache = bold ? AjxStringUtil.WIDTH_BOLD : AjxStringUtil.WIDTH; 1572 if (cache[str] && cache[str][sz]) { 1573 return cache[str][sz]; 1574 } 1575 1576 if (AjxStringUtil._cacheSize >= AjxStringUtil.MAX_CACHE) { 1577 AjxStringUtil.WIDTH = {}; 1578 AjxStringUtil.WIDTH_BOLD = {}; 1579 AjxStringUtil._cacheSize = 0; 1580 } 1581 1582 var span = bold ? AjxStringUtil._testSpanBold : AjxStringUtil._testSpan; 1583 span.innerHTML = str; 1584 span.style.fontSize = fontSize ? (fontSize+"px") : null; 1585 1586 if (!cache[str]) { 1587 cache[str] = {}; 1588 } 1589 1590 var w = cache[str][sz] = Dwt.getSize(span).x; 1591 AjxStringUtil._cacheSize++; 1592 1593 return w; 1594 }; 1595 1596 /** 1597 * Fits as much of a string within the given width as possible. If truncation is needed, adds an ellipsis. 1598 * Truncation could happen at any letter, and not necessarily at a word boundary. 1599 * 1600 * @param {String} str a string 1601 * @param {Number} width available width in pixels 1602 * 1603 * @returns {String} string (possibly truncated) that fits in width 1604 */ 1605 AjxStringUtil.fitString = function(str, width) { 1606 1607 var strWidth = AjxStringUtil.getWidth(str); 1608 if (strWidth < width) { 1609 return str; 1610 } 1611 1612 var ell = AjxStringUtil.ELLIPSIS_NO_SPACE, 1613 ellWidth = AjxStringUtil.getWidth(ell); 1614 1615 while (str.length > 0) { 1616 if (AjxStringUtil.getWidth(str) + ellWidth < width) { 1617 return str + ell; 1618 } 1619 else { 1620 str = str.substring(0, str.length - 1); // remove last letter and try again 1621 } 1622 } 1623 1624 return ''; 1625 }; 1626 1627 /** 1628 * correct the cross domain reference in passed url content 1629 * eg: http://<ipaddress>/ url might have rest url page which points to http://<server name>/ pages 1630 * 1631 */ 1632 AjxStringUtil.fixCrossDomainReference = 1633 function(url, restUrlAuthority, convertToRelativeURL) { 1634 var urlParts = AjxStringUtil.parseURL(url); 1635 if (urlParts.authority == window.location.host) { 1636 return url; 1637 } 1638 1639 if ((restUrlAuthority && url.indexOf(restUrlAuthority) >=0) || !restUrlAuthority) { 1640 if (convertToRelativeURL) { 1641 url = urlParts.path; 1642 } 1643 else { 1644 var oldRef = urlParts.protocol + "://" + urlParts.authority; 1645 var newRef = window.location.protocol + "//" + window.location.host; 1646 url = url.replace(oldRef, newRef); 1647 } 1648 } 1649 return url; 1650 }; 1651 1652 1653 AjxStringUtil._dummyDiv = document.createElement("DIV"); 1654 1655 AjxStringUtil.htmlPlatformIndependent = 1656 function(html) { 1657 var div = AjxStringUtil._dummyDiv; 1658 div.innerHTML = html; 1659 var inner = div.innerHTML; 1660 div.innerHTML = ""; 1661 return inner; 1662 }; 1663 1664 /** 1665 * compare two html code fragments, ignoring the case of tags, since the tags inside innnerHTML are returned differently by different browsers (and from Outlook) 1666 * e.g. IE returns CAPS for tag names in innerHTML while FF returns lowercase tag names. Outlook signature creation also returns lowercase. 1667 * this approach is also good in case the browser removes some of the innerHTML set to it, like I suspect might be in the case of stuff coming from Outlook. (e.g. it removes head tag since it's illegal inside a div) 1668 * 1669 * @param html1 1670 * @param html2 1671 */ 1672 AjxStringUtil.equalsHtmlPlatformIndependent = 1673 function(html1, html2) { 1674 return AjxStringUtil.htmlPlatformIndependent(html1) == AjxStringUtil.htmlPlatformIndependent(html2); 1675 }; 1676 1677 // Stuff for parsing messages to find original (as opposed to quoted) content 1678 1679 // types of content related to finding original content; not all are used 1680 AjxStringUtil.ORIG_UNKNOWN = "UNKNOWN"; 1681 AjxStringUtil.ORIG_QUOTED = "QUOTED"; 1682 AjxStringUtil.ORIG_SEP_STRONG = "SEP_STRONG"; 1683 AjxStringUtil.ORIG_SEP_WEAK = "SEP_WEAK"; 1684 AjxStringUtil.ORIG_WROTE_STRONG = "WROTE_STRONG"; 1685 AjxStringUtil.ORIG_WROTE_WEAK = "WROTE_WEAK"; 1686 AjxStringUtil.ORIG_HEADER = "HEADER"; 1687 AjxStringUtil.ORIG_LINE = "LINE"; 1688 AjxStringUtil.ORIG_SIG_SEP = "SIG_SEP"; 1689 1690 // regexes for parsing msg body content so we can figure out what was quoted and what's new 1691 // TODO: should these be moved to AjxMsg to be fully localizable? 1692 AjxStringUtil.MSG_REGEXES = [ 1693 { 1694 // the two most popular quote characters, > and | 1695 type: AjxStringUtil.ORIG_QUOTED, 1696 regex: /^\s*(>|\|)/ 1697 }, 1698 { 1699 // marker for Original or Forwarded message, used by ZCS and others 1700 type: AjxStringUtil.ORIG_SEP_STRONG, 1701 regex: new RegExp("^\\s*--+\\s*(" + AjxMsg.origMsg + "|" + AjxMsg.forwardedMessage + "|" + AjxMsg.origAppt + ")\\s*--+\\s*$", "i") 1702 }, 1703 { 1704 // marker for Original or Forwarded message, used by ZCS and others 1705 type: AjxStringUtil.ORIG_SEP_STRONG, 1706 regex: new RegExp("^" + AjxMsg.forwardedMessage1 + "$", "i") 1707 }, 1708 { 1709 // one of the commonly quoted email headers 1710 type: AjxStringUtil.ORIG_HEADER, 1711 regex: new RegExp("^\\s*(" + [AjxMsg.from, AjxMsg.to, AjxMsg.subject, AjxMsg.date, AjxMsg.sent, AjxMsg.cc].join("|") + ")") 1712 }, 1713 { 1714 // some clients use a series of underscores as a text-mode separator (text version of <hr>) 1715 type: AjxStringUtil.ORIG_LINE, 1716 regex: /^\s*_{5,}\s*$/ 1717 }/*, 1718 { 1719 // in case a client doesn't use the exact words above 1720 type: AjxStringUtil.ORIG_SEP_WEAK, 1721 regex: /^\s*--+\s*[\w\s]+\s*--+$/ 1722 }, 1723 { 1724 // internet style signature separator 1725 type: AjxStringUtil.ORIG_SIG_SEP, 1726 regex: /^- ?-\s*$/ 1727 }*/ 1728 ]; 1729 1730 // ID for an HR to mark it as ours 1731 AjxStringUtil.HTML_SEP_ID = "zwchr"; 1732 1733 // regexes for finding a delimiter such as "On DATE, NAME (EMAIL) wrote:" 1734 AjxStringUtil.ORIG_EMAIL_RE = /[^@\s]+@[A-Za-z0-9\-]{2,}(\.[A-Za-z0-9\-]{2,})+/; // see AjxUtil.EMAIL_FULL_RE 1735 AjxStringUtil.ORIG_DATE_RE = /\d+\s*(\/|\-|, )20\d\d/; // matches "03/07/2014" or "March 3, 2014" by looking for year 20xx 1736 AjxStringUtil.ORIG_INTRO_RE = new RegExp("^(-{2,}|" + AjxMsg.on + "\\s+)", "i"); 1737 1738 1739 // Lazily creates a test hidden IFRAME and writes the given html to it, then returns the HTML element. 1740 AjxStringUtil._writeToTestIframeDoc = 1741 function(html) { 1742 var iframe; 1743 1744 if (!AjxStringUtil.__curIframeId) { 1745 iframe = document.createElement("IFRAME"); 1746 AjxStringUtil.__curIframeId = iframe.id = Dwt.getNextId(); 1747 1748 // position offscreen rather than set display:none so we can get metrics if needed; no perf difference seen 1749 Dwt.setPosition(iframe, Dwt.ABSOLUTE_STYLE); 1750 Dwt.setLocation(iframe, Dwt.LOC_NOWHERE, Dwt.LOC_NOWHERE); 1751 iframe.setAttribute('aria-hidden', true); 1752 document.body.appendChild(iframe); 1753 } else { 1754 iframe = document.getElementById(AjxStringUtil.__curIframeId); 1755 } 1756 1757 var idoc = Dwt.getIframeDoc(iframe); 1758 1759 html = html && html.replace(AjxStringUtil.IMG_SRC_CID_REGEX, '<img $1 pnsrc="cid:'); 1760 idoc.open(); 1761 idoc.write(html); 1762 idoc.close(); 1763 1764 return idoc.childNodes[0]; 1765 }; 1766 1767 // Firefox only - clean up test iframe since we can't reuse it 1768 AjxStringUtil._removeTestIframeDoc = 1769 function() { 1770 if (AjxEnv.isFirefox) { 1771 var iframe = document.getElementById(AjxStringUtil.__curIframeId); 1772 if (iframe) { 1773 iframe.parentNode.removeChild(iframe); 1774 } 1775 AjxStringUtil.__curIframeId = null; 1776 } 1777 }; 1778 1779 /** 1780 * Analyze the text and return what appears to be original (as opposed to quoted) content. We 1781 * look for separators commonly used by mail clients, as well as prefixes that indicate that 1782 * a line is being quoted. 1783 * 1784 * @param {string} text message body content 1785 * 1786 * @return {string} original content if quoted content was found, otherwise NULL 1787 */ 1788 AjxStringUtil.getOriginalContent = 1789 function(text, isHtml) { 1790 1791 if (!text) { return ""; } 1792 1793 if (isHtml) { 1794 return AjxStringUtil._getOriginalHtmlContent(text); 1795 } 1796 1797 var results = []; 1798 var lines = text.split(AjxStringUtil.SPLIT_RE); 1799 1800 var curType, curBlock = [], count = {}, isMerged, unknownBlock, isBugzilla = false; 1801 for (var i = 0; i < lines.length; i++) { 1802 var line = lines[i]; 1803 var testLine = AjxStringUtil.trim(line); 1804 1805 // blank lines are just added to the current block 1806 if (!AjxStringUtil._NON_WHITESPACE.test(testLine)) { 1807 curBlock.push(line); 1808 continue; 1809 } 1810 1811 // Bugzilla summary looks like QUOTED; it should be treated as UNKNOWN 1812 if ((testLine.indexOf("| DO NOT REPLY") === 0) && (lines[i + 2].indexOf("bugzilla") !== -1)) { 1813 isBugzilla = true; 1814 } 1815 1816 var type = AjxStringUtil._getLineType(testLine); 1817 if (type === AjxStringUtil.ORIG_QUOTED) { 1818 type = isBugzilla ? AjxStringUtil.ORIG_UNKNOWN : type; 1819 } 1820 else { 1821 isBugzilla = false; 1822 } 1823 1824 // WROTE can stretch over two lines; if so, join them into one line 1825 var nextLine = lines[i + 1]; 1826 var isMerged = false; 1827 if (nextLine && (type === AjxStringUtil.ORIG_UNKNOWN) && AjxStringUtil.ORIG_INTRO_RE.test(testLine) && nextLine.match(/\w+:$/)) { 1828 testLine = [testLine, nextLine].join(" "); 1829 type = AjxStringUtil._getLineType(testLine); 1830 isMerged = true; 1831 } 1832 1833 // LINE sometimes used as delimiter; if HEADER follows, lump it in with them 1834 if (type === AjxStringUtil.ORIG_LINE) { 1835 var j = i + 1; 1836 nextLine = lines[j]; 1837 while (!AjxStringUtil._NON_WHITESPACE.test(nextLine) && j < lines.length) { 1838 nextLine = lines[++j]; 1839 } 1840 var nextType = nextLine && AjxStringUtil._getLineType(nextLine); 1841 if (nextType === AjxStringUtil.ORIG_HEADER) { 1842 type = AjxStringUtil.ORIG_HEADER; 1843 } 1844 else { 1845 type = AjxStringUtil.ORIG_UNKNOWN; 1846 } 1847 } 1848 1849 // see if we're switching to a new type; if so, package up what we have so far 1850 if (curType) { 1851 if (curType !== type) { 1852 results.push({type:curType, block:curBlock}); 1853 unknownBlock = (curType === AjxStringUtil.ORIG_UNKNOWN) ? curBlock : unknownBlock; 1854 count[curType] = count[curType] ? count[curType] + 1 : 1; 1855 curBlock = []; 1856 curType = type; 1857 } 1858 } 1859 else { 1860 curType = type; 1861 } 1862 1863 if (isMerged && (type === AjxStringUtil.ORIG_WROTE_WEAK || type === AjxStringUtil.ORIG_WROTE_STRONG)) { 1864 curBlock.push(line); 1865 curBlock.push(nextLine); 1866 i++; 1867 isMerged = false; 1868 } 1869 else { 1870 curBlock.push(line); 1871 } 1872 } 1873 1874 // Handle remaining content 1875 if (curBlock.length) { 1876 results.push({type:curType, block:curBlock}); 1877 unknownBlock = (curType === AjxStringUtil.ORIG_UNKNOWN) ? curBlock : unknownBlock; 1878 count[curType] = count[curType] ? count[curType] + 1 : 1; 1879 } 1880 1881 // Now it's time to analyze all these blocks that we've classified 1882 1883 // Check for UNKNOWN followed by HEADER 1884 var first = results[0], second = results[1]; 1885 if (first && first.type === AjxStringUtil.ORIG_UNKNOWN && second && (second.type === AjxStringUtil.ORIG_HEADER || second.type === AjxStringUtil.ORIG_WROTE_STRONG)) { 1886 var originalText = AjxStringUtil._getTextFromBlock(first.block); 1887 if (originalText) { 1888 var third = results[2]; 1889 if (third && third.type === AjxStringUtil.ORIG_UNKNOWN) { 1890 var originalThirdText = AjxStringUtil._getTextFromBlock(third.block); 1891 if (originalThirdText && originalThirdText.indexOf(ZmItem.NOTES_SEPARATOR) !== -1) { 1892 return originalText + originalThirdText; 1893 } 1894 } 1895 return originalText; 1896 } 1897 } 1898 1899 // check for special case of WROTE preceded by UNKNOWN, followed by mix of UNKNOWN and QUOTED (inline reply) 1900 var originalText = AjxStringUtil._checkInlineWrote(count, results, false); 1901 if (originalText) { 1902 return originalText; 1903 } 1904 1905 // If we found quoted content and there's exactly one UNKNOWN block, return it. 1906 if (count[AjxStringUtil.ORIG_UNKNOWN] === 1 && count[AjxStringUtil.ORIG_QUOTED] > 0) { 1907 var originalText = AjxStringUtil._getTextFromBlock(unknownBlock); 1908 if (originalText) { 1909 return originalText; 1910 } 1911 } 1912 1913 // If we have a STRONG separator (eg "--- Original Message ---"), consider it authoritative and return the text that precedes it 1914 if (count[AjxStringUtil.ORIG_SEP_STRONG] > 0) { 1915 var block = []; 1916 for (var i = 0; i < results.length; i++) { 1917 var result = results[i]; 1918 if (result.type === AjxStringUtil.ORIG_SEP_STRONG) { 1919 break; 1920 } 1921 block = block.concat(result.block); 1922 } 1923 var originalText = AjxStringUtil._getTextFromBlock(block); 1924 if (originalText) { 1925 return originalText; 1926 } 1927 } 1928 1929 return text; 1930 }; 1931 1932 // Matches a line of text against some regexes to see if has structural meaning within a mail msg. 1933 AjxStringUtil._getLineType = 1934 function(testLine) { 1935 1936 var type = AjxStringUtil.ORIG_UNKNOWN; 1937 1938 // see if the line matches any known delimiters or quote patterns 1939 for (var j = 0; j < AjxStringUtil.MSG_REGEXES.length; j++) { 1940 var msgTest = AjxStringUtil.MSG_REGEXES[j]; 1941 var regex = msgTest.regex; 1942 if (regex.test(testLine.toLowerCase())) { 1943 // line that starts and ends with | is considered ASCII art (eg a table) rather than quoted 1944 if (msgTest.type == AjxStringUtil.ORIG_QUOTED && /^\s*\|.*\|\s*$/.test(testLine)) { 1945 continue; 1946 } 1947 type = msgTest.type; 1948 break; // first match wins 1949 } 1950 } 1951 1952 if (type === AjxStringUtil.ORIG_UNKNOWN) { 1953 // "so-and-so wrote:" takes a lot of different forms; look for various common parts and 1954 // assign points to determine confidence 1955 var m = testLine.match(/(\w+):$/); 1956 var verb = m && m[1] && m[1].toLowerCase(); 1957 if (verb) { 1958 var points = 0; 1959 // look for "wrote:" (and discount "changed:", which is used by Bugzilla) 1960 points = points + (verb === AjxMsg.wrote) ? 5 : (verb === AjxMsg.changed) ? 0 : 2; 1961 if (AjxStringUtil.ORIG_EMAIL_RE.test(testLine)) { 1962 points += 4; 1963 } 1964 if (AjxStringUtil.ORIG_DATE_RE.test(testLine)) { 1965 points += 3; 1966 } 1967 var regEx = new RegExp("^(--|" + AjxMsg.on + ")", "i"); 1968 if (AjxStringUtil.ORIG_INTRO_RE.test(testLine)) { 1969 points += 1; 1970 } 1971 if (points >= 7) { 1972 type = AjxStringUtil.ORIG_WROTE_STRONG; 1973 } 1974 else if (points >= 5) { 1975 type = AjxStringUtil.ORIG_WROTE_WEAK; 1976 } 1977 } 1978 } 1979 1980 return type; 1981 }; 1982 1983 AjxStringUtil._getTextFromBlock = 1984 function(block) { 1985 if (!(block && block.length)) { return null; } 1986 var originalText = block.join("\n") + "\n"; 1987 originalText = originalText.replace(/\s+$/, "\n"); 1988 return (AjxStringUtil._NON_WHITESPACE.test(originalText)) ? originalText : null; 1989 }; 1990 1991 AjxStringUtil.SCRIPT_REGEX = /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi; 1992 1993 // nodes to ignore; they won't have anything we're interested in 1994 AjxStringUtil.IGNORE_NODE_LIST = ["#comment", "br", "script", "select", "style"]; 1995 AjxStringUtil.IGNORE_NODE = AjxUtil.arrayAsHash(AjxStringUtil.IGNORE_NODE_LIST); 1996 1997 /** 1998 * For HTML, we strip off the html, head, and body tags and stick the rest in a temporary DOM node so that 1999 * we can go element by element. If we find one that is recognized as a separator, we remove all subsequent elements. 2000 * 2001 * @param {string} text message body content 2002 * 2003 * @return {string} original content if quoted content was found, otherwise NULL 2004 * @private 2005 */ 2006 AjxStringUtil._getOriginalHtmlContent = function(text) { 2007 2008 // strip <script> tags (which should not be there) 2009 var htmlNode = AjxStringUtil._writeToTestIframeDoc(text); 2010 while (AjxStringUtil.SCRIPT_REGEX.test(text)) { 2011 text = text.replace(AjxStringUtil.SCRIPT_REGEX, ""); 2012 } 2013 2014 var done = false, nodeList = []; 2015 AjxStringUtil._flatten(htmlNode, nodeList); 2016 2017 var ln = nodeList.length, i, results = [], count = {}, el, prevEl, nodeName, type, prevType, sepNode; 2018 for (i = 0; i < ln; i++) { 2019 el = nodeList[i]; 2020 if (el.nodeType === AjxUtil.ELEMENT_NODE) { 2021 el.normalize(); 2022 } 2023 nodeName = el.nodeName.toLowerCase(); 2024 type = AjxStringUtil._checkNode(nodeList[i]); 2025 2026 // Check for a multi-element "wrote:" attribution (usually a combo of #text and A nodes), for example: 2027 // 2028 // On Feb 28, 2014, at 3:42 PM, Joe Smith <<a href="mailto:jsmith@zimbra.com" target="_blank">jsmith@zimbra.com</a>> wrote: 2029 2030 // If the current node is a #text with a date or "On ...", find #text nodes within the next ten nodes, concatenate them, and check the result. 2031 if (type === AjxStringUtil.ORIG_UNKNOWN && el.nodeName === '#text' && 2032 (AjxStringUtil.ORIG_DATE_RE.test(el.nodeValue) || AjxStringUtil.ORIG_INTRO_RE.test(el.nodeValue))) { 2033 2034 var str = el.nodeValue; 2035 for (var j = 1; j < 10; j++) { 2036 var el1 = nodeList[i + j]; 2037 if (el1 && el1.nodeName === '#text') { 2038 str += el1.nodeValue; 2039 if (/:$/.test(str)) { 2040 type = AjxStringUtil._getLineType(AjxStringUtil.trim(str)); 2041 if (type === AjxStringUtil.ORIG_WROTE_STRONG) { 2042 i = i + j; 2043 break; 2044 } 2045 } 2046 } 2047 } 2048 } 2049 2050 if (type !== null) { 2051 results.push({ type: type, node: el, nodeName: nodeName }); 2052 count[type] = count[type] ? count[type] + 1 : 1; 2053 // definite separator 2054 if (type === AjxStringUtil.ORIG_SEP_STRONG || type === AjxStringUtil.ORIG_WROTE_STRONG) { 2055 sepNode = el; 2056 done = true; 2057 break; 2058 } 2059 // some sort of line followed by a header 2060 if (type === AjxStringUtil.ORIG_HEADER && prevType === AjxStringUtil.ORIG_LINE) { 2061 sepNode = prevEl; 2062 done = true; 2063 break; 2064 } 2065 prevEl = el; 2066 prevType = type; 2067 } 2068 } 2069 2070 if (sepNode) { 2071 AjxStringUtil._prune(sepNode, true); 2072 } 2073 2074 // convert back to text, restoring html, head, and body nodes; if there is nothing left, return original text 2075 var result = done && htmlNode.textContent ? "<html>" + htmlNode.innerHTML + "</html>" : text; 2076 2077 AjxStringUtil._removeTestIframeDoc(); 2078 return result; 2079 }; 2080 2081 /** 2082 * Traverse the given node depth-first to produce a list of descendant nodes. Some nodes are 2083 * ignored. 2084 * 2085 * @param {Element} node node 2086 * @param {Array} list result list which grows in place 2087 * @private 2088 */ 2089 AjxStringUtil._flatten = function(node, list) { 2090 2091 var nodeName = node && node.nodeName.toLowerCase(); 2092 if (AjxStringUtil.IGNORE_NODE[nodeName]) { 2093 return; 2094 } 2095 2096 list.push(node); 2097 2098 var children = node.childNodes || []; 2099 for (var i = 0; i < children.length; i++) { 2100 this._flatten(children[i], list); 2101 } 2102 }; 2103 2104 /** 2105 * Removes all subsequent siblings of the given node, and then does the same for its parent. 2106 * The effect is that all nodes that come after the given node in a depth-first traversal of 2107 * the DOM will be removed. 2108 * 2109 * @param {Element} node 2110 * @param {Boolean} clipNode if true, also remove the node 2111 * @private 2112 */ 2113 AjxStringUtil._prune = function(node, clipNode) { 2114 2115 var p = node && node.parentNode; 2116 // clip all subsequent nodes 2117 while (p && p.lastChild && p.lastChild !== node) { 2118 p.removeChild(p.lastChild); 2119 } 2120 // clip the node if asked 2121 if (clipNode && p && p.lastChild === node) { 2122 p.removeChild(p.lastChild); 2123 } 2124 var nodeName = p && p.nodeName.toLowerCase(); 2125 if (p && nodeName !== 'body' && nodeName !== 'html') { 2126 AjxStringUtil._prune(p, false); 2127 } 2128 }; 2129 2130 /** 2131 * Tries to determine the type of the given node. 2132 * 2133 * @param {Element} el a DOM node 2134 * @return {String} type, or null 2135 * @private 2136 */ 2137 AjxStringUtil._checkNode = function(el) { 2138 2139 if (!el) { return null; } 2140 2141 var nodeName = el.nodeName.toLowerCase(); 2142 var type = null; 2143 2144 // Text node: test against our regexes 2145 if (nodeName === "#text") { 2146 var content = AjxStringUtil.trim(el.nodeValue); 2147 if (AjxStringUtil._NON_WHITESPACE.test(content)) { 2148 type = AjxStringUtil._getLineType(content); 2149 } 2150 } 2151 // HR: look for a couple different forms that are used to delimit quoted content 2152 else if (nodeName === "hr") { 2153 // see if the HR is ours, or one commonly used by other mail clients such as Outlook 2154 if (el.id === AjxStringUtil.HTML_SEP_ID || (el.size === "2" && el.width === "100%" && el.align === "center")) { 2155 type = AjxStringUtil.ORIG_SEP_STRONG; 2156 } 2157 else { 2158 type = AjxStringUtil.ORIG_LINE; 2159 } 2160 } 2161 // PRE: treat as one big line of text (should maybe go line by line) 2162 else if (nodeName === "pre") { 2163 type = AjxStringUtil._checkNodeContent(el); 2164 } 2165 // DIV: check for Outlook class used as delimiter, or a top border used as a separator, and finally just 2166 // check the text content 2167 else if (nodeName === "div") { 2168 if (el.className === "OutlookMessageHeader" || el.className === "gmail_quote") { 2169 type = AjxStringUtil.ORIG_SEP_STRONG; 2170 } 2171 else if (el.style.borderTop) { 2172 var styleObj = DwtCssStyle.getComputedStyleObject(el); 2173 if (styleObj && styleObj.borderTopWidth && parseInt(styleObj.borderTopWidth) === 1 && styleObj.borderTopColor) { 2174 type = AjxStringUtil.ORIG_SEP_STRONG; 2175 } 2176 } 2177 type = type || AjxStringUtil._checkNodeContent(el); 2178 } 2179 // SPAN: check text content 2180 else if (nodeName === "span") { 2181 type = type || AjxStringUtil._checkNodeContent(el); 2182 } 2183 // IMG: treat as original content 2184 else if (nodeName === "img") { 2185 type = AjxStringUtil.ORIG_UNKNOWN; 2186 } 2187 // BLOCKQUOTE: treat as quoted section 2188 else if (nodeName === "blockquote") { 2189 type = AjxStringUtil.ORIG_QUOTED; 2190 } 2191 2192 return type; 2193 }; 2194 2195 /** 2196 * Checks textContent to see if it's a separator. 2197 * @param {Element} node 2198 * @return {String} 2199 * @private 2200 */ 2201 AjxStringUtil._checkNodeContent = function(node) { 2202 var content = node.textContent || ''; 2203 if (!AjxStringUtil._NON_WHITESPACE.test(content) || content.length > 200) { 2204 return null; 2205 } 2206 // We're really only interested in SEP_STRONG and WROTE_STRONG 2207 var type = AjxStringUtil._getLineType(content); 2208 return (type === AjxStringUtil.ORIG_SEP_STRONG || type === AjxStringUtil.ORIG_WROTE_STRONG) ? type : null; 2209 }; 2210 2211 /** 2212 * Checks the given HTML to see if it is "safe", and cleans it up if it is. It must have only 2213 * the tags in the given list, otherwise false is returned. Attributes in the given list will 2214 * be removed. It is not necessary to include "#text", "html", "head", and "body" in the list 2215 * of allowed tags. 2216 * 2217 * @param {string} html HTML text 2218 * @param {array} okTags whitelist of allowed tags 2219 * @param {array} untrustedAttrs list of attributes to not allow in non-iframe. 2220 */ 2221 AjxStringUtil.checkForCleanHtml = 2222 function(html, okTags, untrustedAttrs) { 2223 2224 var htmlNode = AjxStringUtil._writeToTestIframeDoc(html); 2225 var ctxt = { 2226 allowedTags: AjxUtil.arrayAsHash(okTags), 2227 untrustedAttrs: untrustedAttrs || [] 2228 }; 2229 AjxStringUtil._traverseCleanHtml(htmlNode, ctxt); 2230 2231 var result = "<html>" + htmlNode.innerHTML + "</html>"; 2232 2233 var width = Math.max(htmlNode.scrollWidth, htmlNode.lastChild.scrollWidth); 2234 2235 AjxStringUtil._removeTestIframeDoc(); 2236 return {html:result, width:width, useIframe:ctxt.fail}; 2237 }; 2238 2239 AjxStringUtil._traverseCleanHtml = 2240 function(el, ctxt) { 2241 2242 var isCleanHtml = true; 2243 2244 var nodeName = el.nodeName.toLowerCase(); 2245 2246 // useless <style> that we used to add, remove it 2247 if (nodeName === "style" && el.innerHTML === "p { margin: 0; }") { 2248 el.doDelete = true; 2249 } 2250 2251 // IE likes to insert an empty <title> in the <head>, let it go 2252 else if (nodeName === "title" && !el.innerHTML) { 2253 } 2254 2255 // see if tag is allowed 2256 else if (ctxt.allowedTags[nodeName]) { 2257 2258 //checks for invalid styles and removes them. Bug: 78875 - bad styles from user = email displays incorrectly 2259 if (el.style) { 2260 var style = el.style && el.style.cssText; 2261 style = style.toLowerCase(); 2262 if (!AjxStringUtil._checkStyle(style)){ 2263 isCleanHtml = false; 2264 } 2265 el.style.cssText = AjxStringUtil._fixStyle(style); 2266 } 2267 2268 if (el.removeAttribute && el.attributes && el.attributes.length) { 2269 // check for blacklisted attrs 2270 for (var i = 0; i < ctxt.untrustedAttrs.length; i++) { 2271 if (el.hasAttribute(ctxt.untrustedAttrs[i])) { 2272 isCleanHtml = false; 2273 } 2274 } 2275 2276 // Note that DOM-based handling of attributes is horribly broken in IE, in all sorts of ways. 2277 // In IE it is impossible to find a reliable way to get an attribute's value. The attributes 2278 // collection is supposed to be attributes that were specified in the HTML, but IE fills it with every 2279 // possible attribute. 2280 for (var i = 0, attrs = el.attributes, l = attrs.length; i < l; i++) { 2281 var attr = attrs.item(i); 2282 if (!attr) { 2283 continue; 2284 } 2285 var attrName = attr.nodeName && attr.nodeName.toLowerCase(); 2286 // on* handlers (should have been removed by server, check again to be safe) 2287 if (attrName && attrName.indexOf("on") === 0) { 2288 el.removeAttribute(attrName); 2289 continue; 2290 } 2291 // this might not work in IE 2292 var attrValue = attr.nodeValue && String(attr.nodeValue); 2293 if (attrValue) { 2294 attrValue = attrValue.toLowerCase(); 2295 // we have global CSS rules for TD that trump table properties, so bail 2296 if (nodeName === "table" && (attrName === "cellpadding" || attrName === "cellspacing" || 2297 attrName === "border") && attrValue !== "0") { 2298 isCleanHtml = false; 2299 } 2300 } 2301 } 2302 } 2303 } 2304 2305 // disallowed tag - bail 2306 else { 2307 isCleanHtml = false; 2308 } 2309 2310 // process child nodes 2311 for (var i = 0, len = el.childNodes.length; i < len; i++) { 2312 var childNode = el.childNodes[i]; 2313 AjxStringUtil._traverseCleanHtml(childNode, ctxt); 2314 } 2315 2316 // remove nodes marked for deletion 2317 for (var i = el.childNodes.length - 1; i >= 0; i--) { 2318 var childNode = el.childNodes[i]; 2319 if (childNode.doDelete) { 2320 el.removeChild(childNode); 2321 } 2322 } 2323 2324 if (!isCleanHtml){ 2325 ctxt.fail = true; 2326 } 2327 }; 2328 2329 2330 AjxStringUtil._checkStyle = 2331 function(style) { 2332 2333 //check for absolute positioning 2334 if (style.match(/\bposition\s*:\s*absolute[^;]*;?/)){ 2335 return false; 2336 } 2337 2338 //check for font-<anything> 2339 if (style.match(/\bfont-[^;]*;?/)){ 2340 return false; 2341 } 2342 2343 return true; 2344 }; 2345 2346 AjxStringUtil._fixStyle = 2347 function(style) { 2348 2349 //check for negative margins 2350 style = style.replace(/\bmargin-?(top|left|right|bottom)?\s*:[^;]*-\d+[^;]*;?/gi, ""); 2351 2352 //check for negative padding 2353 style = style.replace(/\bpadding-?(top|left|right|bottom)?\s*:[^;]*-\d+[^;]*;?/gi, ""); 2354 2355 //remove absolute and fixed positioning 2356 style = style.replace(/\bposition\s*:\s*(absolute|fixed)[^;]*;?/, ""); 2357 2358 return style; 2359 }; 2360 2361 /** 2362 * A "... wrote:" separator is not quite as authoritative, since the user might be replying inline. If we have 2363 * a single UNKNOWN block before the WROTE separator, return it unless there is a mix of QUOTED and UNKNOWN 2364 * following the separator, except if there's only a single unknown block after the separator and it comes last. 2365 * 2366 * @private 2367 */ 2368 AjxStringUtil._checkInlineWrote = 2369 function(count, results) { 2370 2371 if (count[AjxStringUtil.ORIG_WROTE_STRONG] > 0) { 2372 var unknownBlock, foundSep = false, afterSep = {}; 2373 for (var i = 0; i < results.length; i++) { 2374 var result = results[i], type = result.type; 2375 if (type === AjxStringUtil.ORIG_WROTE_STRONG) { 2376 foundSep = true; 2377 } 2378 else if (type === AjxStringUtil.ORIG_UNKNOWN && !foundSep) { 2379 if (unknownBlock) { 2380 return null; 2381 } 2382 else { 2383 unknownBlock = result.block; 2384 } 2385 } 2386 else if (foundSep) { 2387 afterSep[type] = true; 2388 } 2389 } 2390 2391 var mixed = (afterSep[AjxStringUtil.ORIG_UNKNOWN] && afterSep[AjxStringUtil.ORIG_QUOTED]); 2392 var endsWithUnknown = (count[AjxStringUtil.ORIG_UNKNOWN] === 2 && results[results.length - 1].type === AjxStringUtil.ORIG_UNKNOWN); 2393 if (unknownBlock && (!mixed || endsWithUnknown)) { 2394 var originalText = AjxStringUtil._getTextFromBlock(unknownBlock); 2395 if (originalText) { 2396 return originalText; 2397 } 2398 } 2399 } 2400 }; 2401 2402 /** 2403 * Removes non-content HTML from the beginning and end. The idea is to remove anything that would 2404 * appear to the user as blank space. This function is an approximation since that's hard to do, 2405 * especially when dealing with HTML as a string. 2406 * 2407 * @param {String} html HTML to fix 2408 * @return {String} trimmed HTML 2409 * @adapts AjxStringUtil.trimHtml 2410 */ 2411 AjxStringUtil.trimHtml = function(html) { 2412 2413 if (!html) { 2414 return ''; 2415 } 2416 var trimmedHtml = html; 2417 2418 // remove doc-level tags if they don't have attributes 2419 trimmedHtml = trimmedHtml.replace(AjxStringUtil.DOC_TAG_REGEX, ''); 2420 2421 // some editors like to put every <br> in a <div> 2422 trimmedHtml = trimmedHtml.replace(/<div><br ?\/?><\/div>/gi, '<br>'); 2423 2424 // remove leading/trailing <br> 2425 var len = 0; 2426 while (trimmedHtml.length !== len && (/^<br ?\/?>/i.test(trimmedHtml) || /<br ?\/?>$/i.test(trimmedHtml))) { 2427 len = trimmedHtml.length; // loop prevention 2428 trimmedHtml = trimmedHtml.replace(/^<br ?\/?>/i, "").replace(/<br ?\/?>$/i, ""); 2429 } 2430 2431 // remove trailing <br> trapped in front of closing tags 2432 var m = trimmedHtml && trimmedHtml.match(/((<br ?\/?>)+)((<\/\w+>)+)$/i); 2433 if (m && m.length) { 2434 var regex = new RegExp(m[1] + m[3] + '$', 'i'); 2435 trimmedHtml = trimmedHtml.replace(regex, m[3]); 2436 } 2437 2438 // remove empty internal <div> containers 2439 trimmedHtml = trimmedHtml.replace(/(<div><\/div>)+/gi, ''); 2440 2441 return AjxStringUtil.trim(trimmedHtml); 2442 }; 2443 2444 // regex for removing empty doc tags from an HTML string 2445 AjxStringUtil.DOC_TAG_REGEX = /<\/?(html|head|body)>/gi; 2446 2447 // Convert the html to DOM nodes, update the img src with the defanged field value, 2448 // and then return the html inside the body. 2449 // TODO: See about using a DocumentFragment 2450 AjxStringUtil.defangHtmlContent = function(html) { 2451 var htmlNode = AjxStringUtil._writeToTestIframeDoc(html); 2452 var images = htmlNode.getElementsByTagName("img"); 2453 if (images && images.length) { 2454 var imgEl; 2455 var dfSrcContent; 2456 var pnSrcContent; 2457 for (var i = 0; i < images.length; i++) { 2458 imgEl = images[i]; 2459 dfSrcContent = imgEl.getAttribute("dfsrc"); 2460 if (dfSrcContent && (dfSrcContent !== "#")) { 2461 imgEl.setAttribute("src", dfSrcContent); 2462 } else { 2463 pnSrcContent = imgEl.getAttribute("pnsrc"); 2464 if (pnSrcContent && (pnSrcContent !== "#")) { 2465 imgEl.setAttribute("src", pnSrcContent); 2466 } 2467 } 2468 imgEl.removeAttribute("dfsrc"); 2469 } 2470 } 2471 var content = ""; 2472 var children = htmlNode.childNodes; 2473 for (var i = 0; i < children.length; i++) { 2474 if (children[i].tagName && (children[i].tagName.toLowerCase() === "body")) { 2475 content = children[i].innerHTML; 2476 break; 2477 } 2478 } 2479 AjxStringUtil._removeTestIframeDoc(); 2480 return content; 2481 }; 2482 2483