Skip to content

Instantly share code, notes, and snippets.

@deanebarker
Last active September 4, 2024 18:45
Show Gist options
  • Save deanebarker/c29ac45fac787a6a2718eae4a4665653 to your computer and use it in GitHub Desktop.
Save deanebarker/c29ac45fac787a6a2718eae4a4665653 to your computer and use it in GitHub Desktop.
Handy C# class for easily and cleanly building HTML tags
// Doc here: https://deanebarker.net/tech/code/tag-builder/
public class Tag
{
private readonly string[] selfClosing = new[] { "img", "br", "hr", "input", "link", "meta" };
public List<Tag> Children = new();
public Tag(string input, string content, params Tag[] children)
{
Init(input, children);
SetContent(content);
}
public Tag(string input, string content, IEnumerable<Tag> children)
{
Init(input, children);
SetContent(content);
}
public Tag(string input, params Tag[] children)
{
Init(input, children);
}
public Tag(string input, IEnumerable<Tag> children)
{
Init(input, children);
}
private void Init(string input, IEnumerable<Tag> children = null)
{
// Input spec looks like this:
// div - a DIV tag
// "div#foo" - a DIV tag with an "id" of "foo"
// "div.bar" - a DIV tag with a "class" of "bar"
// "div#foo.bar" - a DIV tag with an "id" of "foo" and a "class" of "bar"
// Spaces are optional and will be removed (so, no dual classes....)
// Name always has to be first
input = (input ?? string.Empty).Replace(" ", string.Empty);
if (string.IsNullOrWhiteSpace(input))
{
return; // They can pass in nothing, in which case, this will be a DIV
}
// The name is anything up to the first punctuation
Name = input.Split(".#".ToCharArray()).First().ToLower();
SetAttribute("id", Regex.Match(input, @"[^#]*#([^\.]*)").Groups[1].Value);
AddClass(Regex.Match(input, @"[^\.]*\.([^\#]*)").Groups[1].Value);
if (children != null)
{
Children.AddRange(children);
}
}
public Dictionary<string, string> Attributes { get; set; } = new();
public Dictionary<string, string> Styles { get; set; } = new();
public List<string> Classes { get; set; } = new();
public string Name { get; set; } = "div";
private string content;
public string Content
{
get => content;
set
{
if (IsSelfClosing())
{
throw new InvalidOperationException("You cannot assign content to a self-closing tag.");
}
content = value;
}
}
public Tag SetAttribute(string key, string value, bool ignoreEmpty = true)
{
// Attribute names can only be letters or digits (digits, really?)
if (key.Any(c => !char.IsLetterOrDigit(c)))
{
throw new InvalidOperationException($"Invalid attribute name: \"{key}\"");
}
if (ignoreEmpty && string.IsNullOrWhiteSpace(value)) return this; // Value is empty
Attributes[key] = value;
return this;
}
public Tag SetContent(string content)
{
Content = content;
return this;
}
public Tag AddStyle(string key, string value)
{
if(string.IsNullOrWhiteSpace(key) || string.IsNullOrWhiteSpace(value)) return this;
Styles.Add(key, value);
return this;
}
public Tag AddClass(string name)
{
if(string.IsNullOrWhiteSpace(name)) return this;
Classes.Add(name);
return this;
}
public override string ToString()
{
if (Styles.Any())
{
SetAttribute("style", string.Join(';', Styles.Select(s => $"{s.Key}:{s.Value}")));
}
if (Classes.Any())
{
SetAttribute("class", string.Join(' ', Classes.Distinct()));
}
// Start of opening tag
var sb = new StringBuilder();
sb.Append('<');
sb.Append(Name);
// Attributes
foreach (var attr in Attributes)
{
sb.Append(" " + attr.Key + "=\"" + attr.Value.Replace("\"", "&quot;") + "\"");
}
// Self-closing tags
if (IsSelfClosing())
{
sb.Append(" />");
return sb.ToString();
}
// End of opening tag (for non-self-closing)
sb.Append('>');
sb.Append(Content);
// Render the children
// Tag contents will always come before child tags, that's just the way it is...
Children.ForEach(c => sb.Append(c));
// Closing tag
sb.Append("</");
sb.Append(Name);
sb.Append('>');
return sb.ToString();
}
private bool IsSelfClosing()
{
return selfClosing.Contains(Name);
}
// You can pass this to anything like a string...
public static implicit operator string(Tag t)
{
return t.ToString();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment