/* Name: CSharpCodeHighlighter
// Version: 0.9
// Date: 2009-01-19
// Author: Steven van Deursen ->  steven a t cuttingedge dot it
// License: GPL 2.0
	
	Version 0.9 2010-03-05
		Added #region and #endregion keywords.

	Version 0.5 2009-10-25
		Added the C# 3.0 'var' keyword, a list with standard .NET types (mostly types in the System root namespace), and differentiated between
		reference types and value types.

	Version 0.4 2007-02-03
		Added missing get and set keywords in C# 1.2 keywords list.

		Version 0.3 2006-11-13
		Added support for coloring xml tags in xml comments.
		

Usuage:
	var codeHighlighter = new CSharpCodeHighlighter();
	codeHighlighter.ProcessTags('[name of tags to parse]', '[language to parse]', '[attribute name containing the custom types]', '[attribute name containing the custom VALUE types]');
	
Example:
	(HTML code)
	<pre class="cs" language="csharp" customtypes="MyControl Control INamingContainer HtmlTextWriter", customValueTypes="">
		public class MyControl : Control, INamingContainer
		{
			[...]
			
			protected override void Render(HtmlTextWriter writer)
			{
				base.Render(writer);
			}			
		}
	</pre>
	
	(JSCode)
	// Creating a new CSharpCodeHighlighter
	var codeHighlighter = new CSharpCodeHighlighter();
	
	// Declare as custom regex replacement
	codeHighlighter.AddRegExReplacement(/\[\.\.\.\]/g,	"<img src='/codecollapsed.gif' alt='[collapsed code]' border='0' />");
	
	// Start highlighting
	codeHighlighter.ProcessTags('pre', 'csharp', 'customTypes', 'customValueTypes');
*/


