﻿(function()
{

	jQuery.fn.textProcessor = function(settings)
	{
		// merge supplied & default args
		settings = jQuery.extend(jQuery.fn.textProcessor.DefaultSettings, settings);

		return jQuery(this);
	};
	
	/*****************************
	Plugin constants
	*****************************/
	jQuery.fn.textProcessor.ElementEmptyRegex =  new RegExp("/>$");
	jQuery.fn.textProcessor.ElementRegex = new RegExp("^<");
	jQuery.fn.textProcessor.ElementEndRegex = new RegExp("^</");
	jQuery.fn.textProcessor.ElementBeginRegex = new RegExp("^<[^/]");
	jQuery.fn.textProcessor.ElementNameDetectionRegex = new RegExp("^[a-zA-Z]+[ \t\n\r/>]");
	jQuery.fn.textProcessor.KnownInlineElements = ['a','abbr','acronym','b','basefont','bdo','big','br','cite','code','dfn','em','font','i','img','input','kbd','label','q','s','samp','select','small','span','strike','strong','sub','sup','textarea','tt','u','var',];
	jQuery.fn.textProcessor.KnownBlockElements = ['address','blockquote','center','div','dl','fieldset','form','h1','h2','h3','h4','h5','h6','hr','noscript','ol','p','pre','table','ul', 'img', 'li']; // we add img  & li in here as it's a quasi-block element
	jQuery.fn.textProcessor.KnownBlockParents = ['div', 'ul', 'li', 'blockquote', 'center', 'table', 'tr', 'td', 'th', 'tbody', 'thead'];
	jQuery.fn.textProcessor.EmptyLineRegex = new RegExp("^[ \t\n\r]*$");
	jQuery.fn.textProcessor.DefaultSettings = {
		enableEmoticons: false,
		imageRoot: null,
		suppressImages: false,
		enableFormatting: true,
		enableDomainHilighting: true
	};
	

	
	/*****************************
	static methods
	*****************************/
	
	jQuery.fn.textProcessor.ProcessText = function (source, settings)
	{
		// merge supplied & default args
		settings = jQuery.extend(jQuery.fn.textProcessor.DefaultSettings, settings);

		var output = source;
		output = jQuery.fn.textProcessor.HtmlStripper.ProcessText(output, settings);
		output = jQuery.fn.textProcessor.QuoteExpander.ProcessText(output, settings);
		if (settings.enableFormatting == true)
		{
			output = jQuery.fn.textProcessor.Lister.ProcessText(output, settings).join('\n').replace(new RegExp("</li>\n<li>\n<ul>", "g"), '<ul>').replace(new RegExp("</li>\n<li>\n<ol>", "g"), '<ol>');
		}
		output = jQuery.fn.textProcessor.Paragrapher.ProcessText(output, settings);
		if (settings.enableEmoticons == true)
		{
			output = jQuery.fn.textProcessor.Emoticoner.ProcessText(output, settings);
		}
		output = jQuery.fn.textProcessor.Linker.ProcessText(output, settings);
		
		var outputDom = new jQuery.fn.textProcessor.DomNode.BuildTree(output, settings);
		if (settings.enableFormatting == true)
		{
			jQuery.fn.textProcessor.SignatureFolder.ProcessDom(outputDom, settings);
		}
		jQuery.fn.textProcessor.Replacer.ProcessDom(outputDom, settings);
		if (settings.enableFormatting == true)
		{
			jQuery.fn.textProcessor.Formatter.ProcessDom(outputDom, settings);
		}
		
		// clean dom
		outputDom.PruneEmptyDescendents();
		jQuery.fn.textProcessor.DomNode.FirstAndLastBlock(outputDom, settings);
		
		return outputDom.InnerHtml(true).join('\n');
	}
	
	jQuery.fn.textProcessor.Log = function (message, settings)
	{
		try
		{
			if (console)
			{
				console.log(message);
			}
		} 
		catch (err)
		{
		
		}
	}
	
	jQuery.fn.textProcessor.GetElementName = function (elementHtml)
	{
		var output = null;
		var alteredElementHtml = elementHtml.replace(new RegExp("^[ \t]*</*"),'');
		var names = alteredElementHtml.match(jQuery.fn.textProcessor.ElementNameDetectionRegex);
		if (names && names.length > 0) output = names[0].toLowerCase().replace(new RegExp("[^a-z]", "g"),'');
		return output;
	}
	
	jQuery.fn.textProcessor.IsElementLine = function (line)
	{
		return jQuery.fn.textProcessor.ElementRegex.test(jQuery.trim(line));
	}
	
	jQuery.fn.textProcessor.IsEmptyElementLine = function (line)
	{
		return jQuery.fn.textProcessor.ElementEmptyRegex.test(jQuery.trim(line));
	}
	
	jQuery.fn.textProcessor.IsBeginTagLine = function (line)
	{
		return jQuery.fn.textProcessor.ElementBeginRegex.test(jQuery.trim(line)) && (jQuery.fn.textProcessor.IsEmptyElementLine(line) == false);
	}
	
	jQuery.fn.textProcessor.IsEndTagLine = function (line)
	{
		return jQuery.fn.textProcessor.ElementEndRegex.test(jQuery.trim(line));
	}
	
	jQuery.fn.textProcessor.IsInlineElement = function (elementName)
	{
		return jQuery.inArray(elementName.toLowerCase(), jQuery.fn.textProcessor.KnownInlineElements) != -1;
	}
	
	jQuery.fn.textProcessor.CanContainBlockElements = function (elementName)
	{
		return jQuery.inArray(elementName.toLowerCase(), jQuery.fn.textProcessor.KnownBlockParents) != -1;
	}
	
	/******************************
	Class - HtmlStripper
	******************************/
	jQuery.fn.textProcessor.HtmlStripper = function() 
	{
		
	};
	
	/******************
	HtmlStripper - Constants
	*******************/
	jQuery.fn.textProcessor.HtmlStripper.DetectionRegex = new RegExp("<[^>]*>*", "g");
		
	/*******************
	HtmlStripper - Static Methods
	*******************/
	jQuery.fn.textProcessor.HtmlStripper.ProcessText = function (source, settings)
	{
		jQuery.fn.textProcessor.HtmlStripper.DetectionRegex.lastIndex = 0;
		var output = [];
		var lines = source.split('\n');
		for (var i=0; i<lines.length; i++)
		{
			var line = lines[i];
			line = line.replace(jQuery.fn.textProcessor.HtmlStripper.DetectionRegex, '');
			// strip ASP (ported)
			line = line.replace('%>', ' >');
			line = line.replace('%>', '< ');
			output.push(line);
		}
		return output.join('\n');
	}
	
	/******************************
	Class - QuoteExpander
	******************************/
	jQuery.fn.textProcessor.QuoteExpander = function() 
	{
		
	};
	
	/******************
	QuoteExpander - Constants
	*******************/
	jQuery.fn.textProcessor.QuoteExpander.QuoteLinePrefixRegex = new RegExp("^(>|&gt;) *");
	jQuery.fn.textProcessor.QuoteExpander.BeginTag = '<div class="quote">';
	jQuery.fn.textProcessor.QuoteExpander.EndTag = '</div>';
	jQuery.fn.textProcessor.QuoteExpander.AttributionBeginTag = '<div class="attribution">';
	jQuery.fn.textProcessor.QuoteExpander.AttributionEndTag = '&nbsp;wrote:\n</div>';
	jQuery.fn.textProcessor.QuoteExpander.AuthorBeginTag = '<span class="author">';
	jQuery.fn.textProcessor.QuoteExpander.AuthorEndTag = '</span>';
	jQuery.fn.textProcessor.QuoteExpander.QuoteBeginTag = '<blockquote>';
	jQuery.fn.textProcessor.QuoteExpander.QuoteEndTag = '</blockquote>';
	jQuery.fn.textProcessor.QuoteExpander.QuoteLineSuffixRegex = new RegExp("(<|&lt;)[ \t\n\r]*$");
	jQuery.fn.textProcessor.QuoteExpander.WroteLineSuffixRegex = new RegExp("[ \t]wrote:[ \t\n\r]*$", "i");
		
	/*******************
	QuoteExpander - Static Methods
	*******************/
	
	jQuery.fn.textProcessor.QuoteExpander.ProcessText = function (source, settings)
	{
		var lines = source.split('\n');
		var output = [];
		
		var previousLine = null;
		var isMidQuote = false;
		var currentQuoter = null;
		var currentQuote = [];
		for (var i=0; i<lines.length; i++)
		{
			var line = lines[i];
			var ltrimmedLine = line.LTrim();
			if (jQuery.fn.textProcessor.QuoteExpander.QuoteLinePrefixRegex.test(ltrimmedLine))
			{
				// quote line
				if (isMidQuote == false)
				{
					// start new quote
					isMidQuote = true;
					currentQuote = [];
					
					// set the previous line as the quoter 
					if (previousLine && jQuery.fn.textProcessor.QuoteExpander.WroteLineSuffixRegex.test(previousLine))
					{
						var quoterName = jQuery.trim(previousLine.replace(jQuery.fn.textProcessor.QuoteExpander.WroteLineSuffixRegex, ''));
						if (quoterName != '')
						{
							// pop previous line
							output.splice(output.length-1,1);
							currentQuote.push(jQuery.fn.textProcessor.QuoteExpander.AttributionBeginTag, jQuery.fn.textProcessor.QuoteExpander.AuthorBeginTag, quoterName, jQuery.fn.textProcessor.QuoteExpander.AuthorEndTag, jQuery.fn.textProcessor.QuoteExpander.AttributionEndTag);
						}
					}
					
					currentQuote.push(jQuery.fn.textProcessor.QuoteExpander.QuoteBeginTag);
				}
				var unquotedLine = ltrimmedLine.replace(jQuery.fn.textProcessor.QuoteExpander.QuoteLinePrefixRegex, '')
					.replace(jQuery.fn.textProcessor.QuoteExpander.QuoteLineSuffixRegex, '');
				currentQuote.push(unquotedLine);
			}
			else 
			{
				// non-quote line. if previous quote is finished, process it for sub-quotes and push it to output
				if (isMidQuote == true)
				{
					// process any sub-quotes
					currentQuote = jQuery.fn.textProcessor.QuoteExpander.FinishQuote(currentQuote);
					// push quote to output
					output.push(jQuery.fn.textProcessor.QuoteExpander.BeginTag, '\n', currentQuote.join('\n'), '\n', jQuery.fn.textProcessor.QuoteExpander.QuoteEndTag, jQuery.fn.textProcessor.QuoteExpander.EndTag);
					// reset for next quote
					currentQuote = [];
					isMidQuote = false;
				}
				
				output.push(line);
				previousLine = line;
			}
		}
		
		// finish any un-finished quotes
		// TODO: (jm, 09,06/09) this is repeated code (from a few lines up). Should be refactored to prevent duplicate code
		if (isMidQuote == true)
		{
			// process any sub-quotes
			currentQuote = jQuery.fn.textProcessor.QuoteExpander.FinishQuote(currentQuote, settings);
			// push quote to output
			output.push(jQuery.fn.textProcessor.QuoteExpander.BeginTag, '\n', currentQuote.join('\n'), '\n', jQuery.fn.textProcessor.QuoteExpander.QuoteEndTag, jQuery.fn.textProcessor.QuoteExpander.EndTag);
			// reset for next quote
			currentQuote = [];
			isMidQuote = false;
		}
		
		return output.join('\n');
	}
	
	jQuery.fn.textProcessor.QuoteExpander.FinishQuote = function (quoteLines, settings)
	{
		var currentQuote = quoteLines;
		if (jQuery.fn.Any(currentQuote, function(n, i) { return jQuery.fn.textProcessor.QuoteExpander.QuoteLinePrefixRegex.test(jQuery.trim(n)); }))
		{
			// process inner quote
			currentQuote = jQuery.fn.textProcessor.QuoteExpander.ProcessText(currentQuote.join('\n'), settings).split('\n');
		}
		return currentQuote;
	}
	
	
	/******************************
	Class - Lister
	******************************/
	jQuery.fn.textProcessor.Lister = function() 
	{
		
	};
	
	/******************
	Lister - Constants
	*******************/
	jQuery.fn.textProcessor.Lister.ListLinePrefixRegex = new RegExp("^ *(\\*|\\-|#) ");
	jQuery.fn.textProcessor.Lister.BulletLinePrefixRegex = new RegExp("^ *(\\*|\\-) ");
	jQuery.fn.textProcessor.Lister.OrderedLinePrefixRegex = new RegExp("^ *#");
	jQuery.fn.textProcessor.Lister.BulletListTagName = 'ul';
	jQuery.fn.textProcessor.Lister.OrderedListTagName = 'ol';
	jQuery.fn.textProcessor.Lister.ListItemBeginTag = '<li>';
	jQuery.fn.textProcessor.Lister.ListItemEndTag = '</li>';
		
	/*******************
	Lister - Static Methods
	*******************/
	
	jQuery.fn.textProcessor.Lister.ProcessText = function (source, settings)
	{
		var lines = source.split('\n');
		var output = [];
		
		var currentMarkup = null;
		var currentList = [];
		for (var i=0; i<lines.length; i++)
		{
			var line = lines[i];
			var ltrimmedLine = line.LTrim();
			if (jQuery.fn.textProcessor.Lister.BulletLinePrefixRegex.test(ltrimmedLine) == true && jQuery.fn.textProcessor.SignatureFolder.SignatureStartLineRegex.test(ltrimmedLine) == false)
			{
				// list line
				if (currentMarkup == null)
				{
					// start new list
					currentMarkup = jQuery.fn.textProcessor.Lister.BulletListTagName;
					currentList = [];
				}
				else if (currentMarkup != jQuery.fn.textProcessor.Lister.BulletListTagName)
				{
					// end previous list, start a new one
					output.push(jQuery.fn.textProcessor.Lister.FinishQuote(currentList, currentMarkup));
					currentList = [];
					currentMarkup = jQuery.fn.textProcessor.Lister.BulletListTagName;
				}
				var unquotedLine = jQuery.trim(ltrimmedLine.replace(jQuery.fn.textProcessor.Lister.BulletLinePrefixRegex, ''));
				currentList.push(unquotedLine);
			}
			else if (jQuery.fn.textProcessor.Lister.OrderedLinePrefixRegex.test(ltrimmedLine) == true && jQuery.fn.textProcessor.SignatureFolder.SignatureStartLineRegex.test(ltrimmedLine) == false)
			{
				// list line
				if (currentMarkup == null)
				{
					// start new list
					currentMarkup = jQuery.fn.textProcessor.Lister.OrderedListTagName;
					currentList = [];
				}
				else if (currentMarkup != jQuery.fn.textProcessor.Lister.OrderedListTagName)
				{
					// end previous list, start a new one
					output.push(jQuery.fn.textProcessor.Lister.FinishQuote(currentList, currentMarkup));
					currentList = [];
					currentMarkup = jQuery.fn.textProcessor.Lister.OrderedListTagName;
				}
				var unquotedLine = jQuery.trim(ltrimmedLine.replace(jQuery.fn.textProcessor.Lister.OrderedLinePrefixRegex, ''));
				currentList.push(unquotedLine);
			}
			else 
			{
				// finish previous list
				if (currentMarkup != null)
				{
					output.push(jQuery.fn.textProcessor.Lister.FinishQuote(currentList, currentMarkup));
					currentList = [];
					currentMarkup = null;
				}
					
				output.push(line);
			}
		}
		
		// finish any un-finished lists
		// TODO: (jm, 09,06/09) this is repeated code (from a few lines up). Should be refactored to prevent duplicate code
		if (currentMarkup != null)
		{
			// finish previous list
			output.push(jQuery.fn.textProcessor.Lister.FinishQuote(currentList, currentMarkup));
			currentList = [];
			currentMarkup = null;
		}
		
		return output;
	}
	
	jQuery.fn.textProcessor.Lister.FinishQuote = function (currentList, currentMarkup, settings)
	{
		if (jQuery.fn.Any(currentList, function(n, i) { return jQuery.fn.textProcessor.Lister.ListLinePrefixRegex.test(jQuery.trim(n)); }))
		{
			// process inner list
			currentList = jQuery.fn.textProcessor.Lister.ProcessText(currentList.join('\n'), settings);
		}
		
		// push list to output
		var nestedOutput = '';
		nestedOutput += '<' + currentMarkup + '>';
		for (var j=0; j<currentList.length; j++)
		{
			nestedOutput += '\n' + jQuery.fn.textProcessor.Lister.ListItemBeginTag;
			nestedOutput += '\n' + currentList[j];
			nestedOutput += '\n' + jQuery.fn.textProcessor.Lister.ListItemEndTag;
		}
		nestedOutput += '\n</' + currentMarkup + '>';
		
		return nestedOutput;
	}
	
	/******************************
	Class - Paragrapher
	******************************/
	jQuery.fn.textProcessor.Paragrapher = function() 
	{
		
	};
	
	/******************
	Paragrapher - Constants
	*******************/
	jQuery.fn.textProcessor.Paragrapher.MaxLineLengthInclusive = 80;
	jQuery.fn.textProcessor.Paragrapher.ElementRegex = new RegExp("^<");
	jQuery.fn.textProcessor.Paragrapher.ParagraphBeginFirstTag = '<p class="first">';
	jQuery.fn.textProcessor.Paragrapher.ParagraphBeginTag = '<p>';
	jQuery.fn.textProcessor.Paragrapher.ParagraphEndTag = '</p>';
	jQuery.fn.textProcessor.Paragrapher.NewLineTag = '<br/>';
	jQuery.fn.textProcessor.Paragrapher.InvalidParagraphChildren = ['div','p', 'li', 'ul', 'ol'];
	jQuery.fn.textProcessor.Paragrapher.InvalidParagraphParents = ['span'];
		
	/*******************
	Paragrapher - Static Methods
	*******************/
	
	jQuery.fn.textProcessor.Paragrapher.ProcessText = function (source, settings)
	{
		var lines = source.split('\n');
		var output = [];
		var isMidParagraph = false;
		var parentElement = null;
		var isFirstParagraph = true;
		var previousLineWasMarkup = false;
		for (var i=0; i<lines.length; i++)
		{
			var line = lines[i];
			
			if (!jQuery.fn.textProcessor.EmptyLineRegex.test(line))
			{
				var trimmedLine = jQuery.trim(line);
				if (jQuery.fn.textProcessor.Paragrapher.ElementRegex.test(trimmedLine))
				{
					if (jQuery.fn.textProcessor.IsEmptyElementLine(trimmedLine))
					{
						// empty element, push it out
						output.push(trimmedLine);
						previousLineWasMarkup = true;
					}
					else if (jQuery.fn.textProcessor.IsEndTagLine(trimmedLine))
					{
						parentElement = jQuery.fn.textProcessor.GetElementName(trimmedLine);
						if (jQuery.inArray(parentElement, jQuery.fn.textProcessor.Paragrapher.InvalidParagraphChildren) != -1 && isMidParagraph == true && jQuery.inArray(parentElement, jQuery.fn.textProcessor.Paragrapher.InvalidParagraphParents) == -1)
						{
							output.push(jQuery.fn.textProcessor.Paragrapher.ParagraphEndTag);
							isMidParagraph = false;
						}
						
						// markup, just push directly to output
						output.push(trimmedLine);
						previousLineWasMarkup = true;
					}
					else
					{
						// if this is the beginning of a new element, determine whether we need to close any open paragraph tags
						parentElement = jQuery.fn.textProcessor.GetElementName(trimmedLine);
						if (jQuery.inArray(parentElement, jQuery.fn.textProcessor.Paragrapher.InvalidParagraphChildren) != -1 && isMidParagraph == true)
						{
							output.push(jQuery.fn.textProcessor.Paragrapher.ParagraphEndTag);
							isFirstParagraph = true;
							isMidParagraph = false;
						}
						else if (jQuery.inArray(parentElement, jQuery.fn.textProcessor.Paragrapher.InvalidParagraphParents) != -1 && isMidParagraph == false)
						{
							// we've encountered an inline element but aren't in a paragraph
							output.push(jQuery.fn.textProcessor.Paragrapher.ParagraphBeginTag);
							isMidParagraph = true;
						}
						
						// markup, just push directly to output
						output.push(trimmedLine);
						previousLineWasMarkup = true;
					}
				}
				else 
				{
					// we have a non-markup, non-empty line
					
					// determine whether a new paragraph should be started
					if (isMidParagraph == false)
					{
						// TODO: (jm, 10/6/09) start new paragraphs only if it is valid to do so in the current context
						isMidParagraph = true;
						output.push(jQuery.fn.textProcessor.Paragrapher.ParagraphBeginTag);
					}
					
					if (trimmedLine.length <= jQuery.fn.textProcessor.Paragrapher.MaxLineLengthInclusive)
					{
						if (output[output.length-1] != jQuery.fn.textProcessor.Paragrapher.ParagraphBeginTag && previousLineWasMarkup == false)
						{
							output.push(jQuery.fn.textProcessor.Paragrapher.NewLineTag);
						}
						output.push(trimmedLine);
						previousLineWasMarkup = false;
					}
					else
					{
						// line is longer than N characters, just add it
						if (isMidParagraph == true && output[output.length-1] != jQuery.fn.textProcessor.Paragrapher.ParagraphBeginTag)
						{
							output.push(jQuery.fn.textProcessor.Paragrapher.ParagraphEndTag);
							output.push(jQuery.fn.textProcessor.Paragrapher.ParagraphBeginTag);
							isMidParagraph = false;
						}
						output.push(trimmedLine);
						isMidParagraph = true;
						previousLineWasMarkup = false;
					}
				}
			}
			else
			{
				// we've encountered a blank line. end the current paragraph
				if (isMidParagraph == true)
				{
					output.push(jQuery.fn.textProcessor.Paragrapher.ParagraphEndTag);
					isMidParagraph = false;
				}
			}
		}
		
		if (isMidParagraph == true)
		{
			output.push(jQuery.fn.textProcessor.Paragrapher.ParagraphEndTag);
		}
		
		return output.join('\n');
	}
	
	/*********************
	Paragrapher - Statis Methods
	**********************/
	
	// ensures that inline elements are nested inside paragraph tags, trims br elements
	jQuery.fn.textProcessor.Paragrapher.EnsureBlockInlineNesting = function (node)
	{
		if (node.IsInlineElement() == false && node.tagName != 'p')
		{
			// walk through the child collection wrapping clumps of inline elements in p elements
			var firstInlineIndex = -1;
			for (var i=0; i<node.children.length; i++)
			{
				var child = node.children[i];
				if (child.IsInlineElement() == true)
				{
					if (firstInlineIndex == -1)
					{
						firstInlineIndex = i;
					}
				}
				else if (firstInlineIndex != -1) 
				{
					// we've encountered a block level element following a clump of inline elements
					node.WrapChildren(firstInlineIndex, i, jQuery.fn.textProcessor.Paragrapher.ParagraphBeginTag, jQuery.fn.textProcessor.Paragrapher.ParagraphEndTag);
					i = firstInlineIndex+1;
					firstInlineIndex = -1;
				}
			}
			if (firstInlineIndex != -1) 
			{
				// we've reached the end of the collection following a clump of inline elements
				node.WrapChildren(firstInlineIndex, node.children.length, jQuery.fn.textProcessor.Paragrapher.ParagraphBeginTag, jQuery.fn.textProcessor.Paragrapher.ParagraphEndTag);
			}
		}
	}
	
	
	
	/******************************
	Class - Emoticoner
	******************************/
	jQuery.fn.textProcessor.Emoticoner = function() 
	{
		
	};
	
	/******************
	Emoticoner - Constants
	*******************/
	jQuery.fn.textProcessor.Emoticoner.DetectionRegexWholeString = new RegExp("(^|[ \t\n\r])(:|;|=)\\-*[()OD0x|Pp]", "g");
	jQuery.fn.textProcessor.Emoticoner.DetectionRegexWholeLine = new RegExp("(^|[ \t])(:|;|=)\\-*[()OD]", "g");
	// name attribute should contain no spaces
	jQuery.fn.textProcessor.Emoticoner.Emoticons = [
		{ name: 'smile', regex: new RegExp("(^|[ \t])(:|=)\\-*\\)", "g"), imagePath: 'images/emoticon-smile.gif'},
		{ name: 'sad', regex: new RegExp("(^|[ \t])(:|=)\\-*\\(", "g"), imagePath: 'images/emoticon-sad.gif'},
		{ name: 'grin', regex: new RegExp("(^|[ \t])(:|=)\\-*D", "g"), imagePath: 'images/emoticon-grin.gif'},
		{ name: 'surprise', regex: new RegExp("(^|[ \t]):\\-*(O|0)", "g"), imagePath: 'images/emoticon-surprise.gif'},
		{ name: 'wink', regex: new RegExp("(^|[ \t]);\\-*\\)", "g"), imagePath: 'images/emoticon-wink.gif'},
		{ name: 'angry', regex: new RegExp("(^|[ \t])(:|=)\\-*x", "g"), imagePath: 'images/emoticon-angry.gif'},
		{ name: 'tongue', regex: new RegExp("(^|[ \t])(:|=)\\-*(P|p)", "g"), imagePath: 'images/emoticon-tongue.gif'},
		{ name: 'neutral', regex: new RegExp("(^|[ \t])(:|=)\\-*\\|", "g"), imagePath: 'images/emoticon-neutral.gif'}
	];
		
	/*******************
	Emoticoner - Static Methods
	*******************/
	
	jQuery.fn.textProcessor.Emoticoner.ProcessText = function (source, settings)
	{
		var output = [];
		// detect whether there appears to be any emoticons in string (as the emoticon search operation may become expensive over time and we only want to run it if there are actually emoticons)
		if (jQuery.fn.textProcessor.Emoticoner.DetectionRegexWholeString.test(source))
		{
			var lines = source.split('\n');
			for (var i=0; i<lines.length; i++)
			{
				var line = lines[i];
				
				// detect emoticons
				for (var j=0; j<jQuery.fn.textProcessor.Emoticoner.Emoticons.length; j++)
				{
					var emoticon = jQuery.fn.textProcessor.Emoticoner.Emoticons[j];
					line = line.replace(emoticon.regex, '\n&nbsp;\n<img src="' + settings.imageRoot + emoticon.imagePath + '" width="15" height="15" alt="emoticon - ' + emoticon.name + '" class="emoticon emoticon-' + emoticon.name + '" />\n');
					// HACK: (jm, 11/06/09) reset regex last index (javascript regex with 'g' option is flakey in firefox and chrome)
					emoticon.regex.lastIndex = 0;
				}
				
				output.push(line);
			}
		}
		else
		{
			// no emoticons detected, regugitate input
			output.push(source);
		}
		// HACK: (jm, 11/06/09) reset regex last index (javascript regex with 'g' option is flakey in firefox and chrome)
		jQuery.fn.textProcessor.Emoticoner.DetectionRegexWholeString.lastIndex = 0;
		
		return output.join('\n').replace(new RegExp("<p>\\n+&nbsp;", "g"), "<p>\n").replace(new RegExp("<br/>\\n+&nbsp;", "g"), "<br/>\n");
	}
	
	
	
	
	/******************************
	Class - Linker
	******************************/
	jQuery.fn.textProcessor.Linker = function() 
	{
		
	};
	
	/******************
	Linker - Constants
	*******************/
	jQuery.fn.textProcessor.Linker.EmailDetectionQuickRegex = new RegExp("@", "g");
	jQuery.fn.textProcessor.Linker.EmailDetectionSlowRegex = new RegExp("(^|\n|\\(|\\)|\\[|\\]|\\{|\\}| |\\+)[a-zA-Z0-9_\\.\\-]+@");
	jQuery.fn.textProcessor.Linker.EmailDetectionInnerRegex = new RegExp("[a-zA-Z0-9_\\.\\-]+@");
	jQuery.fn.textProcessor.Linker.EmailPrefix = 'mailto:';
	jQuery.fn.textProcessor.Linker.DetectionRegexWholeString = new RegExp("(http://|https://|ftp://|http://www|https://www|ftp://www|mailto:)");
	jQuery.fn.textProcessor.Linker.DetectionRegex = new RegExp("(http://|https://|ftp://|http://www|https://www|ftp://www|www|mailto:)[a-zA-Z0-9_\\-\\.\\?#*:/=@%&\\+\\[\\]~¬,]+"); // we intentionally don't use the 'g' option for this regex
	jQuery.fn.textProcessor.Linker.BlockedUrlMarkup = '<span class="blocked">\nblocked URL\n</span>';	
	jQuery.fn.textProcessor.Linker.BlockedImageMarkup = '<span class="blocked">\nblocked image\n</span>';	
	jQuery.fn.textProcessor.Linker.KnownImageExtensions = ['jpeg', 'jpg', 'png', 'gif'];
	jQuery.fn.textProcessor.Linker.KnownExecutableExtensions = ['exe', 'js', 'hta', 'vbs', 'bat', 'scr', 'cmd'];
	jQuery.fn.textProcessor.Linker.HilightStartMarkup = '<span class="domain">';
	jQuery.fn.textProcessor.Linker.HilightEndMarkup = '</span>';
	jQuery.fn.textProcessor.Linker.HilightClass = 'hilighted';
	jQuery.fn.textProcessor.Linker.MaxUrlLength = 70;
	jQuery.fn.textProcessor.Linker.MinUrlSuffixLength = 5;
	jQuery.fn.textProcessor.Linker.Ellipsis = '...';
	jQuery.fn.textProcessor.Linker.BlockedDomains = [
		'goatse.cx',
		'209.242.124.241',
		'kilobox.com',
		'danasoft.com',
		'livingroom.org.au',
		'letsgodigital.nl',
		'letsgodigital.org',
		'pma-show.com',
		'ces-show.com',
		'photokina-show.com',
		'pbase.com/copperhill/ccd_cleaning',
		'freepay.com',
		'copperhillimages.com',
		'sigmadslr.com',
		'gottadeal.com',
		'fatawallet.com',
		'tinyurl.com',
		'fourthirdsphoto.com',
		'cameralabs.com',
		'denoiser.shorturl.com',
		'datawind.de',
		'arefuge.com',
		'garylivingston.com',
		'wdpics.com',
		'pentaxforums.com',
		'pentaxworld.com',
		'shashinki.com',
		'photomalaysia.com',
		'memoryking.com',
		'mafiawww.com',
		'e-p1.net'
	];
	jQuery.fn.textProcessor.Linker.OperaContainer = 'operaContainer';
		
	/*******************
	Linker - Static Methods
	*******************/
	
	jQuery.fn.textProcessor.Linker.ProcessText = function (source, settings)
	{
		var output = [];
		
		// prepare email URLs
		jQuery.fn.textProcessor.Linker.EmailDetectionQuickRegex.lastIndex = 0;
		if (jQuery.fn.textProcessor.Linker.EmailDetectionQuickRegex.test(source) == true)
		{
			do 
			{
				slowMatches = source.match(jQuery.fn.textProcessor.Linker.EmailDetectionSlowRegex);
				if (slowMatches != null && slowMatches.length != 0)
				{
					var innerMatches = source.substring(slowMatches.index).match(jQuery.fn.textProcessor.Linker.EmailDetectionInnerRegex);
					var relativeIndex = innerMatches.index;
					source = source.substring(0, slowMatches.index + relativeIndex) + jQuery.fn.textProcessor.Linker.EmailPrefix + source.substring(slowMatches.index + relativeIndex);
				}
			}
			while (slowMatches != null && slowMatches.length != 0)
		}
		
		// prefix www with http
		source = source.replace(new RegExp("( |^|\n)www", "g"), '\nhttp://www');
			
		// check the whole string (no sense processing line by line if it contains no links)
		jQuery.fn.textProcessor.Linker.DetectionRegexWholeString.lastIndex = 0; // reset regex before use
		if (jQuery.fn.textProcessor.Linker.DetectionRegexWholeString.test(source))
		{
			var lines = source.split('\n');
			for (var i=0; i<lines.length; i++)
			{
				var lineBuffer = lines[i];
				
				if (jQuery.fn.textProcessor.IsElementLine(lineBuffer) == false)
				{
				
					jQuery.fn.textProcessor.Linker.DetectionRegexWholeString.lastIndex = 0; // reset regex before use
					if (jQuery.fn.textProcessor.Linker.DetectionRegexWholeString.test(lineBuffer))
					{
						// detect each link in the line
						var workingLineBuffer = lineBuffer;
						lineBuffer = '';
						do
						{
							jQuery.fn.textProcessor.Linker.DetectionRegex.lastIndex = 0;
							var linkMatches = workingLineBuffer.match(jQuery.fn.textProcessor.Linker.DetectionRegex);
							if (linkMatches && linkMatches.length > 0)
							{
								var url = linkMatches[0];
								var lastIndex = linkMatches.index + url.length;
								url = url.replace(new RegExp("^www."), 'http://www.');
								
								// determine what to do with this link
								var newUrl = '';
								var isMarkedNonEmbed = workingLineBuffer.charAt(lastIndex) == ';';
								if (jQuery.fn.textProcessor.Linker.IsExecutableUrl(url) || jQuery.fn.textProcessor.Linker.IsBlockedUrl(url))
								{
									// url is exectuble or from a blocked domain
									newUrl = jQuery.fn.textProcessor.Linker.IsImageUrl(url) ? jQuery.fn.textProcessor.Linker.BlockedImageMarkup : jQuery.fn.textProcessor.Linker.BlockedUrlMarkup;
								}
								else
								{
									var displayUrl = url.replace(new RegExp("&", "g"), '&amp;');
									if (jQuery.fn.textProcessor.Linker.IsImageUrl(url) && isMarkedNonEmbed == false && settings.suppressImages == false)
									{
										// embed the image
										newUrl = '</p>\n<div class="' + jQuery.fn.textProcessor.Linker.OperaContainer + '">\n<img src="' + url + '" class="zoom"/>\n</div>\n<p>';
									}
									else
									{
										// non-image url or non-embed image. simply link to it
										// TODO: (jm, 12/6/09) handle internal vs external
										if (settings.enableDomainHilighting == true)
										{
											displayUrl = jQuery.fn.textProcessor.Linker.GetDisplayUrlMarkup(displayUrl);
										}
										var isHilighted = displayUrl.indexOf(jQuery.fn.textProcessor.Linker.HilightStartMarkup) != -1;
										var isExternal = jQuery.fn.textProcessor.Linker.IsExternalLink(url);
										newUrl = '<a href="' + url + '" ' + (isHilighted == true ? ' class="' + jQuery.fn.textProcessor.Linker.HilightClass + '"' : '') + (isExternal == true ? ' target="_blank" rel="nofollow"' : '') + '>\n' + displayUrl + '\n</a>';
									}
								}
										
								// trim off the semi-colon if necessary
								if (isMarkedNonEmbed == true)
								{
									lastIndex++;
								}
								
								// write link (and anything preceeding it in the working string) to line buffer
								lineBuffer += (lineBuffer.length != 0 ? '\n' : '') + workingLineBuffer.substring(0,linkMatches.index) + (linkMatches.index != 0 ? '\n' : '') + newUrl;
								workingLineBuffer = workingLineBuffer.substring(lastIndex);
							}
							else
							{
								// no more matches on the line, dump the working line and move on
								lineBuffer += (lineBuffer.length != 0 && workingLineBuffer.length != 0 ? '\n' : '') + workingLineBuffer;
								workingLineBuffer = '';
							}
						}
						while (workingLineBuffer);
					}
				}
					
					
				// reset regex
				jQuery.fn.textProcessor.Linker.DetectionRegex.lastIndex = 0;
				
				
				output.push(lineBuffer);
			}
		}
		else
		{
			output.push(source);
		}
		
		// reset regex
		jQuery.fn.textProcessor.Linker.DetectionRegexWholeString.lastIndex = 0;
		
		return output.join('\n');
	}
	
	jQuery.fn.textProcessor.Linker.GetDisplayUrlMarkup = function (url)
	{
		var formattedLinkLabel = url;
		if (formattedLinkLabel.substring(0,7) != 'mailto:')
		{
			var startIndex = formattedLinkLabel.indexOf('//') + 2;
			var endIndex = formattedLinkLabel.substring(startIndex).indexOf('/') + startIndex;
			var scheme = formattedLinkLabel.substring(0, startIndex);
			var domain = endIndex > startIndex ? formattedLinkLabel.substring(startIndex, endIndex) : formattedLinkLabel.substring(startIndex);
			var formattedDomain = jQuery.fn.textProcessor.Linker.HilightStartMarkup + domain + jQuery.fn.textProcessor.Linker.HilightEndMarkup;
			var pathAndQueryString = endIndex > startIndex ? formattedLinkLabel.substring(endIndex) : null;
			if (pathAndQueryString != null)
			{
				var pathAndQueryStringLength = Math.max(jQuery.fn.textProcessor.Linker.MaxUrlLength - (scheme.length + domain.length), Math.min(pathAndQueryString.length, jQuery.fn.textProcessor.Linker.MinUrlSuffixLength));
				if (pathAndQueryString.length > pathAndQueryStringLength)
				{
					pathAndQueryString = '/' + jQuery.fn.textProcessor.Linker.Ellipsis + pathAndQueryString.substring(pathAndQueryString.length - (pathAndQueryStringLength + jQuery.fn.textProcessor.Linker.Ellipsis.length));
				}
			}
			else
			{
				// ensure domain isn't excessively long (it can be if there url is itself url encoded and the domain contains the path too)
				if (domain.length > jQuery.fn.textProcessor.Linker.MaxUrlLength)
				{
					formattedDomain = jQuery.fn.textProcessor.Linker.HilightStartMarkup + jQuery.fn.textProcessor.Linker.Ellipsis + domain.substring(domain.length - (jQuery.fn.textProcessor.Linker.MaxUrlLength + jQuery.fn.textProcessor.Linker.Ellipsis.length)) + jQuery.fn.textProcessor.Linker.HilightEndMarkup;
				}
			}
			formattedLinkLabel = scheme + formattedDomain + (pathAndQueryString == null ? '' : pathAndQueryString);
			
		}
		else
		{
			formattedLinkLabel = formattedLinkLabel.substring(7);
		}
		return formattedLinkLabel;
	}
	
	jQuery.fn.textProcessor.Linker.IsExternalLink = function (url)
	{
		var domain = jQuery.fn.textProcessor.Linker.GetDomain(url);
		domain = domain == null ? null : domain.toLowerCase();
		return domain != null && (domain == 'dpreview.com' || domain == 'rock' || domain.EndsWith('.dpreview.com') || domain.EndsWith('.img-dpreview.com')) == false;
	}
	
	jQuery.fn.textProcessor.Linker.GetDomain = function (url)
	{
		var output = null;
		var startIndex = url.indexOf('//') + 2;
		if (startIndex != -1)
		{
			var endIndex = url.substring(startIndex).indexOf('/') + startIndex;
			if (endIndex > startIndex)
			{
				output = url.substring(startIndex, endIndex);
			}
		}
		return output;
	}
	
	jQuery.fn.textProcessor.Linker.GetExtension = function (url)
	{
		var output = null;
		var extensions = url.match(new RegExp("\\.[a-zA-Z0-9]+(\\?|$|#)"));
		if (extensions && extensions.length > 0)
		{
			output = extensions[0].toLowerCase().replace(new RegExp("[^a-z0-9]", "g"), '');
		}
		return output;
	}
	
	jQuery.fn.textProcessor.Linker.IsImageUrl = function (url)
	{
		var extension = jQuery.fn.textProcessor.Linker.GetExtension(url);
		return jQuery.inArray(extension, jQuery.fn.textProcessor.Linker.KnownImageExtensions) != -1;
	}
	
	jQuery.fn.textProcessor.Linker.IsExecutableUrl = function (url)
	{
		var extension = jQuery.fn.textProcessor.Linker.GetExtension(url);
		return jQuery.inArray(extension, jQuery.fn.textProcessor.Linker.KnownExecutableExtensions) != -1;
	}
	
	jQuery.fn.textProcessor.Linker.IsBlockedUrl = function (url)
	{
		for (var i=0; i<jQuery.fn.textProcessor.Linker.BlockedDomains.length; i++)
		{
			if (url.indexOf(jQuery.fn.textProcessor.Linker.BlockedDomains[i]) != -1) return true;
		}
		return false;
	}
	
	
	/******************************
	Class - SignatureFolder
	******************************/
	jQuery.fn.textProcessor.SignatureFolder = function() 
	{
		
	};
	
	/******************
	SignatureFolder - Constants
	*******************/
	jQuery.fn.textProcessor.SignatureFolder.BeginTag = '<div class="signature">';
	jQuery.fn.textProcessor.SignatureFolder.EndTag = '</div>';
	jQuery.fn.textProcessor.SignatureFolder.SignatureStartLineRegex = new RegExp("^[ \t]*[\\\\/\\-=~*][ \\\\/\\-=~*]+[ \t]*($|\n|\r)");
		
	/*******************
	SignatureFolder - Static Methods
	*******************/
	
	jQuery.fn.textProcessor.SignatureFolder.ProcessDom = function (rootNode, settings)
	{
		jQuery.fn.textProcessor.SignatureFolder.NodeWalker(rootNode, settings);
	}
	
	jQuery.fn.textProcessor.SignatureFolder.NodeWalker = function (node, settings)
	{
		for (var i=0; i<node.children.length; i++)
		{
			var child = node.children[i];
			if (child.IsTextNode())
			{
				if (jQuery.fn.textProcessor.SignatureFolder.SignatureStartLineRegex.test(child.text))
				{
					// we've encountered a signature, wrap all remaining children in a new element
					var newNode = new jQuery.fn.textProcessor.DomNode();
					newNode.tagName = jQuery.fn.textProcessor.GetElementName(jQuery.fn.textProcessor.SignatureFolder.BeginTag);
					newNode.beginTag = jQuery.fn.textProcessor.SignatureFolder.BeginTag;
					newNode.endTag = jQuery.fn.textProcessor.SignatureFolder.EndTag;
					newNode.parent = child.parent.parent;
					newNode.children = node.children.slice(i+1);
					node.children = node.children.slice(0,i);
					// clear any preceeding br elements and end any open paragraphs
					if (child.parent.tagName == 'p')
					{
						// clearn any preceeding br elements
						if (i > 0 && node.children[i-1].tagName == 'br')
						{
							node.children = node.children.slice(0,i-1);
						}
					}
					var indexOfChildsParent = jQuery(child.parent.parent.children).indexOf(child.parent);
					newNode.parent.children.splice(indexOfChildsParent+1,0,newNode);
					newNode.children = newNode.children.concat(newNode.parent.children.slice(indexOfChildsParent+2));
					newNode.parent.children = newNode.parent.children.slice(0,indexOfChildsParent+2);
					jQuery.fn.textProcessor.Paragrapher.EnsureBlockInlineNesting(newNode);
					return true;
				}
			}
			else
			{
				if (jQuery.fn.textProcessor.SignatureFolder.NodeWalker(child, settings) == true)
				{
					return false;
				}
			}
		}
		return false;
	}
	
	
	/******************************
	Class - Replacer
	******************************/
	jQuery.fn.textProcessor.Replacer = function() 
	{
		
	};
	
	/******************
	Replacer - Constants
	*******************/
	jQuery.fn.textProcessor.Replacer.KnownExceptions = [
		{ preRegex: new RegExp("\\*ist", "gi"), post: '&#42;ist' },
		{ preRegex: new RegExp("DA\\*", "gi"), post: 'DA&#42;'},
		{ preRegex: new RegExp("FA\\*", "gi"), post: 'FA&#42;'}
	];
		
	/*******************
	Formatter - Static Methods
	*******************/
	
	jQuery.fn.textProcessor.Replacer.ProcessDom = function (rootNode, settings)
	{
		jQuery.fn.textProcessor.Replacer.NodeWalker(rootNode, settings);
	}
	
	jQuery.fn.textProcessor.Replacer.NodeWalker = function (node, settings)
	{
		var currentFormat = null;
		for (var i=0; i<node.children.length; i++)
		{
			var child = node.children[i];
			if (child.IsTextNode() && jQuery.fn.textProcessor.SignatureFolder.SignatureStartLineRegex.test(child.text) == false && child.parent.tagName != 'a')
			{
				// handle known exceptions
				for (var j=0; j<jQuery.fn.textProcessor.Replacer.KnownExceptions.length; j++)
				{
					jQuery.fn.textProcessor.Replacer.KnownExceptions[j].preRegex.lastIndex = 0;
					child.text = child.text.replace(jQuery.fn.textProcessor.Replacer.KnownExceptions[j].preRegex, jQuery.fn.textProcessor.Replacer.KnownExceptions[j].post);
				}
			}
			else
			{
				jQuery.fn.textProcessor.Replacer.NodeWalker(child, settings);
			}
		}
	}
	
	
	/******************************
	Class - Formatter
	******************************/
	jQuery.fn.textProcessor.Formatter = function() 
	{
		
	};
	
	/******************
	Formatter - Constants
	*******************/
	jQuery.fn.textProcessor.Formatter.FormatStartDetectionRegex = new RegExp("(^|[^a-zA-Z0-9<]| )(_|\\*|~|/)[^\n $]"); // deliberately non-global
	jQuery.fn.textProcessor.Formatter.FormatEndDetectionRegex = new RegExp("[^< ](_|\\*|~|/)([^a-zA-Z0-9]|$|\n)"); // deliberately non-global
	jQuery.fn.textProcessor.Formatter.Formats = [
		{ format: '*', elementName: 'strong', encoded: '&#42;', escapedDetectionRegex: new RegExp("\\\\\\*", "g") },
		{ format: '/', elementName: 'em', encoded: '&#47;', escapedDetectionRegex: new RegExp("\\\\/", "g") },
		{ format: '~', elementName: 'del', encoded: '&#126;', escapedDetectionRegex: new RegExp("\\\\\\~", "g") },
		{ format: '_', elementName: 'ins', encoded: '&#95;', escapedDetectionRegex: new RegExp("\\\\\\_", "g") }
	];
	jQuery.fn.textProcessor.Formatter.FormattingCharacters = new RegExp("[_\\*~/]");
	jQuery.fn.textProcessor.Formatter.NonFormattingCharacters = new RegExp("[^_\\*~/]", "g");
		
	/*******************
	Formatter - Static Methods
	*******************/
	
	jQuery.fn.textProcessor.Formatter.ProcessDom = function (rootNode, settings)
	{
		jQuery.fn.textProcessor.Formatter.NodeWalker(rootNode, settings);
	}
	
	jQuery.fn.textProcessor.Formatter.NodeWalker = function (node, settings)
	{
		var currentFormat = null;
		var childrenChanged = false;
		for (var i=0; i<node.children.length; i++)
		{
			var child = node.children[i];
			var originalChildText = child.text;
			if (child.IsTextNode() && jQuery.fn.textProcessor.SignatureFolder.SignatureStartLineRegex.test(child.text) == false && child.parent.tagName != 'a')
			{
				// encode all escaped formatting characters
				for (var j=0; j<jQuery.fn.textProcessor.Formatter.Formats.length; j++)
				{
					jQuery.fn.textProcessor.Formatter.Formats[j].escapedDetectionRegex.lastIndex = 0;
					child.text = child.text.replace(jQuery.fn.textProcessor.Formatter.Formats[j].escapedDetectionRegex, jQuery.fn.textProcessor.Formatter.Formats[j].encoded);
				}
			
				while (jQuery.fn.textProcessor.Formatter.FormatStartDetectionRegex.test(child.text) || jQuery.fn.textProcessor.Formatter.FormatEndDetectionRegex.test(child.text))
				{
					jQuery.fn.textProcessor.Formatter.FormatStartDetectionRegex.lastIndex = 0;
					jQuery.fn.textProcessor.Formatter.FormatEndDetectionRegex.lastIndex = 0;
					var startMatches = child.text.match(jQuery.fn.textProcessor.Formatter.FormatStartDetectionRegex);
					var endMatches = child.text.match(jQuery.fn.textProcessor.Formatter.FormatEndDetectionRegex);
					var matches = startMatches;
					var isEnd = false;
					if ((matches == null && endMatches != null) || (endMatches != null && endMatches.index < matches.index))
					{
						matches = endMatches;
						isEnd = true;
					}
					if (matches != null && matches.length != 0)
					{
						// get a clean match
						jQuery.fn.textProcessor.Formatter.NonFormattingCharacters.lastIndex = 0;
						jQuery.fn.textProcessor.Formatter.FormattingCharacters.lastIndex = 0;
						var indexOfActualFormattingCharacter = matches[0].match(jQuery.fn.textProcessor.Formatter.FormattingCharacters).index;
						var matchText = matches[0].replace(jQuery.fn.textProcessor.Formatter.NonFormattingCharacters,'').substring(0,1);
						var markupToEmbed = '';
						for (var j=0; j<jQuery.fn.textProcessor.Formatter.Formats.length; j++)
						{
							if (jQuery.fn.textProcessor.Formatter.Formats[j].format == matchText)
							{
								markupToEmbed = jQuery.fn.textProcessor.Formatter.Formats[j].elementName;
								break;
							}
						}
						
						// re-determine whether it's a start or end if it follows a > character
						var prologue = child.text.substring(0, matches.index + indexOfActualFormattingCharacter).replace(new RegExp("\n", "g"), '');
						if (prologue.length != 0 && prologue.charAt(prologue.length-1) == '>')
						{
							var walker = currentFormat;
							isEnd == false;
							while (walker != null)
							{
								if (walker.format == matchText)
								{
									isEnd = true;
									break;
								}
								walker = walker.parent;
							}
						}
						
						// keep current format up to date
						if (isEnd == false && (currentFormat == null || currentFormat.format != matchText))
						{
							// format start tag
							currentFormat = {format: matchText, children: [], parent: currentFormat };
							markupToEmbed = '\n<' + markupToEmbed + '>\n';
						}
						else
						{
							// this must be an end tag, determine if this format was actually started
							var walker = currentFormat;
							var startExists = false;
							while (walker != null)
							{
								if (walker.format == matchText)
								{
									startExists = true;
									break;
								}
								walker = walker.parent;
							}
							
							if (startExists == true)
							{
								// close any incorrectly nested tags
								var overlappingMarkupToEmbed = '';
								while (currentFormat != null && currentFormat.format != matchText)
								{
									for (var j=0; j<jQuery.fn.textProcessor.Formatter.Formats.length; j++)
									{
										if (jQuery.fn.textProcessor.Formatter.Formats[j].format == currentFormat.format)
										{
											overlappingMarkupToEmbed += '\n</' + jQuery.fn.textProcessor.Formatter.Formats[j].elementName + '>\n';
											break;
										}
									}
									
									// this must be an end tag
									currentFormat = currentFormat.parent;
								}
							
								// this is an end tag for a matching start
								currentFormat = currentFormat.parent;
								markupToEmbed = overlappingMarkupToEmbed + '\n</' + markupToEmbed + '>\n';
							}
							else
							{
								// this is a spurious end tag, encode it to prevent it being picked up again
								markupToEmbed = jQuery.grep(jQuery.fn.textProcessor.Formatter.Formats, function (n, i) { return n.format == matchText; })[0].encoded;
							}
						}
						
						child.text = child.text.substring(0, matches.index + indexOfActualFormattingCharacter) + markupToEmbed + child.text.substring(matches.index + indexOfActualFormattingCharacter + matchText.length);
					}
				}
				
				// trim line breaks
				child.text = child.text.replace(new RegExp("(^\n+|\n+$)", "g"), '');
			}
			else
			{
				jQuery.fn.textProcessor.Formatter.NodeWalker(child, settings);
			}
			
			if (childrenChanged == false && child.text != originalChildText)
			{
				childrenChanged = true;
			}
		}
		
		// close any unclosed tags
		while (currentFormat != null)
		{
			var markupToEmbed = '';
			for (var j=0; j<jQuery.fn.textProcessor.Formatter.Formats.length; j++)
			{
				if (jQuery.fn.textProcessor.Formatter.Formats[j].format == currentFormat.format)
				{
					markupToEmbed = jQuery.fn.textProcessor.Formatter.Formats[j].elementName;
					break;
				}
			}
			
			// this must be an end tag
			currentFormat = currentFormat.parent;
			markupToEmbed = '\n</' + markupToEmbed + '>\n';
			if (node.IsTextNode() == true)
			{
				child.text += markupToEmbed;
			}
			else
			{
				var newChild = new jQuery.fn.textProcessor.DomNode();
				newChild.text = markupToEmbed;
				newChild.parent = node;
				node.children.push(newChild);
			}
		}
		
					
		// balancing formatting gets HARD, so we'll just re-DOM this entire portion of the tree (if required)
		if (childrenChanged = true)
		{
			node.children = jQuery.fn.textProcessor.DomNode.BuildTree(node.InnerHtml(false).join('\n').replace(new RegExp("\\n+ \\n+", "g"), "\n&nbsp;\n")).children;
		}
	}
	
	
	/******************************
	Class - DomNode
	******************************/
	jQuery.fn.textProcessor.DomNode = function(rawHtml) 
	{
		
		/******************
		DomNode - Instance Methods
		*******************/
		function IsTextNode ()
		{
			return (this.text != null);
		}
		
		function IsEmptyElement ()
		{
			return (this.beginTag != null && this.endTag == null)
		}
		
		function IsInlineElement ()
		{
			return this.IsTextNode() || this.IsEmptyElement() || jQuery.fn.textProcessor.IsInlineElement(this.tagName);
		}
		
		function IsContainerElement ()
		{
			return (this.IsTextNode() == false && this.IsEmptyElement() == false);
		}
		
		function InnerHtml (forDisplay)
		{
			var output = [];
			for (var i=0; i<this.children.length; i++)
			{
				output = output.concat(this.children[i].OuterHtml(forDisplay));
			}
			if (forDisplay == true && this.IsContainerElement() == true && this.tagName != null)
			{
				if (jQuery.fn.textProcessor.CanContainBlockElements(this.tagName) == false)
				{
					output.splice(0, output.length, output.join(''));
				}
			}
			return output;
		}
		
		function OuterHtml (forDisplay)
		{
			var output = [];
			if (this.IsTextNode())
			{
				output.push(this.text);
			}
			else if (this.IsEmptyElement())
			{
				output.push(this.beginTag);
			}
			else
			{
				// non-empty element
				output.push(this.beginTag);
				output = output.concat(this.InnerHtml(forDisplay));
				output.push(this.endTag);
			}
			return output;
		}
		
		function WrapChildren (startIndex, endIndex, beginTag, endTag)
		{
			var newNode = new jQuery.fn.textProcessor.DomNode();
			newNode.parent = this;
			newNode.beginTag = beginTag;
			newNode.endTag = endTag;
			newNode.tagName = jQuery.fn.textProcessor.GetElementName(beginTag);
			newNode.children = this.children.slice(startIndex, endIndex);
			this.children.splice(startIndex, endIndex-startIndex);
			this.children.splice(startIndex, 0, newNode);
		}
		
		function IsEmptyContainer ()
		{
			if (this.IsEmptyElement() == true)
			{
				// empty elements aren't containers
				return false;
			}
			else if (this.IsTextNode())
			{
				return jQuery.fn.textProcessor.EmptyLineRegex.test(this.text);
			}
			else
			{
				// return true if this node has at least one non-br child
				for (var i=0; i<this.children.length; i++)
				{
					var child = this.children[i];
					if (child.IsTextNode() == true || child.tagName != 'br')
					{
						return false;
					}
				}
				return true;
			}
		}
		
		function PruneEmptyDescendents ()
		{
			for (var i=0; i<this.children.length; i++)
			{
				var child = this.children[i];
				
				// strip any leading br tags
				for (var j=0; j<child.children.length; j++)
				{
					if (child.children[j].tagName != 'br')
					{
						child.children = child.children.slice(j);
						break;
					}
				}
				
				// strip any trailing br tags
				for (var j=child.children.length-1; j>=0; j--)
				{
					if (child.children[j].tagName != 'br')
					{
						child.children = child.children.slice(0,j+1);
						break;
					}
				}
				
				child.PruneEmptyDescendents();
				if (child.IsEmptyContainer())
				{
					this.children.splice(i,1);
					i--;
				}
			}
		}
		
		function AddClass (newClass)
		{
			if (this.IsTextNode() == false && this.beginTag != null)
			{
				var indexOfClassAttribute = this.beginTag.indexOf(jQuery.fn.textProcessor.DomNode.ClassAttribueMarkup);
				if (indexOfClassAttribute == -1)
				{
					this.beginTag = this.beginTag.replace(this.IsEmptyElement() == true ? '/>' : '>', ' class="' + newClass + '">');
				}
				else
				{
					var indexOfClassValue = indexOfClassAttribute + jQuery.fn.textProcessor.DomNode.ClassAttribueMarkup.length;
					var endOfClassAttribute = this.beginTag.substring(indexOfClassValue).indexOf('"') + indexOfClassValue;
					var existingClassValue = this.beginTag.substring(indexOfClassValue).substring(0,endOfClassAttribute-indexOfClassValue);
					var existingClasses = existingClassValue.split(' ');
					if (jQuery(existingClasses).indexOf(newClass) == -1)
					{
						// add the class
						existingClasses.push(newClass);
						this.beginTag = this.beginTag.substring(0, indexOfClassValue) + existingClasses.join(' ') + this.beginTag.substring(endOfClassAttribute);
					}
				}
			}
		}
		
		/******************
		DomNode - Constructor
		******************/
		
		// publicly accessible members
		var wrapper = 
		{
			// fields
			tagName: null,
			beginTag: null,
			endTag: null,
			children: [],
			parent: null,
			text: null,
			// methods
			IsTextNode: IsTextNode,
			IsEmptyElement: IsEmptyElement,
			IsContainerElement: IsContainerElement,
			InnerHtml: InnerHtml,
			OuterHtml: OuterHtml,
			IsInlineElement: IsInlineElement, 
			WrapChildren: WrapChildren,
			IsEmptyContainer: IsEmptyContainer,
			PruneEmptyDescendents: PruneEmptyDescendents,
			AddClass: AddClass
		};
		
		
		return wrapper;
	};
	
	/******************
	DomNode - Constants
	******************/
	jQuery.fn.textProcessor.DomNode.FirstClass = 'first';
	jQuery.fn.textProcessor.DomNode.LastClass = 'last';
	jQuery.fn.textProcessor.DomNode.ClassAttribueMarkup = 'class="';
	
	/******************
	DomNode - Static Methods
	******************/
	
	jQuery.fn.textProcessor.DomNode.BuildTree = function (rawHtml)
	{
		var rootElement = new jQuery.fn.textProcessor.DomNode();
		var lines = rawHtml.replace(new RegExp("<", "g"), '\n<').replace(new RegExp(">", "g"), '>\n').split('\n');
		
		if (lines.length != 0)
		{
			var currentElement = rootElement;
			
			// walk through subsequent lines
			for (var i=0; i<lines.length; i++)
			{
				var line = lines[i];
				
				if (jQuery.fn.textProcessor.IsElementLine(line))
				{
					var elementName = jQuery.fn.textProcessor.GetElementName(line);
					
					if (jQuery.fn.textProcessor.IsBeginTagLine(line))
					{
						var newElement = new jQuery.fn.textProcessor.DomNode();
						newElement.parent = currentElement;
						newElement.tagName = elementName;
						newElement.beginTag = line;
						currentElement.children.push(newElement);
						currentElement = newElement;
					}
					else if (jQuery.fn.textProcessor.IsEndTagLine(line))
					{
						currentElement.endTag = line;
						currentElement = currentElement.parent;
					}		
					else
					{
						// is empty element
						var newElement = new jQuery.fn.textProcessor.DomNode();
						newElement.parent = currentElement;
						newElement.tagName = elementName;
						newElement.beginTag = line;
						currentElement.children.push(newElement);
					}	
				}
				else
				{
					// text node
					if (jQuery.fn.textProcessor.EmptyLineRegex.test(line) == false)
					{
						var textNode = new jQuery.fn.textProcessor.DomNode();
						textNode.text = line;
						textNode.parent = currentElement;
						currentElement.children.push(textNode);
					}
				}
			}
		}
		
		return rootElement;
	}
	
	jQuery.fn.textProcessor.DomNode.FirstAndLastBlock = function (node)
	{
		for (var i=0; i<node.children.length; i++)
		{
			var child = node.children[i];
			if (child.IsTextNode() == false && jQuery(jQuery.fn.textProcessor.KnownBlockElements).indexOf(child.tagName) != -1)
			{
				if (i == 0)
				{
					child.AddClass(jQuery.fn.textProcessor.DomNode.FirstClass);
				}
				if (i == node.children.length-1)
				{
					child.AddClass(jQuery.fn.textProcessor.DomNode.LastClass);
				}
				jQuery.fn.textProcessor.DomNode.FirstAndLastBlock(child);
			}
		}
	}
	
	
	/*****************************
	imageZoomer - Plugin
	*****************************/
	jQuery.fn.imageZoomer = function(settings)
	{
		// merge supplied & default args
		settings = jQuery.extend(jQuery.fn.imageZoomer.DefaultSettings(), settings);

		// The jquery objects that contain our collapsable items.  
		var $images = this;
		var zoomers = [];
		
		$images.each(function ()
		{
			var zoomer = new jQuery.fn.imageZoomer.ZoomableImage(jQuery(this), settings);
			zoomers.push(zoomer);
		});
	
		return this;
	};
	
	/*****************************
	imageZoomer - Plugin constants
	*****************************/
	jQuery.fn.imageZoomer.DefaultClass = 'zoom';
	jQuery.fn.imageZoomer.DefaultSettings = function () { return {
			expandAll: true
		};
	}
	
	/*********************
	Class - ZoomableImage
	*********************/
	jQuery.fn.imageZoomer.ZoomableImage = function ($image, settings)
	{
		/******************
		ZoomableImage - Constants
		*******************/
		jQuery.fn.imageZoomer.ZoomableImage.WrapperName = 'wrapper';
		jQuery.fn.imageZoomer.ZoomableImage.ContainerClass = 'imageZoomer';
		jQuery.fn.imageZoomer.ZoomableImage.ToolbarClass = 'toolbar';
		jQuery.fn.imageZoomer.ZoomableImage.ViewportClass = 'viewport';
		jQuery.fn.imageZoomer.ZoomableImage.ExpandedClass = 'expandedViewport';
		jQuery.fn.imageZoomer.ZoomableImage.TolerableWidth = 560;
		jQuery.fn.imageZoomer.ZoomableImage.TolerableHeight = 560;
		jQuery.fn.imageZoomer.ZoomableImage.MinAcceptableZoomPower = -3;
		jQuery.fn.imageZoomer.ZoomableImage.MaxAcceptableZoomPower = 2;
		jQuery.fn.imageZoomer.ZoomableImage.ExpandLabel = 'Expand';
		jQuery.fn.imageZoomer.ZoomableImage.ContractLabel = 'Contract';
		jQuery.fn.imageZoomer.ZoomableImage.ExpandAllLabel = 'Expand All';
		jQuery.fn.imageZoomer.ZoomableImage.ContractAllLabel = 'Contract All';
		jQuery.fn.imageZoomer.ZoomableImage.ExpandIconPath = 'http://a.img-dpreview.com/images/imgexpand0.gif';
		jQuery.fn.imageZoomer.ZoomableImage.ContractIconPath = 'http://a.img-dpreview.com/images/imgexpand1.gif';
		jQuery.fn.imageZoomer.ZoomableImage.ExpandTooltip = 'click to expand image';
		jQuery.fn.imageZoomer.ZoomableImage.ContractTooltip = 'click to contract image';
		jQuery.fn.imageZoomer.ZoomableImage.ExpandAllTooltip = 'click to expand all images';
		jQuery.fn.imageZoomer.ZoomableImage.ContractAllTooltip = 'click to contract all images';
		
		/******************
		ZoomableImage - Public Instance methods
		*******************/
		function /* ZoomableImage. */ Zoom (powerDelta)
		{
			// update dimensions if not known
			if (this.nativeWidth == 0 || this.nativeHeight == 0)
			{
				this.nativeWidth = this.$originalImage.width();
				this.nativeHeight = this.$originalImage.height();
			}
		
			var proposedPower = this.effectivePower + powerDelta;
			if (proposedPower >= jQuery.fn.imageZoomer.ZoomableImage.MinAcceptableZoomPower && proposedPower <= jQuery.fn.imageZoomer.ZoomableImage.MaxAcceptableZoomPower)
			{
				// proposed zoom is acceptable, change image dimensions
				this.effectivePower = proposedPower;
				var effectiveCoefficient = Math.pow(2,this.effectivePower);
				if (this.nativeWidth == 0)
				{
					this.nativeWidth = this.$originalImage.width();
				}
				if (this.nativeHeight == 0)
				{
					this.nativeHeight = this.$originalImage.height();
				}
				this.$originalImage.width(this.nativeWidth * effectiveCoefficient);
				this.$originalImage.height(this.nativeHeight  * effectiveCoefficient);
			}
			this.SynchronizeZoomImage();
			this.UpdateToolbar();
		}
		
		function /* ZoomableImage. */ Reset ()
		{
			this.effectivePower = 0;
			this.$originalImage.width(this.nativeWidth);
			this.$originalImage.height(this.nativeHeight);
			this.SynchronizeZoomImage();
			this.UpdateToolbar();
		}
		
		function /* ZoomableImage. */ Expand ()
		{
			if (this.$originalImage.width() >= this.$container.width())
			{
				this.$expanded.css({display: 'block'});
				this.$originalImage.get(0).parentNode.style.visibility = 'hidden';
				this.isExpanded = true;
				this.SynchronizeZoomImage();
				this.UpdateToolbar();
			}
		}
		
		function /* ZoomableImage. */ ExpandAll ()
		{
			jQuery('.' + jQuery.fn.imageZoomer.ZoomableImage.ContainerClass).each(function ()
			{
				var theWrapper = jQuery(this).data(jQuery.fn.imageZoomer.ZoomableImage.WrapperName);
				theWrapper.Expand();
			});
		}
		
		function /* ZoomableImage. */ Contract ()
		{
			this.$expanded.css({display:'none'});
			this.$originalImage.get(0).parentNode.style.visibility = 'visible';
			this.isExpanded = false;
			this.SynchronizeZoomImage();
			this.UpdateToolbar();
		}
		
		function /* ZoomableImage. */ ContractAll ()
		{
			jQuery('.' + jQuery.fn.imageZoomer.ZoomableImage.ContainerClass).each(function ()
			{
				var theWrapper = jQuery(this).data(jQuery.fn.imageZoomer.ZoomableImage.WrapperName);
				theWrapper.Contract();
			});
		}
		
		function /* ZoomableImage */ SynchronizeZoomImage ()
		{
			var imagePosition = this.$originalImage.position();
			var containerPosition = this.$container.position();
			
			var newLeft = 0;
			this.$expandedImage.get(0).width = this.$originalImage.get(0).width;
			this.$expandedImage.get(0).height = this.$originalImage.get(0).height;
			if (this.$originalImage.width() >= this.$container.width())
			{
				newLeft = Math.max((this.$container.width()/2) - (this.$originalImage.width()/2), (containerPosition.left * -1) + 10);
			}
			//alert(newLeft);
			this.$expanded.css({left: newLeft + 'px', top: imagePosition.top + 'px'});
			if (this.$originalImage.width() < this.$container.width() && this.isExpanded == true)
			{
				this.Contract();
			}
		}
		
		function /* ZoomableImage */ UpdateToolbar ()
		{
			// enable / disable 'zoom in' button
			this.$zoomIn.attr('disabled', this.effectivePower == jQuery.fn.imageZoomer.ZoomableImage.MaxAcceptableZoomPower ? 'disabled' : '');
			// enable / disable 'zoom 100%' button
			this.$zoomFull.attr('disabled', this.effectivePower == 0 ? 'disabled' : '');
			// enable / disable 'zoom out' button
			this.$zoomOut.attr('disabled', this.effectivePower == jQuery.fn.imageZoomer.ZoomableImage.MinAcceptableZoomPower ? 'disabled' : '');
			// 'expand' or 'contract' label
			this.$toggleExpansion.text(this.isExpanded == true ? 
				(this.expandAll == true ? jQuery.fn.imageZoomer.ZoomableImage.ContractAllLabel : jQuery.fn.imageZoomer.ZoomableImage.ContractLabel) 
				: 
				(this.expandAll == true ? jQuery.fn.imageZoomer.ZoomableImage.ExpandAllLabel : jQuery.fn.imageZoomer.ZoomableImage.ExpandLabel)
				);
		}
		
		/******************
		ZoomableImage - Public Members
		*******************/
		var wrapper = 
		{
			// fields
			$originalImage: $image,
			$container: null,
			isExpanded: false,
			nativeWidth: 0,
			nativeHeight: 0,
			effectivePower: 0,
			$expanded: null,
			$expandedImage: null,
			$expandIcon: null,
			$contractIcon: null,
			$imageViewPort: null,
			$zoomIn: null,
			$zoomOut: null,
			$zoomFull: null,
			$toggleExpansion: null,
			expandAll: settings.expandAll == true,
			// methods
			Zoom: Zoom,
			Reset: Reset,
			Expand: Expand,
			ExpandAll: ExpandAll,
			Contract: Contract,
			ContractAll: ContractAll,
			SynchronizeZoomImage: SynchronizeZoomImage,
			UpdateToolbar: UpdateToolbar
		};
		
		/******************
		ZoomableImage - Constructor logic
		*******************/
		
		// determine whether to enable expanding/contracting
		var imageWidth = $image.width();
		var imageHeight = $image.height();
		var imageDimensionsKnown = 
			imageWidth != 0 && 
			imageWidth != 24 && 
			imageHeight != 0 && 
			imageHeight != 24 && 
			(jQuery.browser.msie == false || (imageWidth != 28 || imageHeight != 30));
		var expandable = true;
		if (!jQuery.browser.opera && imageDimensionsKnown == true && imageWidth <= jQuery.fn.imageZoomer.ZoomableImage.TolerableWidth && imageHeight <= jQuery.fn.imageZoomer.ZoomableImage.TolerableHeight)
		{
			// image dimensions are known and are within tolerable values, therefore we'll hide the toolbar
			expandable = false;
		}
		
		if (expandable == true)
		{
			// container (contains toolbar and image)
			wrapper.$container = jQuery('<div></div>').insertBefore($image).addClass(jQuery.fn.imageZoomer.ZoomableImage.ContainerClass);
			wrapper.$container.data(jQuery.fn.imageZoomer.ZoomableImage.WrapperName, wrapper);
			
			// image viewport
			wrapper.$imageViewPort = jQuery('<div title="' + ((wrapper.expandAll == true) ? jQuery.fn.imageZoomer.ZoomableImage.ExpandAllTooltip : jQuery.fn.imageZoomer.ZoomableImage.ExpandTooltip) + '"></div>')
				.appendTo(wrapper.$container)
				.addClass(jQuery.fn.imageZoomer.ZoomableImage.ViewportClass);
			wrapper.$originalImage.appendTo(wrapper.$imageViewPort)
				.data(jQuery.fn.imageZoomer.ZoomableImage.WrapperName, wrapper)
				.click(settings.expandAll == true ?
					function (e)
					{
						var theWrapper = jQuery(this).data(jQuery.fn.imageZoomer.ZoomableImage.WrapperName);
						theWrapper.ExpandAll();
					}
					:
					function (e)
					{
						var theWrapper = jQuery(this).data(jQuery.fn.imageZoomer.ZoomableImage.WrapperName);
						theWrapper.ExpandAll();
					});
					
			// remove 'overflow: auto' property from image container (opera specific) to allow expand to work
			wrapper.$imageViewPort.parents('.' + jQuery.fn.textProcessor.Linker.OperaContainer).css({overflow: 'visible'});
			
			// 'expand' icon
			wrapper.$expandIcon = jQuery('<img src="' + jQuery.fn.imageZoomer.ZoomableImage.ExpandIconPath + '" class="expand-icon" />')
				.appendTo(wrapper.$imageViewPort)
				.css({visibility: 'hidden'});
			// hover behaviour (show/hide 'expand' icon)
			wrapper.$originalImage.hover(function ()
			{
				wrapper.$expandIcon.css({visibility: 'visible'});
			},
			function ()
			{
				wrapper.$expandIcon.css({visibility: 'hidden'});
			});
			
			// expanded viewport
			wrapper.$expanded = jQuery('<div title="' + ((wrapper.expandAll == true) ? jQuery.fn.imageZoomer.ZoomableImage.ContractAllTooltip : jQuery.fn.imageZoomer.ZoomableImage.ContractTooltip) + '"></div>')
				.appendTo(wrapper.$container)
				.addClass(jQuery.fn.imageZoomer.ZoomableImage.ExpandedClass)
				.css({display:'none'});
			wrapper.$expandedImage = jQuery('<img src="' + $image.get(0).src + '"/>')
				.appendTo(wrapper.$expanded)
				.data(jQuery.fn.imageZoomer.ZoomableImage.WrapperName, wrapper)
				.click(settings.expandAll == true ?
					function (e)
					{
						var theWrapper = jQuery(this).data(jQuery.fn.imageZoomer.ZoomableImage.WrapperName);
						theWrapper.ContractAll();
					}
					:
					function (e)
					{
						var theWrapper = jQuery(this).data(jQuery.fn.imageZoomer.ZoomableImage.WrapperName);
						theWrapper.Contract();
					});
			// 'contract' icon
			wrapper.$contractIcon = jQuery('<img src="' + jQuery.fn.imageZoomer.ZoomableImage.ContractIconPath + '" class="contract-icon" />')
				.appendTo(wrapper.$expanded)
				.css({visibility: 'hidden'});
			// hover behaviour (show/hide 'contract' icon)
			wrapper.$expandedImage.hover(function ()
			{
				wrapper.$contractIcon.css({visibility: 'visible'});
			},
			function ()
			{
				wrapper.$contractIcon.css({visibility: 'hidden'});
			});
			
			// toolbar
			var $toolbar = jQuery('<div></div>')
				.insertBefore(wrapper.$imageViewPort)
				.addClass(jQuery.fn.imageZoomer.ZoomableImage.ToolbarClass)
				.append('<span class="prefix">Image control:</span>');
		
			// 'zoom out' button
			wrapper.$zoomOut = jQuery('<a class="button" href="#">Zoom out</a>')
				.appendTo($toolbar)
				.data(jQuery.fn.imageZoomer.ZoomableImage.WrapperName, wrapper)
				.click(function (e)
				{
					e.preventDefault();	
					e.cancelBubble = true;
					this.blur();
					var theWrapper = jQuery(this).data(jQuery.fn.imageZoomer.ZoomableImage.WrapperName);
					theWrapper.Zoom(-1);
				});
			
			// 'zoom 100%' button
			wrapper.$zoomFull = jQuery('<a class="button" href="#">Zoom 100%</a>')
				.appendTo($toolbar)
				.data(jQuery.fn.imageZoomer.ZoomableImage.WrapperName, wrapper)
				.click(function (e)
				{
					e.preventDefault();	
					e.cancelBubble = true;
					this.blur();
					var theWrapper = jQuery(this).data(jQuery.fn.imageZoomer.ZoomableImage.WrapperName);
					theWrapper.Reset();
				});
			
			// 'zoom in' button
			wrapper.$zoomIn = jQuery('<a class="button" href="#">Zoom in</a>')
				.appendTo($toolbar)
				.data(jQuery.fn.imageZoomer.ZoomableImage.WrapperName, wrapper)
				.click(function (e)
				{
					e.preventDefault();	
					e.cancelBubble = true;
					this.blur();
					var theWrapper = jQuery(this).data(jQuery.fn.imageZoomer.ZoomableImage.WrapperName);
					theWrapper.Zoom(1);
				});
			
			// 'expand/contract' button
			wrapper.$toggleExpansion = jQuery('<a class="button" href="#">' + jQuery.fn.imageZoomer.ZoomableImage.ExpandLabel + '</a>')
				.appendTo($toolbar)
				.data(jQuery.fn.imageZoomer.ZoomableImage.WrapperName, wrapper) 
				.click(function (e)
				{
					e.preventDefault();
					e.cancelBubble = true;
					this.blur();
					var theWrapper = jQuery(this).data(jQuery.fn.imageZoomer.ZoomableImage.WrapperName);
					if (theWrapper.isExpanded == true)
					{
						if (theWrapper.expandAll == true)
						{
							theWrapper.ContractAll();
						}
						else
						{
							theWrapper.Contract();
						}
					}
					else
					{
						if (theWrapper.expandAll == true)
						{
							theWrapper.ExpandAll();
						}
						else
						{
							theWrapper.Expand();
						}
					}	
				});
			
			// 'new window' button
			jQuery('<a class="button" href="' + $image.get(0).src + '" target="_blank">Open in new window</a>')
				.appendTo($toolbar);
		
			// update titles of all toolbar buttons
			wrapper.UpdateToolbar();
		}
		else
		{
			//$image.removeClass(jQuery.fn.imageZoomer.DefaultClass);
		}
		
		return wrapper;
	};
	
	
	
	/*****************************
	signatureFolder - Plugin
	*****************************/
	
	jQuery.fn.signatureFolder = function(settings)
	{
		// merge supplied & default args
		settings = jQuery.extend(jQuery.fn.signatureFolder.DefaultSettings(), settings);

		// The jquery objects that contain our collapsable items.  
		var $images = this;
		var zoomers = [];
		
		$images.each(function ()
		{
			var zoomer = new jQuery.fn.signatureFolder.Signature(jQuery(this), settings);
			zoomers.push(zoomer);
		});
	
		return this;
	};
	
	/*****************************
	Plugin constants
	*****************************/
	jQuery.fn.signatureFolder.DefaultClass = 'signature';
	jQuery.fn.signatureFolder.WrapperName = 'wrapper';
	jQuery.fn.signatureFolder.DefaultSettings = function () { return { initiallyHidden: (jQuery.cookie(jQuery.fn.signatureFolder.CookieName) == null || jQuery.cookie(jQuery.fn.signatureFolder.CookieName) == jQuery.fn.signatureFolder.CookieValueHide) }; };
	jQuery.fn.signatureFolder.CookieName = 'labs_sig';
	jQuery.fn.signatureFolder.CookieValueShow = 'show';
	jQuery.fn.signatureFolder.CookieValueHide = 'hide';
	
	/*********************
	Class - Signature
	*********************/
	jQuery.fn.signatureFolder.Signature = function ($signatureElement, settings)
	{
		/******************
		Signature - Constants
		*******************/
		jQuery.fn.signatureFolder.Signature.ContainerClass = "signature-folder";
		jQuery.fn.signatureFolder.Signature.ShowLabel = "-- show signature --";
		jQuery.fn.signatureFolder.Signature.HideLabel = "-- hide signature --";
		jQuery.fn.signatureFolder.Signature.SignatureClass = "signature";
		jQuery.fn.signatureFolder.Signature.TriggerClass = "trigger";
		jQuery.fn.signatureFolder.Signature.ShowTooltip = "click to show signature";
		jQuery.fn.signatureFolder.Signature.HideTooltip = "click to show signature";
		
		/******************
		Signature - Public Instance methods
		*******************/
		
		function /* Signature. */ Toggle ()
		{
			if (this.isExpanded == false)
			{
				this.Show();
			}
			else
			{
				this.Hide();
			}
		}
		
		function /* Signature. */ Show ()
		{
			this.$originalElement.show();
			this.isExpanded = true;
			this.$trigger.attr('title', jQuery.fn.signatureFolder.Signature.ShowTooltip);
			this.Update();
		}
		
		function /* Signature. */ Hide ()
		{
			this.$originalElement.hide();
			this.isExpanded = false;
			this.$trigger.attr('title', jQuery.fn.signatureFolder.Signature.HideTooltip);
			this.Update();
		}
		
		function /* Signature. */ Update ()
		{
			this.$trigger.text(this.isExpanded == true ? jQuery.fn.signatureFolder.Signature.HideLabel : jQuery.fn.signatureFolder.Signature.ShowLabel);
		}
		
		function /* Signature. */ SavePreferences ()
		{
			jQuery.cookie(jQuery.fn.signatureFolder.CookieName, this.isExpanded == true ? jQuery.fn.signatureFolder.CookieValueShow : jQuery.fn.signatureFolder.CookieValueHide, { expires: 60, path: '/'});
		}
		
		/******************
		Signature - Public Members
		*******************/
		var wrapper = 
		{
			// fields
			$originalElement: $signatureElement,
			$container: null,
			$trigger: null,
			isExpanded: false,
			// methods
			Show: Show,
			Hide: Hide,
			Toggle: Toggle,
			Update: Update,
			SavePreferences: SavePreferences
		};
		
		/******************
		Signature - Constructor logic
		*******************/
		
		// container (contains toolbar and image)
		wrapper.$container = jQuery('<div></div>')
			.insertBefore($signatureElement)
			.addClass(jQuery.fn.signatureFolder.Signature.ContainerClass)
			.data(jQuery.fn.signatureFolder.WrapperName, wrapper);
		
		// signature
		wrapper.$originalElement = $signatureElement.appendTo(wrapper.$container).attr('class', '').addClass(jQuery.fn.signatureFolder.Signature.SignatureClass);
		
		// trigger
		wrapper.$trigger = jQuery('<a href="#">' + jQuery.fn.signatureFolder.Signature.ShowLabel + '</a>')
			.insertBefore(wrapper.$originalElement)
			.addClass(jQuery.fn.signatureFolder.Signature.TriggerClass)
			.data(jQuery.fn.signatureFolder.WrapperName, wrapper)
			.click(function (e)
			{
				e.cancelBubble = true;
				e.preventDefault();
				var theWrapper = jQuery(this).data(jQuery.fn.signatureFolder.WrapperName);
				theWrapper.Toggle();
				this.blur();
				theWrapper.SavePreferences();
			});
			
		if (settings.initiallyHidden == true)
		{
			wrapper.Hide();
		}
		else
		{
			wrapper.Show();
		}
		
		return wrapper;
	};
	
	
})(jQuery);


String.prototype.LTrim = function() 
{
	return this.replace(new RegExp("^\\s+"),"");
}

jQuery.fn.indexOf = function(e)
{
	/// <summary>
	/// jquery extension method to determine the position of the supplied item in the callee's associative array
	/// </summary>
	
	var output = -1;
	for (var i=0; i<this.length; i++)
	{
		if (this[i] == e) 
		{
			output = i;
			break;
		}
	}
	return output;
};


jQuery.fn.Any = function(a,f) 
{
	if (a != null)
    {
        for (var i=0; i<a.length; i++)
        {
            if (f(a[i],i))
			{
				return true;
            }
        }
    }
	return false;
};

String.prototype.EndsWith = function String$endsWith(suffix) 
{
    return (this.substr(this.length - suffix.length) === suffix);
}

String.prototype.StartsWith = function String$startsWith(prefix) 
{
    return (this.substr(0, prefix.length) === prefix);
}