Code Review Stack Exchange is a question and answer site for peer programmer code reviews. Join them; it only takes a minute:

Sign up
Here's how it works:
  1. Anybody can ask a question
  2. Anybody can answer
  3. The best answers are voted up and rise to the top

I came to the conclusion that the previous framework was too complicated and not easily extendable so I tried it again but this time with dynamics.

It's much much shorter and I think it's much easier to extend it now. Now it's also much easier to write another renderer that creates i.e. a XDocument/XElement.


Core

The main part of the new framework is the MarkupBuilder that is derived from the DynamicObject. It provides all the basic functionality for creating elements.

public class MarkupBuilder : DynamicObject, IEnumerable<object>
{
    private readonly List<IMarkupBuilderExtension> _extensions = new List<UserQuery.IMarkupBuilderExtension>();

    private MarkupBuilder(MarkupBuilder markupBuilder, string tag)
    {
        _extensions = markupBuilder._extensions;
        MarkupSchema = new MarkupSchema(markupBuilder.MarkupSchema);
        Tag = tag;
        Attributes = new List<MarkupAttribute>();
        Content = new List<object>();
    }

    public MarkupBuilder(MarkupSchema markupSchema = null)
    {
        MarkupSchema = new MarkupSchema(markupSchema ?? new MarkupSchema());
        Attributes = new List<MarkupAttribute>();
        Content = new List<object>();
    }

    public IEnumerable<IMarkupBuilderExtension> Extensions => _extensions.AsReadOnly();

    public MarkupSchema MarkupSchema { get; }

    public string Tag { get; }

    public List<MarkupAttribute> Attributes { get; }

    public List<object> Content { get; }

    public MarkupBuilder Parent { get; private set; }

    private int Depth
    {
        get
        {
            var depth = 0;
            var parent = Parent;
            while (parent != null)
            {
                depth++;
                parent = parent.Parent;
            }
            return depth;
        }
    }

    public MarkupBuilder Register<T>() where T : IMarkupBuilderExtension, new()
    {
        _extensions.Add(new T());
        return this;
    }

    // supports object initializer
    public void Add(object content)
    {
        if (content != null)
        {
            Content.Add(content);
            var htmlElement = content as MarkupBuilder;
            if (htmlElement != null)
            {
                htmlElement.Parent = this;
            }
        }
    }

    public MarkupBuilder AddRange(params object[] content)
    {
        foreach (var item in content)
        {
            Add(item);
        }
        return this;
    }

    public IEnumerator<object> GetEnumerator()
    {
        return Content.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }

    // --- DynamicObject members

    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        foreach (var extension in Extensions)
        {
            if (extension.TryGetMember(this, binder, out result))
            {
                return true;
            }
        }
        result = new MarkupBuilder(this, binder.Name);
        return true;
    }

    public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
    {
        foreach (var extension in Extensions)
        {
            if (extension.TryInvokeMember(this, binder, args, out result))
            {
                return true;
            }
        }

        if (MarkupSchema.Tags.Any() && !MarkupSchema.Tags.ContainsKey(binder.Name))
        {
            throw new NotSupportedException($"Method '{binder.Name}' is not supported.");
        }

        result = new MarkupBuilder(this, binder.Name).AddRange(args);
        return true;
    }
}

It can be customized with the MarkupSchema a very simple schema that defines which tags/attributes are allowed and how to format the markup later:

public class MarkupSchema
{
    public MarkupSchema()
    {
        Tags = new Dictionary<string, MarkupFormattingOptions>();
        GlobalAttributes = new HashSet<string>();
        IndentWidth = 4;
    }

    internal MarkupSchema(MarkupSchema other)
    {
        Tags = new Dictionary<string, MarkupFormattingOptions>(other.Tags);
        GlobalAttributes = new HashSet<string>(other.GlobalAttributes);
        IndentWidth = other.IndentWidth;
    }

    public Dictionary<string, MarkupFormattingOptions> Tags { get; set; }

    public HashSet<string> GlobalAttributes { get; set; }

    public int IndentWidth { get; set; }

