Last active
September 4, 2024 18:45
-
-
Save deanebarker/c29ac45fac787a6a2718eae4a4665653 to your computer and use it in GitHub Desktop.
Handy C# class for easily and cleanly building HTML tags
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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("\"", """) + "\""); | |
} | |
// 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