7
\$\begingroup\$

Sometimes I need to auto-generate html-emails. To make this task a little bit easier I created a simple framework that takes care of rendering HTML. Because I'm mainly interested in generating HTML for emails the framework adds styles only via the style attribute.

Although the code is several hundred lines long the actual core part isn't long and should be easy to understand. The most parts are just element classes derived from the base type.


Core

The heart of the framework is the HtmlElement class. It stores general data about an HTML element and can render it to HTML.

public interface IHtmlElement
{
    HtmlTag Tag { get; }
    List<HtmlAttribute> Attributes { get; }
    IHtmlElement Parent { get; set; }
}

public abstract class HtmlElement<TContent> : IHtmlElement, IEnumerable<TContent> where TContent : class
{
    protected HtmlElement(HtmlTag tag)
    {
        Tag = tag;
        Attributes = new List<HtmlAttribute>();
        Content = new List<TContent>();
    }

    public HtmlTag Tag { get; protected set; }

    public List<HtmlAttribute> Attributes { get; private set; }

    public List<TContent> Content { get; private set; }

    public IHtmlElement Parent { get; set; }

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

    private int IndentWidth
    {
        get { return HtmlConstants.OneIndentWidth * Depth; }
    }

    private string IndentString
    {
        get { return new string(' ', IndentWidth); }
    }

    public bool IsVoid
    {
        get { return this.TagHasAttribute<HtmlVoidElementAttribute>(); }
    }