    // Creates a Html schema.
    public static MarkupSchema Html => new MarkupSchema
    {
        Tags =
        {
            ["body"] = MarkupFormattingOptions.PlaceClosingTagOnNewLine,
            ["br"] = MarkupFormattingOptions.IsVoid,
            ["span"] = MarkupFormattingOptions.None,
            ["p"] = MarkupFormattingOptions.PlaceOpeningTagOnNewLine,
            ["h1"] = MarkupFormattingOptions.PlaceOpeningTagOnNewLine,
            ["h2"] = MarkupFormattingOptions.PlaceOpeningTagOnNewLine,
            ["h3"] = MarkupFormattingOptions.PlaceOpeningTagOnNewLine,
            ["h4"] = MarkupFormattingOptions.PlaceOpeningTagOnNewLine,
            ["h5"] = MarkupFormattingOptions.PlaceOpeningTagOnNewLine,
            ["h6"] = MarkupFormattingOptions.PlaceOpeningTagOnNewLine,          
            ["ul"] = MarkupFormattingOptions.PlaceBothTagsOnNewLine,
            ["ol"] = MarkupFormattingOptions.PlaceBothTagsOnNewLine,
            ["li"] = MarkupFormattingOptions.PlaceOpeningTagOnNewLine,
            // ...
        },
        GlobalAttributes = { "style" }
    };

    public bool TagHasFormattingOptions(string tagName, MarkupFormattingOptions options)
    {
        var tagFormattingOptions = MarkupFormattingOptions.None;
        return Tags.TryGetValue(tagName, out tagFormattingOptions) ? tagFormattingOptions.HasFlag(options) : false;
    }
}

Unfortunatelly the normal C# extensions does not work with a dynamic object so to be able to extend it I created a different system of extensions. Their base is the IMarkupBuilderExtension interface:

public interface IMarkupBuilderExtension
{
    bool TryGetMember(MarkupBuilder element, GetMemberBinder binder, out object result);
    bool TryInvokeMember(MarkupBuilder element, InvokeMemberBinder binder, object[] args, out object result);
}

Here are two examples of such extensions. One is for adding css and other for any other attribute:

public class cssExtension : IMarkupBuilderExtension
{
    public bool TryGetMember(MarkupBuilder builder, GetMemberBinder binder, out object result)
    {
        result = null;
        return false;
    }

    public bool TryInvokeMember(MarkupBuilder builder, InvokeMemberBinder binder, object[] args, out object result)
    {
        if (binder.Name == "css")
        {
            builder.Attributes.Add(new MarkupAttribute("style") { Value = (string)args[0] });
            result = builder;
            return true;
        }

        result = null;
        return false;
    }
}

public class attrExtension : IMarkupBuilderExtension
{
    public bool TryGetMember(MarkupBuilder builder, GetMemberBinder binder, out object result)
    {
        result = null;
        return false;
    }

    public bool TryInvokeMember(MarkupBuilder builder, InvokeMemberBinder binder, object[] args, out object result)
    {
        if (binder.Name == "attr")
        {
            result = new MarkupAttribute((string)args[0]) { Value = (string)args[1] };
            return true;
        }

        result = null;
        return false;
    }
}

Rendering

I moved the rendering part into a separate class which is now the MarkupRenderer:

public class MarkupRenderer
{
    public static string RenderMarkup(MarkupBuilder builder) 
    {
        return RenderMarkup(builder, builder.MarkupSchema);
    }

    private static string RenderMarkup(object value, MarkupSchema markupSchema)
    {
        var markupBuilder = value as MarkupBuilder;
        if (markupBuilder == null)
        {
            return value == null ? string.Empty : (string)value;
        }

        var content = markupBuilder.Content.Aggregate(
                new StringBuilder(),
                (sb, next) => sb.Append(MarkupRenderer.RenderMarkup(next, markupSchema))).ToString();

        var isEmpty = string.IsNullOrEmpty(content);

        var html = new StringBuilder();

        if (markupBuilder.Parent != null && markupSchema.TagHasFormattingOptions(markupBuilder.Tag, MarkupFormattingOptions.PlaceOpeningTagOnNewLine))
        {
            html.AppendLine().Append(IndentString(markupSchema.IndentWidth));
        }

        html.Append(CreateOpeningElement(markupBuilder));

        //        if (IsVoid)
        //        {
        //            return html.ToString();
        //        }

        if (!isEmpty)
        {
            html.Append(content);
        }

        if (!isEmpty && markupSchema.TagHasFormattingOptions(markupBuilder.Tag, MarkupFormattingOptions.PlaceClosingTagOnNewLine))
        {
            html.AppendLine();
            if (markupBuilder.Parent != null) { html.Append(IndentString(markupSchema.IndentWidth)); }
        }

        html.Append(CreateClosingElement(markupBuilder));

        return html.ToString();
    }

