/// <summary> /// Converts a stack trace to formatted HTML with styling and linkifiation. /// </summary> /// <param name="stackTrace">The stack trace to HTMLify.</param> /// <param name="settings">The <see cref="StackTraceSettings"/> to use in this render.</param> /// <returns>An HTML-pretty version of the stack trace.</returns> public static string HtmlPrettify(string stackTrace, StackTraceSettings settings) { string GetBetween(Capture prev, Capture next) => stackTrace.Substring(prev.Index + prev.Length, next.Index - (prev.Index + prev.Length)); int pos = 0; var sb = StringBuilderCache.Get(); var matches = _regex.Matches(stackTrace); for (var mi = 0; mi < matches.Count; mi++) { Match m = matches[mi]; Group leadIn = m.Groups[Groups.LeadIn], frame = m.Groups[Groups.Frame], type = m.Groups[Groups.Type], asyncMethod = m.Groups[Groups.AsyncMethod], method = m.Groups[Groups.Method], allParams = m.Groups[Groups.Params], sourceInfo = m.Groups[Groups.SourceInfo], path = m.Groups[Groups.Path], linePrefix = m.Groups[Groups.LinePrefix], line = m.Groups[Groups.Line]; CaptureCollection paramTypes = m.Groups[Groups.ParamType].Captures, paramNames = m.Groups[Groups.ParamName].Captures; bool nextIsAsync = false; if (mi < matches.Count - 1) { Group nextFrame = matches[mi + 1].Groups[Groups.Frame]; nextIsAsync = _asyncFrames.Contains(nextFrame.Value); } var isAsync = _asyncFrames.Contains(frame.Value); // The initial message may be above an async frame if (sb.Length == 0 && isAsync && leadIn.Index > pos) { sb.Append("<span class=\"stack stack-row\">") .Append("<span class=\"stack misc\">") .AppendHtmlEncode(stackTrace.Substring(pos, leadIn.Index - pos).Trim(NewLine_CarriageReturn)) .Append("</span>") .Append("</span>"); pos += sb.Length; } sb.Append(isAsync ? "<span class=\"stack stack-row async\">" : "<span class=\"stack stack-row\">"); if (leadIn.Index > pos) { var miscContent = stackTrace.Substring(pos, leadIn.Index - pos); if (miscContent.Contains(EndStack)) { // Handle end-of-stack removals and redundant multilines remaining miscContent = miscContent.Replace(EndStack, "") .Replace("\r\n\r\n", "\r\n") .Replace("\n\n", "\n\n"); } sb.Append("<span class=\"stack misc\">") .AppendHtmlEncode(miscContent) .Append("</span>"); } sb.Append("<span class=\"stack leadin\">") .AppendHtmlEncode(leadIn.Value) .Append("</span>"); // Check if the next line is the end of an async hand-off var nextEndStack = stackTrace.IndexOf(EndStack, m.Index + m.Length); if ((nextEndStack > -1 && nextEndStack < m.Index + m.Length + 3) || (!isAsync && nextIsAsync)) { sb.Append("<span class=\"stack async-tag\">async</span> "); } if (asyncMethod.Success) { sb.Append("<span class=\"stack type\">") .AppendGenerics(GetBetween(leadIn, asyncMethod), settings) .Append("</span>") .Append("<span class=\"stack method\">") .AppendHtmlEncode(asyncMethod.Value) .Append("</span>") .Append("<span class=\"stack type\">") .AppendGenerics(GetBetween(asyncMethod, method), settings); sb.Append("</span>"); } else { sb.Append("<span class=\"stack type\">") .AppendGenerics(type.Value, settings) .Append("<span class=\"stack dot\">") .AppendHtmlEncode(GetBetween(type, method)) // "." .Append("</span>") .Append("</span>"); } sb.Append("<span class=\"stack method-section\">") .Append("<span class=\"stack method\">") .AppendHtmlEncode(NormalizeMethodName(method.Value)) .Append("</span>"); if (paramTypes.Count > 0) { sb.Append("<span class=\"stack parens\">") .Append(GetBetween(method, paramTypes[0])) .Append("</span>"); for (var i = 0; i < paramTypes.Count; i++) { if (i > 0) { sb.Append("<span class=\"stack misc\">") .AppendHtmlEncode(GetBetween(paramNames[i - 1], paramTypes[i])) // ", " .Append("</span>"); } sb.Append("<span class=\"stack paramType\">") .AppendGenerics(paramTypes[i].Value, settings) .Append("</span>") .AppendHtmlEncode(GetBetween(paramTypes[i], paramNames[i])) // " " .Append("<span class=\"stack paramName\">") .AppendHtmlEncode(paramNames[i].Value) .Append("</span>"); } var last = paramNames[paramTypes.Count - 1]; sb.Append("<span class=\"stack parens\">") .AppendHtmlEncode(allParams.Value.Substring(last.Index + last.Length - allParams.Index)) .Append("</span>"); } else { sb.Append("<span class=\"stack parens\">") .AppendHtmlEncode(allParams.Value) // "()" .Append("</span>"); } sb.Append("</span>"); // method-section for table layout if (sourceInfo.Value.HasValue()) { sb.Append("<span class=\"stack source-section\">"); var curPath = sourceInfo.Value; if (settings.LinkReplacements.Count > 0) { foreach (var replacement in settings.LinkReplacements) { curPath = replacement.Key.Replace(curPath, replacement.Value); } } if (curPath != sourceInfo.Value) { sb.Append("<span class=\"stack misc\">") .AppendHtmlEncode(GetBetween(allParams, sourceInfo)) .Append("</span>") .Append(curPath); } else if (path.Value.HasValue()) { var subPath = GetSubPath(path.Value, type.Value); sb.Append("<span class=\"stack misc\">") .AppendHtmlEncode(GetBetween(allParams, path)) .Append("</span>") .Append("<span class=\"stack path\">") .AppendHtmlEncode(subPath) .Append("</span>") .AppendHtmlEncode(GetBetween(path, linePrefix)) .Append("<span class=\"stack line-prefix\">") .AppendHtmlEncode(linePrefix.Value) .Append("</span>") .Append("<span class=\"stack line\">") .AppendHtmlEncode(line.Value) .Append("</span>"); } sb.Append("</span>"); } sb.Append("</span>"); pos = frame.Index + frame.Length; } // append anything left sb.Append("<span class=\"stack misc\">"); LinkifyRest(sb, stackTrace, pos, settings.LinkReplacements); sb.Append("</span>"); return(sb.ToStringRecycle()); }
public static StringBuilder AppendGenerics(this StringBuilder sb, string typeOrMethod, StackTraceSettings settings) { const string _dotSpan = "<span class=\"stack dot\">.</span>"; if (!settings.EnablePrettyGenerics) { return(sb.AppendHtmlEncode(typeOrMethod)); } // Check the common framework list above _commonGenerics.TryGetValue(typeOrMethod, out string[] args); // Break each type down by namespace and class (remember, we *could* have nested generic classes) var classes = typeOrMethod.Split(_dot); // Loop through each dot component of the type, e.g. "System", "Collections", "Generics" for (var i = 0; i < classes.Length; i++) { if (i > 0) { sb.Append(_dotSpan); } var match = _genericTypeRegex.Match(classes[i]); if (match.Success) { // If arguments aren't known, get the defaults if (args == null && int.TryParse(match.Groups["ArgCount"].Value, out int count)) { if (count == 1) { args = _singleT; } else { args = new string[count]; for (var j = 0; j < count; j++) { args[j] = "T" + (j + 1).ToString(); // <T>, or <T1, T2, T3> } } } // In the known case, BaseClass is "System.Collections.Generic.Dictionary" // In the unknown case, we're hitting here at "Class" only sb.AppendHtmlEncode(match.Groups["BaseClass"].Value); AppendArgs(args); } else { sb.AppendHtmlEncode(classes[i]); } } return(sb); void AppendArgs(string[] tArgs) { switch (settings.Language) { case StackTraceSettings.CodeLanguage.VB: sb.Append("(Of "); break; case StackTraceSettings.CodeLanguage.CSharp: case StackTraceSettings.CodeLanguage.FSharp: sb.Append("<"); break; } // Don't put crazy amounts of arguments in here if (tArgs.Length > 5) { sb.Append("<span class=\"stack generic-type\">").Append(tArgs[0]).Append("</span>") .Append(",") .Append("<span class=\"stack generic-type\">").Append(tArgs[1]).Append("</span>") .Append(",") .Append("<span class=\"stack generic-type\">").Append(tArgs[2]).Append("</span>") .Append("…") .Append("<span class=\"stack generic-type\">").Append(tArgs[tArgs.Length - 1]).Append("</span>"); } else { for (int i = 0; i < tArgs.Length; i++) { if (i > 0) { sb.Append(","); } if (settings.IncludeGenericTypeNames) { sb.Append("<span class=\"stack generic-type\">"); if (settings.Language == StackTraceSettings.CodeLanguage.FSharp) { sb.Append("'"); } sb.Append(tArgs[i]) .Append("</span>"); } } } switch (settings.Language) { case StackTraceSettings.CodeLanguage.VB: sb.Append(")"); break; case StackTraceSettings.CodeLanguage.CSharp: case StackTraceSettings.CodeLanguage.FSharp: sb.Append(">"); break; } } }
/// <summary> /// Converts a stack trace to formatted HTML with styling and linkifiation. /// </summary> /// <param name="stackTrace">The stack trace to HTMLify.</param> /// <param name="settings">The settings to use when prettifying this stack trace.</param> /// <returns>An HTML-pretty version of the stack trace.</returns> public static string HtmlPrettify(string stackTrace, StackTraceSettings settings) { string GetBetween(Capture prev, Capture next) => stackTrace.Substring(prev.Index + prev.Length, next.Index - (prev.Index + prev.Length)); int pos = 0; var sb = StringBuilderCache.Get(); foreach (Match m in _regex.Matches(stackTrace)) { Group leadIn = m.Groups[Groups.LeadIn], frame = m.Groups[Groups.Frame], type = m.Groups[Groups.Type], asyncMethod = m.Groups[Groups.AsyncMethod], method = m.Groups[Groups.Method], allParams = m.Groups[Groups.Params], path = m.Groups[Groups.Path], // TODO: URLs linePrefix = m.Groups[Groups.LinePrefix], line = m.Groups[Groups.Line]; CaptureCollection paramTypes = m.Groups[Groups.ParamType].Captures, paramNames = m.Groups[Groups.ParamName].Captures; var isAsync = _asyncFrames.Contains(frame.Value); sb.Append(isAsync ? "<span class=\"stack row async\">" : "<span class=\"stack row\">"); sb.Append("<span class=\"stack misc\">") .AppendHtmlEncode(stackTrace.Substring(pos, leadIn.Index - pos)) .Append("</span>") .Append("<span class=\"stack leadin\">") .AppendHtmlEncode(leadIn.Value) .Append("</span>"); // Check if the next line is the end of an async hand-off var nextEndStack = stackTrace.IndexOf(EndStack, m.Index + m.Length); if (nextEndStack > -1 && nextEndStack < m.Index + m.Length + 3) { sb.Append("<span class=\"stack async-tag\">async</span> "); } if (asyncMethod.Success) { sb.Append("<span class=\"stack type\">") .AppendGenerics(GetBetween(leadIn, asyncMethod), settings) .Append("</span>") .Append("<span class=\"stack method\">") .AppendHtmlEncode(asyncMethod.Value) .Append("</span>") .Append("<span class=\"stack type\">") .AppendGenerics(GetBetween(asyncMethod, method), settings); sb.Append("</span>"); } else { sb.Append("<span class=\"stack type\">") .AppendGenerics(type.Value, settings) .Append("<span class=\"stack dot\">") .AppendHtmlEncode(GetBetween(type, method)) // "." .Append("</span>") .Append("</span>"); } sb.Append("<span class=\"stack method-section\">") .Append("<span class=\"stack method\">") .AppendHtmlEncode(method.Value) .Append("</span>"); if (paramTypes.Count > 0) { sb.Append("<span class=\"stack parens\">") .Append(GetBetween(method, paramTypes[0])) .Append("</span>"); for (var i = 0; i < paramTypes.Count; i++) { if (i > 0) { sb.Append("<span class=\"stack misc\">") .AppendHtmlEncode(GetBetween(paramNames[i - 1], paramTypes[i])) // ", " .Append("</span>"); } sb.Append("<span class=\"stack paramType\">") .AppendGenerics(paramTypes[i].Value, settings) .Append("</span>") .AppendHtmlEncode(GetBetween(paramTypes[i], paramNames[i])) // " " .Append("<span class=\"stack paramName\">") .AppendHtmlEncode(paramNames[i].Value) .Append("</span>"); } var last = paramNames[paramTypes.Count - 1]; sb.Append("<span class=\"stack parens\">") .AppendHtmlEncode(allParams.Value.Substring(last.Index + last.Length - allParams.Index)) .Append("</span>"); } else { sb.Append("<span class=\"stack parens\">") .AppendHtmlEncode(allParams.Value) // "()" .Append("</span>"); } sb.Append("</span>"); // method-section for table layout // TODO: regular expression replacement for SourceLink if (path.Value.HasValue()) { var subPath = GetSubPath(path.Value, type.Value); sb.Append("<span class=\"stack source-section\">") .Append("<span class=\"stack misc\">") .AppendHtmlEncode(GetBetween(allParams, path)) .Append("</span>") .Append("<span class=\"stack path\">") .AppendHtmlEncode(subPath) .Append("</span>") .AppendHtmlEncode(GetBetween(path, linePrefix)) .Append("<span class=\"stack line-prefix\">") .AppendHtmlEncode(linePrefix.Value) .Append("</span>") .Append("<span class=\"stack line\">") .AppendHtmlEncode(line.Value) .Append("</span>") .Append("</span>"); } sb.Append("</span>"); pos = frame.Index + frame.Length; } // append anything left sb.Append("<span class=\"stack misc\">") .AppendHtmlEncode(stackTrace.Substring(pos)) .Append("</span>"); return(sb.ToStringRecycle()); }