/// /// A basic implementation of a pretty-printer or syntax highlighter for C# soure code. /// public class SourceColorer { private string _commentCssClass; private string _keywordCssClass; private string _quotesCssClass; private string _typeCssClass; private bool _addStyleDefinition; private HashSet _keywords; private bool _addPreTags; /// /// Gets the list of reserved words/keywords. /// public HashSet Keywords { get { return _keywords; } } /// /// Gets or sets the CSS class used for comments. The default is 'comment'. /// public string CommentCssClass { get { return _commentCssClass; } set { _commentCssClass = value; } } /// /// Gets or sets the CSS class used for keywords. The default is 'keyword'. /// public string KeywordCssClass { get { return _keywordCssClass; } set { _keywordCssClass = value; } } /// /// Gets or sets the CSS class used for string quotes. The default is 'quotes'. /// public string QuotesCssClass { get { return _quotesCssClass; } set { _quotesCssClass = value; } } /// /// Gets or sets the CSS class used for types. The default is 'type'. /// public string TypeCssClass { get { return _typeCssClass; } set { _typeCssClass = value; } } /// /// Whether to add the CSS style definition to the top of the highlighted code. /// public bool AddStyleDefinition { get { return _addStyleDefinition; } set { _addStyleDefinition = value; } } /// /// Whether to insert opening and closing pre tags around the highlighted code. /// public bool AddPreTags { get { return _addPreTags; } set { _addPreTags = value; } } /// /// Initializes a new instance of the class. /// public SourceColorer() { _addStyleDefinition = true; _commentCssClass = "comment"; _keywordCssClass = "keyword"; _quotesCssClass = "quotes"; _typeCssClass = "type"; _keywords = new HashSet() { "static", "using", "true", "false","new", "namespace", "void", "private", "public", "bool", "string", "return", "class","internal", "const", "readonly", "int", "double","lock", "float", "if", "else", "foreach", "for","var", "get","set","byte\\[\\]","char\\[\\]","int\\[\\]","string\\[\\]" // dumb array matching. Escaped as [] is regex syntax }; } /// /// Highlights the specified source code and returns it as stylised HTML. /// /// The source code. /// public string Highlight(string source) { StringBuilder builder = new StringBuilder(); if (AddStyleDefinition) { builder.Append(""); } if (AddPreTags) builder.Append("
");
			
		builder.Append(HighlightSource(source));
		
		if (AddPreTags)
			builder.Append("
"); return builder.ToString(); } /// /// Occurs when the source code is highlighted, after any style (CSS) definitions are added. /// /// The source code content. /// The highlighted source code. protected virtual string HighlightSource(string content) { if (string.IsNullOrEmpty(CommentCssClass)) throw new InvalidOperationException("The CommentCssClass should not be null or empty"); if (string.IsNullOrEmpty(KeywordCssClass)) throw new InvalidOperationException("The KeywordCssClass should not be null or empty"); if (string.IsNullOrEmpty(QuotesCssClass)) throw new InvalidOperationException("The CommentCssClass should not be null or empty"); if (string.IsNullOrEmpty(TypeCssClass)) throw new InvalidOperationException("The TypeCssClass should not be null or empty"); // Some fairly secure token placeholders const string COMMENTS_TOKEN = "`````"; const string MULTILINECOMMENTS_TOKEN = "~~~~~"; const string QUOTES_TOKEN = "¬¬¬¬¬"; // Remove /* */ quotes, taken from ostermiller.org Regex regex = new Regex(@"/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+/", RegexOptions.Singleline); List multiLineComments = new List(); if (regex.IsMatch(content)) { foreach (Match item in regex.Matches(content)) { if (!multiLineComments.Contains(item.Value)) multiLineComments.Add(item.Value); } } for (int i = 0; i < multiLineComments.Count; i++) { content = content.ReplaceToken(multiLineComments[i], MULTILINECOMMENTS_TOKEN, i); } // Remove the quotes first, so they don't get highlighted List quotes = new List(); bool onEscape = false; bool onComment1 = false; bool onComment2 = false; bool inQuotes = false; int start = -1; for (int i = 0; i < content.Length; i++) { if (content[i] == '/' && !inQuotes && !onComment1) onComment1 = true; else if (content[i] == '/' && !inQuotes && onComment1) onComment2 = true; else if (content[i] == '"' && !onEscape && !onComment2) { inQuotes = true; // stops cases of: var s = "// I'm a comment"; if (start > -1) { string quote = content.Substring(start, i - start + 1); if (!quotes.Contains(quote)) quotes.Add(quote); start = -1; inQuotes = false; } else { start = i; } } else if (content[i] == '\\' || content[i] == '\'') onEscape = true; else if (content[i] == '\n') { onComment1 = false; onComment2 = false; } else { onEscape = false; } } for (int i = 0; i < quotes.Count; i++) { content = content.ReplaceToken(quotes[i], QUOTES_TOKEN, i); } // Remove the comments next, so they don't get highlighted regex = new Regex("(/{2,3}.+)\n", RegexOptions.Multiline); List comments = new List(); if (regex.IsMatch(content)) { foreach (Match item in regex.Matches(content)) { if (!comments.Contains(item.Value + "\n")) comments.Add(item.Value); } } for (int i = 0; i < comments.Count; i++) { content = content.ReplaceToken(comments[i], COMMENTS_TOKEN, i); } // Highlight single quotes content = Regex.Replace(content, "('.{1,2}')", "$1", RegexOptions.Singleline); // Highlight class names based on the logic: {space OR start of line OR >}{1 capital){alphanumeric}{space} regex = new Regex(@"((?:\s|^)[A-Z]\w+(?:\s))", RegexOptions.Singleline); List highlightedClasses = new List(); if (regex.IsMatch(content)) { foreach (Match item in regex.Matches(content)) { string val = item.Groups[1].Value; if (!highlightedClasses.Contains(val)) highlightedClasses.Add(val); } } for (int i = 0; i < highlightedClasses.Count; i++) { content = content.ReplaceWithCss(highlightedClasses[i], TypeCssClass); } // Pass 2. Doing it in N passes due to my inferior regex knowledge of back/forwardtracking. // This does {space or [}{1 capital){alphanumeric}{]} regex = new Regex(@"(?:\s|\[)([A-Z]\w+)(?:\])", RegexOptions.Singleline); highlightedClasses = new List(); if (regex.IsMatch(content)) { foreach (Match item in regex.Matches(content)) { string val = item.Groups[1].Value; if (!highlightedClasses.Contains(val)) highlightedClasses.Add(val); } } for (int i = 0; i < highlightedClasses.Count; i++) { content = content.ReplaceWithCss(highlightedClasses[i], TypeCssClass); } // Pass 3. Generics regex = new Regex(@"(?:\s|\[|\()([A-Z]\w+(?:<|<))", RegexOptions.Singleline); highlightedClasses = new List(); if (regex.IsMatch(content)) { foreach (Match item in regex.Matches(content)) { string val = item.Groups[1].Value; if (!highlightedClasses.Contains(val)) highlightedClasses.Add(val); } } for (int i = 0; i < highlightedClasses.Count; i++) { string val = highlightedClasses[i]; val = val.Replace("<", "").Replace("<", ""); content = content.ReplaceWithCss(highlightedClasses[i], val, "<", TypeCssClass); } // Pass 4. new keyword with a type regex = new Regex(@"new\s+([A-Z]\w+)(?:\()", RegexOptions.Singleline); highlightedClasses = new List(); if (regex.IsMatch(content)) { foreach (Match item in regex.Matches(content)) { string val = item.Groups[1].Value; if (!highlightedClasses.Contains(val)) highlightedClasses.Add(val); } } // Replace the highlighted classes for (int i = 0; i < highlightedClasses.Count; i++) { content = content.ReplaceWithCss(highlightedClasses[i], TypeCssClass); } // Highlight keywords foreach (string keyword in _keywords) { Regex regexKeyword = new Regex("(" + keyword + @")(>|>|\s|\n|;|<)", RegexOptions.Singleline); content = regexKeyword.Replace(content, "$1$2"); } // Shove the multiline comments back in for (int i = 0; i < multiLineComments.Count; i++) { content = content.ReplaceTokenWithCss(multiLineComments[i], MULTILINECOMMENTS_TOKEN, i, CommentCssClass); } // Shove the quotes back in for (int i = 0; i < quotes.Count; i++) { content = content.ReplaceTokenWithCss(quotes[i], QUOTES_TOKEN, i, QuotesCssClass); } // Shove the single line comments back in for (int i = 0; i < comments.Count; i++) { string comment = comments[i]; // Add quotes back in for (int n = 0; n < quotes.Count; n++) { comment = comment.Replace(string.Format("{0}{1}{0}", QUOTES_TOKEN, n), quotes[n]); } content = content.ReplaceTokenWithCss(comment, COMMENTS_TOKEN, i, CommentCssClass); } return content; } } public static class MoreExtensions { public static string ReplaceWithCss(this string content, string source, string cssClass) { return content.Replace(source, string.Format("{1}", cssClass, source)); } public static string ReplaceWithCss(this string content, string source, string replacement, string cssClass) { return content.Replace(source, string.Format("{1}", cssClass, replacement)); } public static string ReplaceWithCss(this string content, string source, string replacement, string suffix, string cssClass) { return content.Replace(source, string.Format("{1}{2}", cssClass, replacement, suffix)); } public static string ReplaceTokenWithCss(this string content, string source, string token, int counter, string cssClass) { string formattedToken = String.Format("{0}{1}{0}", token, counter); return content.Replace(formattedToken, string.Format("{1}", cssClass, source)); } public static string ReplaceToken(this string content, string source, string token, int counter) { string formattedToken = String.Format("{0}{1}{0}", token, counter); return content.Replace(source, formattedToken); } }