    private static string IndentString(int indentWidth)
    {
        return new string(' ', indentWidth);
    }

    private static string CreateOpeningElement(MarkupBuilder markupBuilder)
    {
        var attributes = CreateAttributesString(markupBuilder);

        var html = new StringBuilder()
            .Append("<").Append(markupBuilder.Tag)
            .Append(string.IsNullOrEmpty(attributes) ? string.Empty : " ")
            .Append(attributes)
            //.Append(IsVoid ? "/" : string.Empty)
            .Append(">")
            .ToString();
        return html;
    }

    private static string CreateAttributesString(MarkupBuilder markupBuilder)
    {
        return string.Join(" ", markupBuilder.Attributes);
    }

    private static string CreateClosingElement(MarkupBuilder markupBuilder)
    {
        return markupBuilder.MarkupSchema.TagHasFormattingOptions(markupBuilder.Tag, MarkupFormattingOptions.IsVoid)
            ? string.Empty
            : new StringBuilder()
                .Append("</")
                .Append(markupBuilder.Tag)
                .Append(">")
                .ToString();
    }
}

There are also two more supporting types.

One is the formatting options enum:

[Flags]
public enum MarkupFormattingOptions
{
    None = 0,
    PlaceOpeningTagOnNewLine = 1,
    PlaceClosingTagOnNewLine = 2,
    PlaceBothTagsOnNewLine =
        PlaceOpeningTagOnNewLine |
        PlaceClosingTagOnNewLine,
    IsVoid = 4,
    CloseEmptyTag = 8
}

The other is a markup attribute:

public class MarkupAttribute
{
    public MarkupAttribute(string name)
    {
        Name = name;
    }

    public string Name { get; private set; }

    public string Value { get; set; }

    public override string ToString()
    {
        return string.Format("{0}=\"{1}\"", Name, Value);
    }
}

Usage/Examples

dynamic html = 
    new MarkupBuilder(MarkupSchema.Html)
        .Register<cssExtension>()
        .Register<attrExtension>();

var body = html.body
(
    html.p("foo"),
    html.p
    (
        "bar",
        html.span("quux").css("blah"),
        html.br,
        "baz"
    )
);

MarkupRenderer.RenderMarkup(body as MarkupBuilder).Dump();

Output:

<body>
   <p>foo</p>
   <p>bar<span style="blah">quux</span><br>baz</p>
</body>
share|improve this question
    
I guess my comments below is invalid as your code as changed – Tolani Jaiye-Tikolo Aug 1 at 1:21
    
@TolaniJaiye-Tikolo I've updated it slightly because there were no comments yet at that point. I wouldn't have done it if I already have seen any. I can restore the previous version. – t3chb0t Aug 1 at 3:37
    
It's alright.. I wasn't refreshing my screen for a while and hence I didn't see the changes- I can work with the new one. No worries. – Tolani Jaiye-Tikolo Aug 1 at 6:21
    
Mate, where have you got UserQuery.IMarkupBuilderExtension defined? I can't seem to find it. – Tolani Jaiye-Tikolo Aug 1 at 23:21
    
@TolaniJaiye-Tikolo the UserQuery.IMarkupBuilderExtensionis defined just before the extensions example. the UserQuery is a copy/paste error from LINQPad where I was refactoring/simplifying the code for the question. UserQuery shouldn't be there. – t3chb0t Aug 2 at 3:45

This looks really weird:

public static string RenderMarkup(MarkupBuilder builder) 
{
    return RenderMarkup(builder, builder.MarkupSchema);
}