    protected string CreateOpeningElement()
    {
        var attributes = CreateAttributesString();

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

    protected string CreateAttributesString()
    {
        return string.Join(" ", Attributes);
    }

    protected string CreateClosingElement()
    {
        return IsVoid
            ? string.Empty
            : new StringBuilder()
                .Append("</")
                .Append(Tag)
                .Append(">")
                .ToString();
    }

    public override string ToString()
    {
        var content = Content.Aggregate(
                new StringBuilder(),
                (builder, next) => builder.Append(next)).ToString();

        var isEmpty = string.IsNullOrEmpty(content);

        var html = new StringBuilder();

        if (Parent != null && this.FormattingOptions().HasFlag(HtmlFormattingOptions.PlaceOpeningTagOnNewLine)) 
        {
            html.AppendLine().Append(IndentString);
        }

        html.Append(CreateOpeningElement());

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

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

        if (!isEmpty && this.FormattingOptions().HasFlag(HtmlFormattingOptions.PlaceClosingTagOnNewLine))
        {
            html.AppendLine().Append(IndentString);
        }

        html.Append(CreateClosingElement());

        return html.ToString();        
    }

    public static implicit operator string (HtmlElement<TContent> htmlElement)
    {
        return htmlElement.ToString();
    }

    public static HtmlElement<TContent> operator +(HtmlElement<TContent> x, HtmlElement<TContent> y)
    {
        x.Add(y);
        return x;
    }

    // supports object initializer
    public void Add(object content)
    {
        Add(content as TContent);        
    }

    public HtmlElement<TContent> AddRange(params TContent[] content)
    {
        foreach (var item in content)
        {
            Add(item);
        }
        return this;
    }

    public void Add(TContent content)
    {
        if (content != null)
        {
            Content.Add(content);
            var htmlElement = content as IHtmlElement;
            if (htmlElement != null)
            {
                htmlElement.Parent = this;
            }
        }
    }

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

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

Actually, it is already possible to work with this alone (if it wasn't abstract):

var p = new HtmlParagraph
{
    "foo ",
    new HtmlSpan { "bar" }.css("color: #ff0011;"),
    " baz"
}.css("font-style: bold;");

but to make it still easier I defined a concrete class for each HTML element type that I need (to make it shorter I removed some):

public class HtmlParagraph : HtmlElement<object>
{
    public HtmlParagraph() : base(HtmlTag.p) { }
}

public class HtmlSpan : HtmlElement<object>
{
    public HtmlSpan() : base(HtmlTag.span) { }
}

public class HtmlTableCell : HtmlElement<object>
{
    public HtmlTableCell() : base(HtmlTag.td) { }
}

public class HtmlTableHeaderCell : HtmlElement<object>
{
    public HtmlTableHeaderCell() : base(HtmlTag.th) { }
}

public class HtmlTableRow : HtmlElement<HtmlTableCell>
{
    public HtmlTableRow() : base(HtmlTag.tr) { }
}

public class HtmlTableHeaderRow : HtmlElement<HtmlTableHeaderCell>
{
    public HtmlTableHeaderRow() : base(HtmlTag.tr) { }
}

public abstract class HtmlTableRowSet<TContent> : HtmlElement<TContent> where TContent : class
{
    protected HtmlTableRowSet(HtmlTag tag) : base(tag) { }
}

public class HtmlTableHead : HtmlTableRowSet<HtmlTableHeaderRow>, IHtmlTableElement
{
    public HtmlTableHead() : base(HtmlTag.thead) { }
}

public class HtmlTableBody : HtmlTableRowSet<HtmlTableRow>, IHtmlTableElement
{
    public HtmlTableBody() : base(HtmlTag.tbody) { }
}

public class HtmlTableFoot : HtmlTableRowSet<HtmlTableRow>, IHtmlTableElement
{
    public HtmlTableFoot() : base(HtmlTag.tfoot) { }
}

public interface IHtmlTableElement : IHtmlElement { }

public class HtmlTable : HtmlTableRowSet<IHtmlTableElement>
{
    public HtmlTable() : base(HtmlTag.table) { }
}

public class HtmlTableColumnElement : HtmlElement<object>
{
    public HtmlTableColumnElement() : base(HtmlTag.col) { }
}

public class HtmlTableColumnGroupElement : HtmlElement<HtmlTableColumnElement>, IHtmlTableElement
{
    public HtmlTableColumnGroupElement() : base(HtmlTag.colgroup) { }
}

HtmlTag

HTML tags and their properties or formatting are defined in an enum:

public enum HtmlTag
{
    [HtmlFormatting(HtmlFormattingOptions.PlaceOpeningTagOnNewLine)]
    h1,

    [HtmlFormatting(HtmlFormattingOptions.PlaceOpeningTagOnNewLine)]
    h2,

    [HtmlFormatting(HtmlFormattingOptions.PlaceOpeningTagOnNewLine)]
    h3,

    [HtmlFormatting(HtmlFormattingOptions.PlaceOpeningTagOnNewLine)]
    h4,

    [HtmlFormatting(HtmlFormattingOptions.PlaceOpeningTagOnNewLine)]
    h5,

    [HtmlFormatting(HtmlFormattingOptions.PlaceOpeningTagOnNewLine)]
    h6,

    [HtmlFormatting(HtmlFormattingOptions.PlaceOpeningTagOnNewLine)]
    p,

    [HtmlVoidElement()]
    [HtmlFormatting(HtmlFormattingOptions.None)]
    br,

    [HtmlFormatting(HtmlFormattingOptions.None)]
    span,

    [HtmlFormatting(HtmlFormattingOptions.PlaceBothTagsOnNewLine)]
    table,

    [HtmlFormatting(HtmlFormattingOptions.PlaceOpeningTagOnNewLine)]
    td,

    [HtmlFormatting(HtmlFormattingOptions.PlaceBothTagsOnNewLine)]
    tr,

    [HtmlFormatting(HtmlFormattingOptions.PlaceBothTagsOnNewLine)]
    thead,

    [HtmlFormatting(HtmlFormattingOptions.PlaceBothTagsOnNewLine)]
    tfoot,

    [HtmlFormatting(HtmlFormattingOptions.PlaceBothTagsOnNewLine)]
    tbody,

    [HtmlFormatting(HtmlFormattingOptions.PlaceBothTagsOnNewLine)]
    colgroup,

    [HtmlVoidElement()]
    [HtmlFormatting(HtmlFormattingOptions.PlaceOpeningTagOnNewLine)]
    col,    

    [HtmlFormatting(HtmlFormattingOptions.PlaceBothTagsOnNewLine)]
    pre,

    [HtmlFormatting(HtmlFormattingOptions.PlaceOpeningTagOnNewLine)]
    th,
}

and here are the attributes:

[AttributeUsage(AttributeTargets.Field)]
public class HtmlVoidElementAttribute : Attribute { }

[AttributeUsage(AttributeTargets.Field)]
public class HtmlBlockElementAttribute : Attribute { }

[AttributeUsage(AttributeTargets.Field)]
public class HtmlInlineElementAttribute : Attribute { }

[AttributeUsage(AttributeTargets.Field)]
public class HtmlFormattingAttribute : Attribute
{
    public HtmlFormattingAttribute(HtmlFormattingOptions options)
    {
        Options = options;
    }

    public HtmlFormattingOptions Options { get; private set; }
}

[Flags]
public enum HtmlFormattingOptions
{
    None = 0,
    PlaceOpeningTagOnNewLine = 1,
    PlaceClosingTagOnNewLine = 2,
    PlaceBothTagsOnNewLine = 
        PlaceOpeningTagOnNewLine | 
        PlaceClosingTagOnNewLine
}

Other supporting types:

Style attribute for CSS:

public class HtmlStyleAttribute : HtmlAttribute
{
    public HtmlStyleAttribute() : base("style") { }

    public HtmlStyleAttribute(string css) : this()
    {
        Value = css;
    }
}

and a constant for indentation:

class HtmlConstants
{
    public const int OneIndentWidth = 4;
}

Extensions

I use also a few extensions: one for applying CSS and two for getting general info about an element:

public static class HtmlElementExtensions
{
    public static T css<T>(this T element, string css) where T : IHtmlElement
    {
        element.Attributes.Add(new HtmlStyleAttribute(css));
        return element;
    }

    public static bool TagHasAttribute<TAttribute>(this IHtmlElement element)
        where TAttribute : Attribute
    {
        return
            typeof(HtmlTag)
            .GetMember(element.Tag.ToString())
            .First()
            .GetCustomAttribute(typeof(TAttribute), false) != null;
    }

    public static HtmlFormattingOptions FormattingOptions(this IHtmlElement element)
    {
        var htmlFormattingAttribute =
            typeof(HtmlTag)
            .GetMember(element.Tag.ToString())
            .First()
            .GetCustomAttribute(typeof(HtmlFormattingAttribute), false) as HtmlFormattingAttribute;

        return htmlFormattingAttribute == null ? HtmlFormattingOptions.None : htmlFormattingAttribute.Options;
    }
}

HTML

Finally, the Html class provides shortcut methods for creating elements in a fluent way:

public static class Html
{
    public static HtmlParagraph p(params object[] content)
    {
        return Create<HtmlParagraph, object>(content);
    }

    public static HtmlSpan span(params object[] content)
    {
        return Create<HtmlSpan, object>(content);
    }

    public static HtmlTableRow tr(params HtmlTableCell[] content)
    {
        return Create<HtmlTableRow, HtmlTableCell>(content);
    }

    public static HtmlTableHeaderRow tr(params HtmlTableHeaderCell[] content)
    {
        return Create<HtmlTableHeaderRow, HtmlTableHeaderCell>(content);
    }

    public static HtmlTableHeaderCell th(params object[] content)
    {
        return Create<HtmlTableHeaderCell, object>(content);
    }

    public static HtmlTableCell td(params object[] content)
    {
        return Create<HtmlTableCell, object>(content);
    }

    public static HtmlTableHead thead(params HtmlTableHeaderRow[] content)
    {
        return Create<HtmlTableHead, HtmlTableHeaderRow>(content);
    }

    public static HtmlTableBody tbody(params HtmlTableRow[] content)
    {
        return Create<HtmlTableBody, HtmlTableRow>(content);
    }

    public static HtmlTableFoot tfoot(params HtmlTableRow[] content)
    {
        return Create<HtmlTableFoot, HtmlTableRow>(content);
    }

    public static HtmlTable table(params IHtmlTableElement[] content)
    {
        return Create<HtmlTable, IHtmlTableElement>(content);
    }

    public static HtmlTableColumnGroupElement colgroup(params HtmlTableColumnElement[] content)
    {
        return Create<HtmlTableColumnGroupElement, HtmlTableColumnElement>(content);
    }

    public static HtmlTableColumnElement col()
    {
        return Create<HtmlTableColumnElement, object>(new object[] { });
    }

    private static T Create<T, TContent>(params TContent[] content)
        where T : HtmlElement<TContent>, new()
        where TContent : class
    {
        return new T().AddRange(content) as T;
    }
}

Usage

And this is how I use it:

Paragraph:

Html.p(
    "foo ",
    Html.span("bar").css("color: #ff0011;"),
    " baz"
).css("font-style: bold;").ToString().Dump();

Result:

<p style="font-style: bold;">foo <span style="color: #ff0011;">bar</span> baz</p>

Table:

Html.table(
    Html.colgroup(Html.col().css("background-color: red;")).attr("span", "4"),
    Html.thead(Html.tr(Html.th("foo"), Html.th("bar"))),
    Html.tbody(Html.tr(Html.td("foo"), Html.td("bar"))),
    Html.tfoot(Html.tr(Html.td("foo"), Html.td("bar")))
).ToString().Dump();

Result:

<table>
    <colgroup span="4">
        <col style="background-color: red;">
    </colgroup>
    <thead>
        <tr>
            <th>foo</th>
            <th>bar</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>foo</td>
            <td>bar</td>
        </tr>
    </tbody>
    <tfoot>
        <tr>
            <td>foo</td>
            <td>bar</td>
        </tr>
    </tfoot>
</table>

or a table with a loop (I'm going to create a lambda for this):

var body = Html.tbody();
for (int i = 0; i < 3; i++)
{
    body.Add(Html.tr(Html.td(i), Html.td(i*i)));
}

Html.table(
    Html.thead(Html.tr(Html.th("n"), Html.th("n*n"))),
    body
).ToString().Dump();

Result:

<table>
    <thead>
        <tr>
            <th>n</th>
            <th>n*n</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>0</td>
            <td>0</td>
        </tr>
        <tr>
            <td>1</td>
            <td>1</td>
        </tr>
        <tr>
            <td>2</td>
            <td>4</td>
        </tr>
    </tbody>
</table>
\$\endgroup\$
  • \$\begingroup\$ Do you have an example of a rendering? I'd be curious to see what it looks like. \$\endgroup\$ – syb0rg Jul 28 '16 at 13:10
  • \$\begingroup\$ @syb0rg at the bottom of the question there actually are three examples ;-) \$\endgroup\$ – t3chb0t Jul 28 '16 at 13:26
  • 1
    \$\begingroup\$ Ah, my bad. A quick read-through of your question led me to believe that there would be an interface generated. \$\endgroup\$ – syb0rg Jul 28 '16 at 13:29
  • \$\begingroup\$ After all I didn't like this version so I created a new one with dynamics. Here it is: Html rendering framework with dynamics \$\endgroup\$ – t3chb0t Aug 5 '16 at 3:55
  • \$\begingroup\$ I just looked at this and thought "use dynamics". I guess you should really make a self-answer, though, since you already figured everything out. \$\endgroup\$ – Hosch250 Aug 29 '16 at 4:59
1
\$\begingroup\$

After all I didn't like this version so I created a new one with dynamics. You can find it here: Html rendering framework with dynamics

The biggest flaw of this solution was the extensibility. It wasn't possible to add new tags outside the library because the user couldn't add new methods to the Html class.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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