()
{
"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);
}
}