private static string RenderMarkup(object value, MarkupSchema markupSchema)
{
    var markupBuilder = value as MarkupBuilder;
    if (markupBuilder == null)
    {
        return value == null ? string.Empty : (string)value;
    }

what is the point of casting MarkupBuilder to object and then casting it back?


As I'm sure you know, classes' and methods' names should start with capital letter and blah blah as per C# naming convention. Why do you insist on using camel case?


You should make your api more consistent:

public void Add(object content)

public MarkupBuilder AddRange(params object[] content)

either return MarkupBuilder from both methods, or make both methods void.


As for your design, my main concern is that MarkupBuilder appears to have two distinct responsibilities. First - constructing an html element using extensions and dynamic methods. Second - holding the actual result of construction: a strongly typed data. I think you should have two separate classes for that. So the usage would look like this:

var body = 
         //describe the markup
         html.body(html.p("foo"))
         //return strongly typed non-dynamic and, possibly, immutable object, that holds actual data
         .Build();

You know, similar to how StringBuilder works, where you call ToString() once you are done, and get rid of all the extra overhead.

P.S. Personally, I find dynamic objects useful as part of internal implementation, for which you write unit tests and leave it alone. I would not use dynamics as part of public API. Yes the code is shorter. But at the same time it is more complex and is more error prone. It's really easy to get dynamic objects wrong, and there is no compiler to tell you that "hey, this, html.spun method you have just called, looks suspicions, did you mean span?" So i would rather implement a bunch of extra classes once (as you did in your previous implementation), than hunt dynamic-related bugs for the rest of my life. :)

share|improve this answer
    
This looks really weird - I thought the same and was experimenting with different interfaces so I actually rewrote it and now it uses an interface and only a single recursive method. MarkupBuilder appears to have two distinct responsibilities - I started with inheritance and separated concerns but it didn't work with the DynamicObject and it couldn't find some methods. I need to defend this design. The previous one couldn't be extended by the user and this one is super easy to extend. I'll post the refactored code with some more examples later. – t3chb0t Aug 3 at 10:10
    
I don't have time to implement a schema validation but all tags and attributes could be validated so there would be no bug hunt. On the other side it's just html so it's really easy to spot a bug in the markup. – t3chb0t Aug 3 at 10:11
    
Add vs AddRange - the rationale behind this was that the Add as void is required by the object initializer for IEnumerable<T> types and the AddRange should return the object so that I don't have to use a helper variables and can chain it aka fluent API. I admit this isn't consistant but I didn't find another way to solve it. – t3chb0t Aug 3 at 10:43

To be honest, there isn't enough to refactor here as your code looks clean but I notice in your constructors you've this:

MarkupSchema = new MarkupSchema(markupSchema ?? new MarkupSchema());

You could just do this:

MarkupSchema = markupSchema ?? new MarkupSchema();

Your two constructors are doing similar stuff and the first one is private, you could just do this

public MarkupBuilder(MarkupSchema markupSchema = null)
: this(markupSchema,string.Empty)
    {
    }

Your code can be extended for XML documents as well. All you need is to ensure xmlRenderer includes <?xml version="1.0" encoding="UTF-8"?> to the output and a root element with other contents as child elements.

share|improve this answer
    
Thank you for your comments ;-) no refactoring is also a good sign ;-] as to the constructor. The rationale behind it is that theMarkupSchemaisn't immutable so I use a private copy constructor and a private property so that the user cannot change the values after creating the builder. Or he could be the changes would have no effect on the schema anymore. I didn't know how else I could prevent it and passing those huge arrays aka schema definitions didn't sound like a good idea either. – t3chb0t Aug 2 at 3:49

This is the latest version that I currently use.

I post it here just for reference so I won't comment every class.

There are however some changes. I replaced the MarkupSchema with MarkupFormatting. I don't care for schema validation now (and I actually don't have time to implement it).

The MarkupBuilder requires an IMarkupRenderer so that we can ToString it and see the result in debug if necessary.

There were question why I don't separate it and return a specialized object. The reason for this is the following: I wouldn't be able to do this

dynamic html =
    new MarkupBuilder(new HtmlRenderer())
    .Add<cssExtension>()
    .Add<attrExtension>();    

var table = html.table(html.thead(html.tr(html.th("n"), html.th("n*n"))));

// add tfoot to table
table.tfoot(html.tr(html.td("foo"), html.td("bar")));

If the table weren't a builder I couldn't append anything to it and had to use different syntax like maybe Append or Add.

I've added also one more extension the styleExtension that allows to predefine some styles and use them by name later:

dynamic html = new MarkupBuilder(new HtmlRenderer())
    .Add<cssExtension>()
    .Add<attrExtension>()
    .Add<styleExtension>()
    .Configure<styleExtension>(ext => ext.Styles = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
    {
        { "abc", "color: #0066cc;" }
    }
);

