// 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 = @" "; private static readonly HtmlElement _nullSpan = HtmlElement.CreateSpan("(null)", "null"); // List of chars to escape within strings private static readonly Dictionary _printableEscapeChars = new Dictionary { { '\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 _elementStack = new Stack(); 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 keys, Func 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 names, Func typeSelector, Func 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(); } } }