Jo Shields a575963da9 Imported Upstream version 3.6.0
Former-commit-id: da6be194a6b1221998fc28233f2503bd61dd9d14
2014-08-13 10:39:27 +01:00

414 lines
13 KiB
C#

// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Web.Helpers.Resources;
namespace System.Web.Helpers
{
internal class HtmlObjectPrinter : ObjectVisitor
{
private const string Styles =
@"<style type=""text/css"">
.objectinfo { font-size: 13px; }
.objectinfo .type { color: #0000ff; }
.objectinfo .complexType { color: #2b91af; }
.objectinfo .name { color: Black; }
.objectinfo .value { color: Black; }
.objectinfo .quote { color: Brown; }
.objectinfo .null { color: Red; }
.objectinfo .exception { color:Red; }
.objectinfo .typeContainer { border-left: solid 2px #7C888A; padding-left: 3px; margin-left:3px; }
.objectinfo h3, h2 { margin:0; padding:0; }
.objectinfo ul { margin-top:0; margin-bottom:0; list-style-type:none; padding-left:10px; margin-left:10px; }
</style>
";
private static readonly HtmlElement _nullSpan = HtmlElement.CreateSpan("(null)", "null");
// List of chars to escape within strings
private static readonly Dictionary<char, string> _printableEscapeChars = new Dictionary<char, string>
{
{ '\0', "\\0" },
{ '\\', "\\\\" },
{ '\'', "'" },
{ '\"', "\\\"" },
{ '\a', "\\a" },
{ '\b', "\\b" },
{ '\f', "\\f" },
{ '\n', "\\n" },
{ '\r', "\\r" },
{ '\t', "\\t" },
{ '\v', "\\v" },
};
// We want to exclude the type name next to the value for members
private bool _excludeTypeName;
private Stack<HtmlElement> _elementStack = new Stack<HtmlElement>();
public HtmlObjectPrinter(int recursionLimit, int enumerationLimit)
: base(recursionLimit, enumerationLimit)
{
}
private HtmlElement Current
{
get
{
Debug.Assert(_elementStack.Count > 0);
return _elementStack.Peek();
}
}
public void WriteTo(object value, TextWriter writer)
{
HtmlElement rootElement = new HtmlElement("div");
rootElement.AddCssClass("objectinfo");
PushElement(rootElement);
Visit(value, 0);
PopElement();
Debug.Assert(_elementStack.Count == 0, "Stack should be empty");
// REVIEW: We should only do this once per page/request
writer.Write(Styles);
rootElement.WriteTo(writer);
}
public override void VisitKeyValues(object value, IEnumerable<object> keys, Func<object, object> valueSelector, int depth)
{
string id = GetObjectId(value);
HtmlElement ul = new HtmlElement("ul");
ul.AddCssClass("typeEnumeration");
ul["id"] = id;
PushElement(ul);
base.VisitKeyValues(value, keys, valueSelector, depth);
PopElement();
Current.AppendChild(ul);
}
public override void VisitKeyValue(object key, object value, int depth)
{
HtmlElement keyElement = new HtmlElement("span");
PushElement(keyElement);
Visit(key, depth);
PopElement();
HtmlElement valueElement = new HtmlElement("span");
PushElement(valueElement);
Visit(value, depth);
PopElement();
// Append the elements to the li
HtmlElement li = new HtmlElement("li");
li.AppendChild(keyElement);
li.AppendChild(" = ");
li.AppendChild(valueElement);
Current.AppendChild(li);
}
public override void VisitEnumerable(IEnumerable enumerable, int depth)
{
string id = GetObjectId(enumerable);
HtmlElement ul = new HtmlElement("ul");
ul.AddCssClass("typeEnumeration");
ul["id"] = id;
PushElement(ul);
base.VisitEnumerable(enumerable, depth);
PopElement();
Current.AppendChild(ul);
}
public override void VisitIndexedEnumeratedValue(int index, object item, int depth)
{
HtmlElement li = new HtmlElement("li");
li.AppendChild(String.Format(CultureInfo.InvariantCulture, "[{0}] = ", index));
PushElement(li);
base.VisitIndexedEnumeratedValue(index, item, depth);
PopElement();
Current.AppendChild(li);
}
public override void VisitEnumeratedValue(object item, int depth)
{
HtmlElement li = new HtmlElement("li");
PushElement(li);
base.VisitEnumeratedValue(item, depth);
PopElement();
Current.AppendChild(li);
}
public override void VisitEnumeratonLimitExceeded()
{
HtmlElement li = new HtmlElement("li");
li.AppendChild("...");
Current.AppendChild(li);
}
public override void VisitMembers(IEnumerable<string> names, Func<string, Type> typeSelector, Func<string, object> valueSelector, int depth)
{
HtmlElement ul = new HtmlElement("ul");
ul.AddCssClass("typeProperties");
PushElement(ul);
base.VisitMembers(names, typeSelector, valueSelector, depth);
PopElement();
Current.AppendChild(ul);
}
public override void VisitMember(string name, Type type, object value, int depth)
{
HtmlElement li = new HtmlElement("li");
if (type != null)
{
li.AppendChild(CreateTypeNameSpan(type));
li.AppendChild(" ");
}
li.AppendChild(CreateNameSpan(name));
li.AppendChild(" = ");
PushElement(li);
_excludeTypeName = true;
base.VisitMember(name, type, value, depth);
_excludeTypeName = false;
PopElement();
Current.AppendChild(li);
}
public override void VisitComplexObject(object value, int depth)
{
string id = GetObjectId(value);
HtmlElement objectElement = new HtmlElement("div");
objectElement.AddCssClass("typeContainer");
objectElement["id"] = id;
PushElement(objectElement);
base.VisitComplexObject(value, depth);
PopElement();
if (objectElement.Children.Any())
{
Current.AppendChild(objectElement);
}
}
public override void VisitNull()
{
Current.AppendChild(_nullSpan);
}
public override void VisitStringValue(string stringValue)
{
// Convert the string escape sequences
stringValue = "\"" + ConvertEscapseSequences(stringValue) + "\"";
Current.AppendChild(CreateQuotedSpan(stringValue));
}
public override void VisitVisitedObject(string id, object value)
{
Current.AppendChild(CreateVisitedLink(id));
}
public override void Visit(object value, int depth)
{
if (value != null)
{
if (!_excludeTypeName)
{
Current.AppendChild(CreateTypeNameSpan(value.GetType()));
Current.AppendChild(" ");
}
_excludeTypeName = false;
}
base.Visit(value, depth);
}
public override void VisitObjectVisitorException(ObjectVisitorException exception)
{
Current.AppendChild(CreateExceptionSpan(exception));
}
[SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "Making the value lowercase has nothing to do with normalization. It's used to show true or false instead of the Title case version")]
public override void VisitConvertedValue(object value, string convertedValue)
{
Type type = value.GetType();
if (type.Equals(typeof(bool)))
{
// Convert True or False to lowercase
convertedValue = convertedValue.ToLowerInvariant();
Current.AppendChild(CreateTypeSpan(convertedValue));
return;
}
if (type.Equals(typeof(char)))
{
string charValue = GetCharValue((char)value);
Current.AppendChild(CreateQuotedSpan("'" + charValue + "'"));
return;
}
// See if the value is a Type itself
Type valueAsType = value as Type;
if (valueAsType != null)
{
// For types we're going to generate elements that print typeof(TypeName)
Current.AppendChild(CreateParentSpan(CreateTypeSpan("typeof"),
CreateOperatorSpan("("),
CreateTypeNameSpan(valueAsType),
CreateOperatorSpan(")")));
}
else
{
Current.AppendChild(CreateValueSpan(convertedValue));
}
}
private static HtmlElement CreateParentSpan(params HtmlElement[] elements)
{
HtmlElement span = new HtmlElement("span");
foreach (var e in elements)
{
span.AppendChild(e);
}
return span;
}
private static HtmlElement CreateNameSpan(string name)
{
return HtmlElement.CreateSpan(name, "name");
}
private static HtmlElement CreateOperatorSpan(string @operator)
{
return HtmlElement.CreateSpan(@operator, "operator");
}
private static HtmlElement CreateValueSpan(string value)
{
return HtmlElement.CreateSpan(value, "value");
}
private static HtmlElement CreateExceptionSpan(ObjectVisitorException exception)
{
HtmlElement span = new HtmlElement("span");
span.AppendChild(HelpersResources.ObjectInfo_PropertyThrewException);
span.AppendChild(HtmlElement.CreateSpan(exception.InnerException.Message, "exception"));
return span;
}
private static HtmlElement CreateQuotedSpan(string value)
{
return HtmlElement.CreateSpan(value, "quote");
}
private static HtmlElement CreateLink(string href, string linkText, string cssClass = null)
{
HtmlElement a = new HtmlElement("a");
a.SetInnerText(linkText);
a["href"] = href;
if (!String.IsNullOrEmpty(cssClass))
{
a.AddCssClass(cssClass);
}
return a;
}
private static HtmlElement CreateVisitedLink(string id)
{
string text = String.Format(CultureInfo.InvariantCulture, "[{0}]", HelpersResources.ObjectInfo_PreviousDisplayed);
return CreateLink("#" + id, text);
}
private static HtmlElement CreateTypeSpan(string value)
{
return HtmlElement.CreateSpan(value, "type");
}
private static HtmlElement CreateTypeNameSpan(Type type)
{
string typeName = GetTypeName(type);
HtmlElement span = new HtmlElement("span");
StringBuilder sb = new StringBuilder();
// Convert the type name into html elements with different css classes
foreach (var ch in typeName)
{
if (IsOperator(ch))
{
if (sb.Length > 0)
{
span.AppendChild(CreateTypeSpan(sb.ToString()));
sb.Clear();
}
span.AppendChild(CreateOperatorSpan(ch.ToString()));
}
else
{
sb.Append(ch);
}
}
if (sb.Length > 0)
{
span.AppendChild(CreateTypeSpan(sb.ToString()));
}
return span;
}
private static bool IsOperator(char ch)
{
// These are the operators we expect to see within type names
return ch == '[' || ch == ']' || ch == '<' || ch == '>' || ch == '&' || ch == '*';
}
internal void PushElement(HtmlElement element)
{
_elementStack.Push(element);
}
internal HtmlElement PopElement()
{
Debug.Assert(_elementStack.Count > 0);
return _elementStack.Pop();
}
internal static string ConvertEscapseSequences(string value)
{
StringBuilder sb = new StringBuilder();
foreach (var ch in value)
{
sb.Append(GetCharValue(ch));
}
return sb.ToString();
}
private static string GetCharValue(char ch)
{
string value;
if (_printableEscapeChars.TryGetValue(ch, out value))
{
return value;
}
// REVIEW: Perf?
return ch.ToString();
}
}
}