Skip to content

HtmlWriter

by Alex Peck on August 20th, 2009

Today I needed to generate an HTML file. Having used XmlWriter, I wanted to follow a similar approach. In the beginning, I didn’t find HtmlTextWriter (largely because I searched for HtmlWriter on MSDN), so I wrote the class below. It uses a stack ensure you open and close matching tags and to properly indent the output. I used a TextWriter, so I could plug in a StringWriter to unit test. It’s easy to instantiate and write directly to a file with new HtmlWriter(new StreamWriter(path)).

I later tried to replace my HtmlWriter with System.Web.UI.HtmlTextWriter, but because it insists on doing some weird things (I was unable to fully control the linebreaks in the output, despite my best attempts), all my unit tests broke. If you don’t want to generate HTML which is also readable by a human then HtmlTextWriter is what you need, otherwise I offer an alternative.

/// <summary>
/// A writer that provides a fast, non-cached, forward-only means of generating HTML streams.
/// </summary>
public class HtmlWriter : IDisposable
{
    public HtmlWriter(TextWriter writer)
    {
        this.Writer = writer;
        this.TagStack = new Stack<string>();
    }
 
    ~HtmlWriter()
    {
        Dispose(false);
    }
 
    private TextWriter Writer
    { 
        get; 
        set; 
    }
 
    private Stack<string> TagStack
    {
        get;
        set;
    }
 
    public static string MakeAnchor(string href, string text)
    {
        return string.Format("<a href=\"{0}\">{1}</a>", href, text);
    }
 
    public static string MakeAttribute(Attribute attribute)
    {
        return string.Format("{0}=\"{1}\"", attribute.Name, attribute.Value);
    }
 
    public static string MakeAttributeSet(Collection<Attribute> attributes)
    {
        StringBuilder attributeBuilder = new StringBuilder();
 
        foreach (Attribute attribute in attributes)
        {
            attributeBuilder.Append(" " + MakeAttribute(attribute));
        }
 
        return attributeBuilder.ToString();
    }
 
    public void WriteLine(string line)
    {
        // indent according to the tag stack depth
        if (this.TagStack.Count > 0)
        {
            line = new string('\t', this.TagStack.Count) + line;
        }
 
        this.Writer.WriteLine(line);
    }
 
    public void OpenTag(string tag)
    {
        this.WriteLine(string.Format("<{0}>", tag));
        this.TagStack.Push(tag);
    }
 
    public void OpenTag(string tag, Attribute attribute)
    {
        this.WriteLine(string.Format("<{0} {1}>", tag, MakeAttribute(attribute)));
        this.TagStack.Push(tag);
    }
 
    public void OpenTag(string tag, Collection<Attribute> attributes)
    {
        this.WriteLine(string.Format("<{0}{1}>", tag, MakeAttributeSet(attributes)));
        this.TagStack.Push(tag);
    }
 
    public void CloseTag(string tag)
    {
        if (this.TagStack.Count == 0)
        {
            throw new InvalidOperationException("Attempt to close HTML '" + tag + "' when there are no open tags.");
        }
 
        if (this.TagStack.Peek() != tag)
        {
            throw new InvalidOperationException("Attempt to close HTML '" + tag + "' when '" + this.TagStack.Peek() + "' is currently open.");
        }
 
        this.TagStack.Pop();
        this.WriteLine(string.Format("</{0}>", tag));
    }
 
    public void Tag(string tag, string content)
    {
        this.WriteLine(string.Format("<{0}>{1}</{0}>", tag, content));
    }
 
    public void Tag(string tag, Attribute attribute, string content)
    {
        this.WriteLine(string.Format("<{0} {1}>{2}</{0}>", tag, MakeAttribute(attribute), content));
    }
 
    public void Tag(string tag, Collection<Attribute> attributes, string content)
    {
        this.WriteLine(string.Format("<{0} {1}>{2}</{0}>", tag, MakeAttributeSet(attributes), content));
    }
 
    public void WriteStartDocument()
    {
        this.WriteLine("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml11-strict.dtd\">");
    }
 
    public void WriteMeta()
    {
        this.WriteLine("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=iso-8859-1\" />");
    }
 
    public void RefStylesheet(string stylesheet)
    {
        string tag = "<link rel=\"stylesheet\" href=\"{0}\" type=\"text/css\" media=\"screen\" />";
        this.WriteLine(string.Format(tag, stylesheet));
    }
 
    public void RefJavascript(string javascript)
    {
        string tag = "<script src=\"{0}\" type=\"text/javascript\"></script>";
        this.WriteLine(string.Format(tag, javascript));
    }
 
    #region IDisposable Members
 
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
 
    protected virtual void Dispose(bool disposing)
    {
        this.Writer.Dispose();
    }
 
    #endregion
}

And here is the attribute struct, just for completeness.

/// <summary>
/// Html tag attribute as name value pair
/// </summary>
public struct Attribute
{
    public Attribute(string name, string value)
        : this()
    {
        Name = name;
        Value = value;
    }
 
    public string Name
    {
        get;
        private set;
    }
 
    public string Value
    {
        get;
        private set;
    }
}
No comments yet

Leave a Reply

Note: XHTML is allowed. Your email address will never be published.

Subscribe to this comment feed via RSS