html.p("foo").style("abc");

I have also fixed the renderer & the Add APIs as suggested by @NikitaB. It was indeed not optimal.


Complete code:

public class MarkupBuilder : DynamicObject, IEnumerable<object>
{
    private readonly Dictionary<string, string> _attributes = new Dictionary<string, string>();
    private readonly List<object> _content = new List<object>();
    private readonly List<IMarkupBuilderExtension> _extensions = new List<IMarkupBuilderExtension>();

    private MarkupBuilder(MarkupBuilder markupBuilder, string tag)
    {
        _extensions = markupBuilder._extensions;
        Renderer = markupBuilder.Renderer;
        Tag = tag;
    }

    public MarkupBuilder(IMarkupRenderer renderer)
    {
        Renderer = renderer;
    }

    public string Tag { get; private set; }

    public IDictionary<string, string> Attributes { get { return _attributes; } }

    public IReadOnlyCollection<object> Content { get { return _content; } }

    public MarkupBuilder Parent { get; private set; }

    public IEnumerable<IMarkupBuilderExtension> Extensions { get { return _extensions.AsReadOnly(); } }

    internal int Depth
    {
        get
        {
            var depth = 0;
            var parent = Parent;
            while (parent != null)
            {
                depth++;
                parent = parent.Parent;
            }
            return depth;
        }
    }

    // Using a renderer + ToString allows us to see the markup in the debug.
    public IMarkupRenderer Renderer { get; private set; }

    public MarkupBuilder Add<T>() where T : IMarkupBuilderExtension, new()
    {
        _extensions.Add(new T());
        return this;
    }

    public MarkupBuilder Configure<T>(Action<T> extension) where T : IMarkupBuilderExtension
    {
        var ext = Extensions.OfType<T>().SingleOrDefault();
        extension(ext);
        return this;
    }

    private MarkupBuilder Add(object content)
    {
        if (content == null) { return this; }
        _content.Add(content);
        var builder = content as MarkupBuilder;
        if (builder != null) { builder.Parent = this; }
        return this;
    }

    public MarkupBuilder AddRange(IEnumerable<object> content)
    {
        foreach (var item in content) { Add(item); }
        return this;
    }

    public MarkupBuilder AddRange(params object[] content)
    {
        return AddRange((IEnumerable<object>)content);
    }

    public IEnumerator<object> GetEnumerator()
    {
        return Content.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    // --- DynamicObject members

    public override bool TryConvert(ConvertBinder binder, out object result)
    {
        if (binder.ReturnType == typeof(string))
        {
            result = ToString();
            return true;
        }
        result = null;
        return false;
    }

    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        foreach (var extension in Extensions)
        {
            if (extension.TryGetMember(this, binder, out result))
            {
                return true;
            }
        }
        result = new MarkupBuilder(this, binder.Name);
        return true;
    }

    public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
    {
        foreach (var extension in Extensions)
        {
            if (extension.TryInvokeMember(this, binder, args, out result))
            {
                return true;
            }
        }

        var isEnumerable = new Func<Type, bool>(t =>
        {
            return
                t.GetInterfaces()
                .Where(x => x.IsGenericType)
                .Select(x => x.GetGenericTypeDefinition())
                .Contains(typeof(IEnumerable<>)) &&
                t != typeof(string) &&
                t != typeof(MarkupBuilder);
        });

        var mb = new MarkupBuilder(this, binder.Name);
        result = mb;

        // Allows us to pass an IEnumerable<T> of elements.
        if (args != null && args.Any() && isEnumerable(args[0].GetType()))
        {
            mb.AddRange((IEnumerable<object>)args[0]);
        }
        else
        {
            mb.AddRange(args);
        }

        // The first builder is not an element and has not tag.
        var isElement = !string.IsNullOrEmpty(Tag);
        if (isElement) { Add(result); }

        return true;
    }

    public override string ToString()
    {
        return Renderer.Render(this);
    }
}

public interface IMarkupBuilderExtension
{
    bool TryGetMember(MarkupBuilder element, GetMemberBinder binder, out object result);
    bool TryInvokeMember(MarkupBuilder element, InvokeMemberBinder binder, object[] args, out object result);
}

