Most C# source generator examples I have encountered included more advanced features like augmenting existing classes with generated code. There are much simpler scenarios where they are useful though. For instance, just avoiding typing repetitive code. For the full source code, consult my GitHub repo here.
Imagine writing a number of overloads where only the function name changes. In this case, I want to create HTML tags as strings using a function. The example is a set of overloads for the anchor ('a') tag:
public static Node a(params Node[] nodes) =>
element(nameof(a), Array.Empty<IAttribute>(), nodes);
public static Node a(params IAttribute[] attributes) =>
element(nameof(a), attributes, Array.Empty<Node>());
public static Node a(IEnumerable<IAttribute> attributes, params Node[] children) =>
element(nameof(a), attributes, children);
Now I want the have the same overloads for all HTML tags. That's a lot of repetition. To create a source generator for this all I basically have to do is create a text template for the above. Now I am not going to describe the nuts and bolts of C# source generators because there is plenty of documentation. So I will only show the source of the generator here:
[Generator]
public class ElementsGenerator : ISourceGenerator
{
private string GetElementMethods(string tagName) =>
$@"
public static Node {tagName}(params Node[] nodes) =>
element(nameof({tagName}), Array.Empty<IAttribute>(), nodes);
public static Node {tagName}(params IAttribute[] attributes) =>
element(nameof({tagName}), attributes, Array.Empty<Node>());
public static Node {tagName}(IEnumerable<IAttribute> attributes, params Node[] children) =>
element(nameof({tagName}), attributes, children);
";
public void Execute(GeneratorExecutionContext context)
{
string[] tagNames = new string[]
{
"a", "abbr", "acronym", "address", "applet", "area", "article", "aside", "audio", "b", "@base", "basefont",
"bdi", "bdo", "big", "br", "button", "canvas", "caption", "center", "cite", "code", "col", "colgroup", "content",
"data", "datalist", "dd", "del", "details", "dfn", "dialog", "dir", "div", "dl", "dt", "em", "embed", "fieldset", "figcaption",
"figure", "font", "footer", "form", "frame", "fraeset", "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hr", "html",
"i", "iframe", "img", "input", "ïns", "kbd", "label", "legend", "li", "link", "main", "map", "mark", "menu", "menuitem",
"meta", "meter", "nav", "noembed", "noframes", "noscript", "@object", "ol", "optgroup", "option", "output", "p", "param",
"picture", "pre", "progress", "q", "rb", "rp", "rt", "rtc", "ruby", "s", "samp", "script", "section", "select", "shadow", "slot",
"small", "source", "span", "strike", "strong", "style", "sub", "summary", "sup", "svg", "table", "tbody", "td", "template", "text",
"textarea", "tfoot", "th", "thead", "time", "title", "tr", "track", "tt", "u", "ul", "var", "video", "wbr"
};
var methodsStringBuilder = new StringBuilder();
for (int i = 0; i < tagNames.Length; i++)
{
methodsStringBuilder.Append(GetElementMethods(tagNames[i]));
methodsStringBuilder.Append(Environment.NewLine);
}
string classText =
$@"
using System;
namespace Radix.Components.Html;
public static partial class Elements
{{
public static Node text(string text) => new Text(text);
public static Concat concat(params Node[] nodes) => new(nodes);
public static Element element(Name name, IEnumerable<IAttribute> attributes, params Node[] children) => new(name, attributes, children);
{methodsStringBuilder}
}}
";
// Register the source
context.AddSource("Elements", classText);
}
public void Initialize(GeneratorInitializationContext context) { }
}
As you can see, the only method that is implemented is the execute method and the only thing it does is adding generated code to the compilation of the project where the generator project is included.
Comments
Post a Comment