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