function CSharpCodeHighlighter() {
	var NormalCharacterDictionary = new Array();
	var KeywordsDictionary = new Array();
	var DefaultTypeDictionary = new Array();
	var DefaultValueTypeDictionary = new Array();
	var MultiCharSymbols = ['///', '//', '/*', '*/']; 

	function AddWordsToDictionary(words, dictionary)
	{
		for (i = 0; i < words.length; i++)
			dictionary[words[i]] = true;	
	}
	
	function Initialize()
	{
		var normalCharacters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789';
		var i;
		for (i = 0; i < normalCharacters.length; i++)
			NormalCharacterDictionary[normalCharacters.charAt(i)] = true;
		
		// C# 1.2 keywords
		var keywords12 = [		
			 'abstract'	,'as'			,'base'			,'bool'			,'break'
			,'byte'		,'case'			,'catch'		,'char'			,'checked'
			,'class'	,'const'		,'continue'		,'decimal'		,'default'
			,'delegate'	,'do'			,'double'		,'else'			,'enum'
			,'event'	,'explicit'		,'extern'		,'false'		,'finally'
			,'fixed'	,'float'		,'for'			,'foreach'		,'get'
			,'goto'		,'if'			,'implicit'		,'in'			,'int'
			,'interface','internal'		,'is'			,'lock'			,'long'
			,'namespace','new'			,'null'			,'object'		,'operator'
			,'out'		,'override'		,'params'		,'private'		,'protected'
			,'public'	,'readonly'		,'ref'			,'return'		,'sbyte'
			,'sealed'	,'set'			,'short'		,'sizeof'		,'stackalloc'
			,'static'	,'string'		,'struct'		,'switch'		,'this'
			,'throw'	,'true'			,'try'			,'typeof'		,'uint'
			,'ulong'	,'unchecked'	,'unsafe'		,'ushort'		,'using'
			,'virtual'	,'void'			,'volatile'		,'while'		,'endregion'
			,'region'
		];
		
		// C# 2.0 keywords
		var keywords20 = [
			'yield', 'where', 'partial' // bugfix (2009-04-07): partial keyword was missing.
		];
		
		// C# 3.0 keywords
		var keywords30 = [
			'var', 'select', 'from', 'where', 'orderby', 'group', 'by', 'let', 'descending'
		];
		
		//	var DoubleOperators = [
		//		'++',	'--',	'&&',	'||',	'<<',	'>>',
		//		'==',	'!=',	'<=',	'>=',	'+=',	'-=',	'*=',	'/=',	'%=',	'&=',
		//		'|=',	'^=',	'<<=',	'>>=',	'->'
		//	];		
		
		AddWordsToDictionary(keywords12, KeywordsDictionary);
		AddWordsToDictionary(keywords20, KeywordsDictionary);
		AddWordsToDictionary(keywords30, KeywordsDictionary);
		
		/* use this code to get a list of the types:
		            var types =
		                (from assembly in AppDomain.CurrentDomain.GetAssemblies()
		                 from type in assembly.GetTypes()
		                 where type.FullName.StartsWith("System.")
		                 where !type.FullName.Contains("+")
		                 where type.IsPublic
		                 where !type.Name.StartsWith("_")
		                 where ((IEnumerable<char>)type.FullName).Where(c => c == '.').Count() == 1
		                 where !type.IsValueType
		                 orderby type.Name
		                 select (type.Name.Contains("`") ? type.Name.Substring(0, type.Name.IndexOf("`")) : type.Name))
		                 .Distinct()
		                 .ToArray();

		            var attributes =
		                (from type in types
		                 where type.EndsWith("Attribute") && type != "Attribute"
		                select type.Substring(0, type.Length - "Attribute".Length)).ToArray();

		            types = (from type in types.Concat(attributes) orderby type select type).Distinct().ToArray();

		            string systemTypes = "'" + String.Join("', '", types) + "'";
		*/		
		
		// .NET 3.5 System types.
		var systemTypes = [	'AccessViolationException', 'Action', 'ActivationContext', 'Activator', 'AppDomain', 'AppDomainInitializer', 'AppDomainManager', 'AppDomainSetup', 'AppDomainUnloadedException', 'ApplicationException', 'ApplicationId', 'ApplicationIdentity', 'ArgumentException', 'ArgumentNullException', 'ArgumentOutOfRangeException', 'ArithmeticException', 'Array', 'ArrayTypeMismatchException', 'AssemblyLoadEventArgs', 'AssemblyLoadEventHandler', 'AsyncCallback', 'Attribute', 'AttributeUsage', 'AttributeUsageAttribute', 'BadImageFormatException', 'BitConverter', 'Buffer', 'CannotUnloadAppDomainException', 'CharEnumerator', 'CLSCompliant', 'CLSCompliantAttribute', 'Comparison', 'Console', 'ConsoleCancelEventArgs', 'ConsoleCancelEventHandler', 'ContextBoundObject', 'ContextMarshalException', 'ContextStatic', 'ContextStaticAttribute', 'Convert', 'Converter', 'CrossAppDomainDelegate', 'DataMisalignedException', 'DBNull', 'Delegate', 'DivideByZeroException', 'DllNotFoundException', 'DuplicateWaitObjectException', 'EntryPointNotFoundException', 'Enum', 'Environment', 'EventArgs', 'EventHandler', 'Exception', 'ExecutionEngineException', 'FieldAccessException', 'FileStyleUriParser', 'Flags', 'FlagsAttribute', 'FormatException', 'FtpStyleUriParser', 'Func', 'GC', 'GenericUriParser', 'GopherStyleUriParser', 'HttpStyleUriParser', 'IAppDomainSetup', 'IAsyncResult', 'ICloneable', 'IComparable', 'IConvertible', 'ICustomFormatter', 'IDisposable', 'IEquatable', 'IFormatProvider', 'IFormattable', 'IndexOutOfRangeException', 'InsufficientMemoryException', 'InvalidCastException', 'InvalidOperationException', 'InvalidProgramException', 'InvalidTimeZoneException', 'IServiceProvider', 'LdapStyleUriParser', 'LoaderOptimization', 'LoaderOptimizationAttribute', 'LocalDataStoreSlot', 'MarshalByRefObject', 'Math', 'MemberAccessException', 'MethodAccessException', 'MissingFieldException', 'MissingMemberException', 'MissingMethodException', 'MTAThread', 'MTAThreadAttribute', 'MulticastDelegate', 'MulticastNotSupportedException', 'NetPipeStyleUriParser', 'NetTcpStyleUriParser', 'NewsStyleUriParser', 'NonSerialized', 'NonSerializedAttribute', 'NotFiniteNumberException', 'NotImplementedException', 'NotSupportedException', 'Nullable', 'NullReferenceException', 'Object', 'ObjectDisposedException', 'Obsolete', 'ObsoleteAttribute', 'OperatingSystem', 'OperationCanceledException', 'OutOfMemoryException', 'OverflowException', 'ParamArray', 'ParamArrayAttribute', 'PlatformNotSupportedException', 'Predicate', 'Random', 'RankException', 'ResolveEventArgs', 'ResolveEventHandler', 'Serializable', 'SerializableAttribute', 'StackOverflowException', 'STAThread', 'STAThreadAttribute', 'String', 'StringComparer', 'SystemException', 'ThreadStatic', 'ThreadStaticAttribute', 'TimeoutException', 'TimeZone', 'TimeZoneInfo', 'TimeZoneNotFoundException', 'Type', 'TypeInitializationException', 'TypeLoadException', 'TypeUnloadedException', 'UnauthorizedAccessException', 'UnhandledExceptionEventArgs', 'UnhandledExceptionEventHandler', 'Uri', 'UriBuilder', 'UriFormatException', 'UriParser', 'UriTypeConverter', 'ValueType', 'Version', 'WeakReference' ];
		var systemCollectionTypes = [ 'ArrayList', 'BitArray', 'CaseInsensitiveComparer', 'CaseInsensitiveHashCodeProvider', 'Collection', 'CollectionBase', 'CollectionsUtil', 'Comparer', 'Dictionary', 'DictionaryBase', 'EqualityComparer', 'HashSet', 'Hashtable', 'HybridDictionary', 'ICollection', 'IComparer', 'IDictionary', 'IDictionaryEnumerator', 'IEnumerable', 'IEnumerator', 'IEqualityComparer', 'IHashCodeProvider', 'IList', 'IOrderedDictionary', 'KeyedCollection', 'KeyNotFoundException', 'LinkedList', 'LinkedListNode', 'List', 'ListDictionary', 'NameObjectCollectionBase', 'NameValueCollection', 'OrderedDictionary', 'Queue', 'ReadOnlyCollection', 'ReadOnlyCollectionBase', 'SortedDictionary', 'SortedList', 'Stack', 'StringCollection', 'StringDictionary', 'StringEnumerator' ];
		var systemValueTypes = [ 'AppDomainManagerInitializationOptions', 'ArgIterator', 'ArraySegment', 'AttributeTargets', 'Base64FormattingOptions', 'Boolean', 'Byte', 'Char', 'ConsoleColor', 'ConsoleKey', 'ConsoleKeyInfo', 'ConsoleModifiers', 'ConsoleSpecialKey', 'DateTime', 'DateTimeKind', 'DateTimeOffset', 'DayOfWeek', 'Decimal', 'Double', 'EnvironmentVariableTarget', 'GCCollectionMode', 'GCNotificationStatus', 'GenericUriParserOptions', 'Guid', 'Int16', 'Int32', 'Int64', 'IntPtr', 'LoaderOptimization', 'MidpointRounding', 'ModuleHandle', 'Nullable', 'PlatformID', 'RuntimeArgumentHandle', 'RuntimeFieldHandle', 'RuntimeMethodHandle', 'RuntimeTypeHandle', 'SByte', 'StringComparison', 'StringSplitOptions', 'TimeSpan', 'TypeCode', 'TypedReference', 'UInt16', 'UInt32', 'UInt64', 'UIntPtr', 'Void' ];
		var systemLinqTypes = [ 'IGrouping', 'ILookup', 'IQueryable', 'IQueryProvider', 'Lookup', 'ParallelEnumerable', 'ParallelQuery', 'Queryable' ];
		
		AddWordsToDictionary(systemTypes, DefaultTypeDictionary);
		AddWordsToDictionary(systemCollectionTypes, DefaultTypeDictionary);
		AddWordsToDictionary(systemLinqTypes, DefaultTypeDictionary);
		AddWordsToDictionary(systemValueTypes, DefaultValueTypeDictionary);
	}

	Initialize();
	
	function IsSymbol(c)
	{
		return !(typeof(NormalCharacterDictionary[c]) != "undefined");
	}
	
	function IsKeyword(str)
	{
		return typeof(KeywordsDictionary[str]) != "undefined";
	}
	
	function CreateToken(value, isSymbol, isFinalized)
	{
		this.IsSymbol = isSymbol;
		this.IsFinalized = isFinalized;
		this.Value = value;
		this.IsComment = false;
		this.IsStringLiteral = false;
		this.ClassName = '';
	}


	function GetTokenList(code)
	{
		var tokens = new Array();
		
		for (var i = 0; i < code.length; i++)
		{
			var ch = code.charAt(i);
			
			var isSymbol = IsSymbol(ch);
			
			if (tokens.length == 0 || tokens[tokens.length-1].IsSymbol != isSymbol)
			{
				// add new token
				tokens[tokens.length] = new CreateToken(ch, isSymbol, false);
			}
			else
			{
				var token = tokens[tokens.length-1];
				
				if (isSymbol == true)
				{
					if (token.IsFinalized == true || (ch != '/' && ch != '*'))
					{
						// The token is finalized or the current char is no / and *
						
						// That new symbol-token is finalized when it doesn't contain a / or *
						// because / and * are the only symbols that consist of multiple characters
						var isFinalized = (ch == '/' || ch == '*') ? false : true;
						
						tokens[tokens.length] = new CreateToken(ch, isSymbol, isFinalized);
					}
					else
					{
						// The last token hasn't been finalized and the ch is either / or *
					
						var value = token.Value;
						
						if ((value == '/' && ch == '*') || (value == '*' && ch == '/'))
						{
							// finalize the token (no more characters can be added).
							token.IsFinalized = true;
						}
						
						token.Value += '' + ch;
					}
				}
				else
				{
					token.Value += '' + ch;
				}
			}
		}
		
		return tokens;
	}
		
	function IsValueInArray(value, arr)
	{
		if (arr == null)
			return false;
			
		for (var b = 0; b < arr.length; b++)
		{
			if (arr[b] == value)
				return true;
		}
		return false;
	}
	
	// Marks all tokens starting at index as Comment till a new line is found.
	// returns the index of the new line or eof character
	function MarkAndMergeXmlCommentTokens(tokens, index, breakTokens)
	{
		var baseToken = null;
	
		for (var i = index; i < tokens.length; i++)
		{
			var token = tokens[i];
			
			if (token != null)
			{
				var value = tokens[i].Value;
				
				if (baseToken == null)
				{
					// Create the token at the current index as base token
					baseToken = token;
					baseToken.IsComment = true;
					baseToken.IsSymbol = false;
					baseToken.ClassName = value == '<' ? 'xmlcomment' : 'comment';
					
					// When the value is a break token, we are done
					if (IsValueInArray(value, breakTokens) == true)
						return i + 1;
						
					// We stored the current token, so continue with the next token
					continue;
				}
			
				if (value == '<' && baseToken.ClassName == 'comment')
				{
					// The current token is the new baseToken with xmlcomment classname.
					baseToken = token;
					baseToken.IsComment = true;
					baseToken.IsSymbol = false;
					baseToken.ClassName = 'xmlcomment';
				}
				else if (value == '>' && baseToken.ClassName == 'xmlcomment')
				{
					// This is the last token of the current xmlcomment, so we reset the current baseToken.
					// So that the next token will be the new baseToken.
					baseToken = null;
				}
				else
				{		
					baseToken.Value += '' + value;
					
					tokens[i] = null;
					
					// Check if a given break token is in the value of the current token
					// and return if this is the case
					if (IsValueInArray(value, breakTokens) == true)
						return i + 1;			
				}
			}	
	
		}
	
	
		for (var i = index + 1; i < tokens.length; i++)
		{

		}
		return tokens.length;
	}
	
	// Marks all tokens starting at index as Comment till a new line is found.
	// returns the index of the new line or eof character
	function MarkAndMergeCommentTokens(tokens, index, breakTokens)
	{	
		var baseToken = tokens[index];
		baseToken.IsComment = true;
		baseToken.IsSymbol = false;
		baseToken.ClassName = 'comment';
		
		for (var i = index + 1; i < tokens.length; i++)
		{
			var token = tokens[i];
			
			if (token != null)
			{
				var value = tokens[i].Value;
				
				baseToken.Value += '' + value;
				
				tokens[i] = null;
				
				// Check if a given break token is in the value of the current token
				// and return if this is the case
				for (var b = 0; b < breakTokens.length; b++)
				{
					if (breakTokens[b] == value)
						return i + 1;
				}
			}
		}
		return tokens.length;	
	}
	
	// Marks all tokens starting at index as String Literal till a endOfLiteralCharacter or optionaly an end of line character.
	// returns the index of the new line or eof character.
	function MarkAndMergeStringLiteralTokens(tokens, index, endOfLiteralCharacter, escapeCharacter, allowMultiline)
	{
		var baseToken = tokens[index];
		baseToken.IsStringLiteral = true;
		baseToken.IsSymbol = false;
		baseToken.ClassName = 'string';
	
		for (var i = index + 1; i < tokens.length; i++)
		{
			var token = tokens[i];
			
			if (token == null)
				continue;
			
			var value = tokens[i].Value;
			
			baseToken.Value += '' + value;
			
			tokens[i] = null;

			if (endOfLiteralCharacter != escapeCharacter)
			{
				// use look behind
				baseValue = baseToken.Value;
				if (value == endOfLiteralCharacter && 
					(baseValue.length > 1 && baseValue.charAt(baseValue.length-2) != escapeCharacter))
					return i + 1;
			}
			else
			{
				// use look ahead
				if (value == endOfLiteralCharacter)
				{
					if ((i + 1) >= tokens.length || tokens[i+1] == null || tokens[i+1].Value != endOfLiteralCharacter)
						return i + 1;
				
					// skip the next character, we've already seen it
					baseToken.Value += '' + tokens[i+1].Value;
					tokens[i+1] = null;
					i++;
				}
			}
	
			if (allowMultiline == false && (value == '\n' || value == '\r'))
				return i + 1;
		}
		return tokens.length;		
	}
			
	function CreateDictionary(arrayOfStrings)
	{
		var obj = new Object();
		
		for (var i = 0; i < arrayOfStrings.length; i++)
			obj[arrayOfStrings[i]] = true;
			
		return obj;
	}
	
		
	function WrapWithSpan(text, className)
	{
		text = text.replace(/</gi, '&lt;');
		text = text.replace(/>/gi, '&gt;');
		return className == '' ? text : '<span class="' + className + '">' + text + '</span>';
	}
	
	var RegExprDictionary = new Array();
	
	// PUBLIC METHOD
	this.AddRegExReplacement = function(regExpr, replacement)
	{
		var obj = new Object();
		obj.RegExpr = regExpr;
		obj.Replacement = replacement;
	
		RegExprDictionary[RegExprDictionary.length] = obj;
	}	
	
	// PUBLIC METHOD
	this.Parse = function(code, typeNames, valueTypeNames) {
		// Replace break tags with returns
		code = code.replace(/\<br[\s]*[\/]*\>/gi, '\n');
		code = code.replace(/&lt;/gi, '<');
		code = code.replace(/&gt;/gi, '>');
		
		var tokens = GetTokenList(code);
	
		var singleLineBreakTokens = ['\n'];
		var multiLineBreakTokens = ['*/'];
		
		// Find Single line /// XML Comments, merge all comment tokens and set the ClassName
		for (var i = 0; i < tokens.length; i++)
		{
			var token = tokens[i];
			if (token != null && token.IsComment == false && token.Value == '///')
			{
				token.IsComment = true;
				token.ClassName = 'xmlcomment';
				i = MarkAndMergeXmlCommentTokens(tokens, i + 1, singleLineBreakTokens);
			}
		}
		
		// Find Single line // comments, merge all comment tokens and set the ClassName
		for (var i = 0; i < tokens.length; i++)
		{
			var token = tokens[i];
			if (token != null && token.IsComment == false && token.Value.indexOf('//') != -1)
			{
				token.ClassName = 'comment';
				i = MarkAndMergeCommentTokens(tokens, i, singleLineBreakTokens);
			}
		}
		
		// Find Multiline /* */ comments, merge all comment tokens and set the ClassName
		for (var i = 0; i < tokens.length; i++)
		{
			var token = tokens[i];
			if (token != null && token.IsComment == false && token.Value == '/*')
			{
				token.ClassName = 'comment';
				i = MarkAndMergeCommentTokens(tokens, i, multiLineBreakTokens);
			}			
		}
		
		// Find strings literals
		for (var i = 0; i < tokens.length; i++)
		{
			var token = tokens[i];
			if (token != null && token.IsStringLiteral == false && token.IsComment == false)
			{
				var value = token.Value;
				
				if (value == "'")
				{
					// Process a single quoted string literal
					i = MarkAndMergeStringLiteralTokens(tokens, i, '\'', '\\', false);
				}
				else if (value == '@')
				{
					if (tokens[i+1] != null && tokens[i+1].Value == '"')
					{
						// Process a multiline string literal
						i = MarkAndMergeStringLiteralTokens(tokens, i + 1, '"', '"', true);
					}
				}
				else if (value == '"')
				{
					// Process a double quoted string literal
					i = MarkAndMergeStringLiteralTokens(tokens, i, '"', '\\', false);
				}
			}			
		}		

		// Set ClassNames for keyword and type 
	
		var typeNameDictionary = new CreateDictionary(typeNames);
		var valueTypeNameDictionary = new CreateDictionary(valueTypeNames);
		for (var i = 0; i < tokens.length; i++)
		{
			var token = tokens[i];
			
			if (token != null && token.IsComment == false && token.IsSymbol == false)
			{
				if (IsKeyword(token.Value))
				{
					previousToken = i == 0 ? null : tokens[i-1];
					previousValue = previousToken == null ? '' : previousToken.Value;
					
					// Check for the @ sign. Keywords can be escaped with 
					if (previousValue.length == 0 || previousValue[previousValue.length-1] != '@')
						token.ClassName = 'keyword';
				}
				else if (typeNameDictionary[token.Value] == true || DefaultTypeDictionary[token.Value] == true)
				{
					// token is a type
					token.ClassName = "type";
				}
				else if (valueTypeNameDictionary[token.Value] == true || DefaultValueTypeDictionary[token.Value] == true)
				{
					// token is a value type
					token.ClassName = "valuetype";
				}
			}
		}
		
		var highlightedCode = '';
		
		// Find Keywords
		for (var i = 0; i < tokens.length; i++)
		{
			var token = tokens[i];
			
			if (token != null)
			{
				highlightedCode += WrapWithSpan(token.Value, token.ClassName);
			
				var isSymbol = token.IsSymbol ? 'true' : 'false';
			}
		}
		
		// Extra replacements using custom regular expressions
		for (var i = 0; i < RegExprDictionary.length; i++)
		{
			var expr = RegExprDictionary[i];
			
			highlightedCode = highlightedCode.replace(expr.RegExpr, expr.Replacement);
		}
		
		return highlightedCode;
	}
	
	// PUBLIC METHOD
	this.ProcessTags = function(tagName, language, customTypeNameAttribute, customValueTypeNameAttribute)
	{
		var preTags = document.getElementsByTagName(tagName);
		
		for (var i = 0; i < preTags.length; i++)
		{
			var pre = preTags[i];
			
			if (pre.getAttribute('language') == language)
			{
				var typeNames = pre.getAttribute(customTypeNameAttribute)+''!='' ? (pre.getAttribute(customTypeNameAttribute) + '').split(' ') : [''];
				
				var valueTypeNames = pre.getAttribute(customValueTypeNameAttribute)+''!='' ? (pre.getAttribute(customValueTypeNameAttribute) + '').split(' ') : [''];
				
				var innerHtml = this.Parse(pre.innerHTML, typeNames, valueTypeNames);

				// Workaround for IE <pre> innerHTML normalization quirk
				if ('outerHTML' in pre) {
					if (document.URL.indexOf('debug') == -1)
						innerHtml = innerHtml.replace(/\n/g,'<br />');
						
					pre.outerHTML = pre.outerHTML.substring(0,pre.outerHTML.indexOf('>')+1) + innerHtml + '<' + '/' + pre.tagName.toLowerCase() + '>';
				} else {
					pre.innerHTML = innerHtml;
				}
			}
		}	
	}
}