public class cssExtension : IMarkupBuilderExtension
{
    public bool TryGetMember(MarkupBuilder builder, GetMemberBinder binder, out object result)
    {
        result = null;
        return false;
    }

    public bool TryInvokeMember(MarkupBuilder builder, InvokeMemberBinder binder, object[] args, out object result)
    {
        if (binder.Name == "css")
        {
            builder.Attributes.Add("style", (string)args[0]);
            result = builder;
            return true;
        }
        result = null;
        return false;
    }
}

public class attrExtension : IMarkupBuilderExtension
{
    public bool TryGetMember(MarkupBuilder builder, GetMemberBinder binder, out object result)
    {
        result = null;
        return false;
    }

    public bool TryInvokeMember(MarkupBuilder builder, InvokeMemberBinder binder, object[] args, out object result)
    {
        if (binder.Name == "attr")
        {
            builder.Attributes.Add(binder.Name, (string)args[0]);
            result = builder;
            return true;
        }
        result = null;
        return false;
    }
}

public class styleExtension : IMarkupBuilderExtension
{
    public IDictionary<string, string> Styles { get; set; }

    public bool TryGetMember(MarkupBuilder builder, GetMemberBinder binder, out object result)
    {
        result = null;
        return false;
    }

    public bool TryInvokeMember(MarkupBuilder builder, InvokeMemberBinder binder, object[] args, out object result)
    {
        if (binder.Name == "style")
        {
            var styleName = (string)args[0];
            var style = (string)null;
            if (Styles != null && Styles.TryGetValue(styleName, out style))
            {
                builder.Attributes.Add(binder.Name, style);
                result = builder;
                return true;
            }
        }
        result = null;
        return false;
    }
}

[Flags]
public enum MarkupFormattingOptions
{
    None = 0,
    PlaceOpeningTagOnNewLine = 1,
    PlaceClosingTagOnNewLine = 2,
    PlaceBothTagsOnNewLine =
        PlaceOpeningTagOnNewLine |
        PlaceClosingTagOnNewLine,
    IsVoid = 4,
    CloseEmptyTag = 8
}

public abstract class MarkupFormatting
{
    public MarkupFormattingOptions this[string tag]
    {
        get
        {
            var tagFormattingOptions = MarkupFormattingOptions.None;
            return
                TagFormattingOptions.TryGetValue(tag, out tagFormattingOptions)
                ? tagFormattingOptions
                : MarkupFormattingOptions.None;
        }
    }

    public Dictionary<string, MarkupFormattingOptions> TagFormattingOptions { get; set; }

    public int IndentWidth { get; set; }
}

public class HtmlFormatting : MarkupFormatting
{
    public HtmlFormatting()
    {
        TagFormattingOptions = new Dictionary<string, MarkupFormattingOptions>
        {
            { "body", MarkupFormattingOptions.PlaceClosingTagOnNewLine },
            { "br", MarkupFormattingOptions.IsVoid },
            { "span", MarkupFormattingOptions.None },
            { "p", MarkupFormattingOptions.PlaceOpeningTagOnNewLine },
            { "pre", MarkupFormattingOptions.PlaceOpeningTagOnNewLine },
            { "h1", MarkupFormattingOptions.PlaceOpeningTagOnNewLine },
            { "h2", MarkupFormattingOptions.PlaceOpeningTagOnNewLine },
            { "h3", MarkupFormattingOptions.PlaceOpeningTagOnNewLine },
            { "h4", MarkupFormattingOptions.PlaceOpeningTagOnNewLine },
            { "h5", MarkupFormattingOptions.PlaceOpeningTagOnNewLine },
            { "h6", MarkupFormattingOptions.PlaceOpeningTagOnNewLine },
            { "ul", MarkupFormattingOptions.PlaceBothTagsOnNewLine },
            { "ol", MarkupFormattingOptions.PlaceBothTagsOnNewLine },
            { "li", MarkupFormattingOptions.PlaceOpeningTagOnNewLine },
            { "table", MarkupFormattingOptions.PlaceClosingTagOnNewLine },
            { "caption", MarkupFormattingOptions.PlaceOpeningTagOnNewLine },
            { "thead", MarkupFormattingOptions.PlaceBothTagsOnNewLine },
            { "tbody", MarkupFormattingOptions.PlaceBothTagsOnNewLine },
            { "tfoot", MarkupFormattingOptions.PlaceBothTagsOnNewLine },
            { "tr", MarkupFormattingOptions.PlaceBothTagsOnNewLine },
            { "th", MarkupFormattingOptions.PlaceOpeningTagOnNewLine },
            { "td", MarkupFormattingOptions.PlaceOpeningTagOnNewLine },
            { "colgroup", MarkupFormattingOptions.PlaceOpeningTagOnNewLine },
            { "col", MarkupFormattingOptions.PlaceOpeningTagOnNewLine },
        };
        IndentWidth = 4;
    }
}

public interface IMarkupRenderer
{
    string Render(MarkupBuilder markupBuilder);
}

public class MarkupRenderer : IMarkupRenderer
{
    public MarkupRenderer(MarkupFormatting markupFormatting)
    {
        MarkupFormatting = markupFormatting;
    }

    private MarkupFormatting MarkupFormatting { get; set; }

    public string Render(MarkupBuilder markupBuilder)
    {
        var content = markupBuilder.Aggregate(new StringBuilder(), (sb, next) =>
        {
            var mb = next as MarkupBuilder;
            return sb.Append(mb == null ? next : Render(mb));
        })
        .ToString();

        var isEmpty = string.IsNullOrEmpty(content);

        var placeOpeningTagOnNewLine = new Func<string, bool>(tag
            => markupBuilder.Parent != null && MarkupFormatting[tag].HasFlag(MarkupFormattingOptions.PlaceOpeningTagOnNewLine)
        );

        var placeClosingTagOnNewLine = new Func<string, bool>(tag
            => !isEmpty && MarkupFormatting[tag].HasFlag(MarkupFormattingOptions.PlaceClosingTagOnNewLine)
        );

        var indent = new Func<string>(() => IndentString(MarkupFormatting.IndentWidth * markupBuilder.Depth));

        var html = new StringBuilder();

        if (placeOpeningTagOnNewLine(markupBuilder.Tag))
        {
            html.AppendLine();
            html.Append(indent());
        }

        html.Append(CreateOpeningElement(markupBuilder.Tag, markupBuilder.Attributes));
        html.Append(content);

        if (placeClosingTagOnNewLine(markupBuilder.Tag))
        {
            html.AppendLine();
            html.Append(markupBuilder.Parent == null ? string.Empty : indent());
        }

        html.Append(CreateClosingElement(markupBuilder.Tag));

        return html.ToString();
    }

    private static string IndentString(int indentWidth)
    {
        return new string(' ', indentWidth);
    }

    private string CreateOpeningElement(string tag, IEnumerable<KeyValuePair<string, string>> attributes)
    {
        var attributeString = CreateAttributeString(attributes);

        var html = new StringBuilder()
            .Append("<").Append(tag)
            .Append(string.IsNullOrEmpty(attributeString) ? string.Empty : " ")
            .Append(attributeString)
            //.Append(IsVoid ? "/" : string.Empty)
            .Append(">")
            .ToString();
        return html;
    }

    private static string CreateAttributeString(IEnumerable<KeyValuePair<string, string>> attributes)
    {
        return attributes.Aggregate(
            new StringBuilder(),
            (result, kvp) => result
                .Append(result.Length > 0 ? " " : string.Empty)
                .AppendFormat("{0}=\"{1}\"", kvp.Key, kvp.Value)
        ).ToString();
    }

    private static string CreateClosingElement(string tag)
    {
        return
            new StringBuilder()
            .Append("</")
            .Append(tag)
            .Append(">")
            .ToString();
    }
}

public class HtmlRenderer : IMarkupRenderer
{
    public HtmlRenderer(MarkupFormatting markupFormatting = null)
    {
        MarkupRenderer = new MarkupRenderer(markupFormatting ?? new HtmlFormatting());
    }

    private MarkupRenderer MarkupRenderer { get; set; }

    public string Render(MarkupBuilder markupBuilder)
    {
        return MarkupRenderer.Render(markupBuilder);
    }
}
share|improve this answer
    
I wouldn't be able to do this. I think you would. By either a) not calling Build method in my example and passing the builder around until you are done "building" your html, or b) implementing another constructor on MarkupBuilder, which would take the html object that needs to be edited (similar to StringBuilder(string) constructor) – Nikita B Aug 5 at 7:22

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.