A small and simple no guardrails html generator library for .NET 6+
Helium makes generating snippets of well-formed html easy.
Start by statically importing the class that gave this library its name so that your html snippets will look nice and clean.:
using static Cubist.Helium.He;Then use the static methods of the He class to build your snippets of html.
var doc = Document(
Head(
Title("Hello, World!")),
Body(
H1("Hello, World!")));and render it to a string.
var html = doc.ToString();Or write it to a TextWriter
doc.WriteTo(Console.Out);This will render the following html:
<!DOCTYPE html><html><head><title>Hello, World!</title></head><body><h1>Hello, World!</h1></body></html>Make it nicer to read using PrettyPrint()...
var html = doc.PrettyPrint();
// Or write it to a TextWriter
doc.PrettyPrintTo(Console.Out);Much better...
<!DOCTYPE html>
<html>
<head>
<title>Hello, World!</title>
</head>
<body>
<h1>Hello, World!</h1>
</body>
</html>Now witness the power of this fully equipped and operational library!
using static Cubist.Helium.He;
var items = new object[] { "some text", 1, DateTime.Now, };
var html = Ul(items.Select(item => Li(TemplateFor(item)))).PrettyPrint();
Node TemplateFor(object item) => item switch
{
int i => Div("A number: ", Data(i, "The number " + i)),
string s => Div("Some text: ", Q(Span(("style", "font-style: italic;"), s))),
DateTime dt => Div("A date-time: ", Time(dt, dt.ToString("M"))),
_ => Div("Some data: ", CData(item)),
};Renders to:
<ul>
<li>
<div>Some text: <q><span style="font-style: italic;">some text</span></q></div>
</li>
<li>
<div>A number: <data value="1">The number 1</data></div>
</li>
<li>
<div>A date-time: <time datetime="2023-03-06 17:19:28Z">6 maart</time></div>
</li>
</ul>Most elements take a params array of objects as content, for example:
public static He Div(params object[] content) => new(Tags.Div) { content };The content objects are interpreted as follows:
- Any two-element tuple is interpreted as an HTML attribute. e.g.
Div(("tabindex", 1))renders as<div tabindex="1"></div> - Ordering of attributes relative to child elements does not matter,
Div(("tabindex", 1), "content")andDiv("content",("tabindex", 1))both render as<div tabindex="1">content</div> - The order of attributes relative to other attributes is kept.
Div(("class","highlight"), ("tabindex", 1))renders as<div class="highlight" tabindex="1"></div>andDiv(("tabindex", 1), ("class","highlight"))renders as<div tabindex="1" class="highlight"></div>
See He.Add(object? content) for the nitty-gritty details.
- Any two-element tuple is added to the element's attribute list as
(string, object?)tuple, converting the first element to a string if necessary. - Any string content is added as a child
Textnode, and written as-is to the output.Div("some text")becomes<div>some text</div>andDiv("<div></div>")will render as<div><div></div></div>. This is the no guardrails part... - Any content that is a .NET primitive type uses the
ToString()method of the content and is added as a childTextnode - Any content that implements
IEnumerablewill be added as a range of nodes. (This can be a mixed list of attribute tuples,Nodes, primitive types, etc...). - Any content that does not derive from
Nodeor is not a .NET primitive type uses theToString()of the content and is added as aCDatanode.
For more examples see Cubist.Helium.Examples.
Helium is designed to be used in a functional style. Because of that the html construction looks, dare I say, almost Lisp-like.
It is also designed so that code strongly resembles the html output in structure. This lowers the cognitive load of using this library.
For example:
Document(
Head(
MetaCharsetUtf8(),
MetaViewPort(),
MetaRobots(index: true, follow: false),
Link("icon", "favicon.ico"),
Link("stylesheet", "/css/main.css"),
Link("stylesheet", "/css/mobile.css", ("media", "screen and (max-width: 600px)")),
Style(Css("html", ("background", "white"))),
Script("module", "js/module.js")
),
Body(
/* more here */
)
);Another goal is to have a minimal set of dependencies, prefereably none. This makes it easy to quickly code up some html without needing a lot of infrastructure in place.
Because we live in the age of custom web elements, extensibility and using custom elements should be easy and not look out-of-place.
The `Components.TodoList() example shows how this works.
By creating static methods that return Node instances, and by using using static <Your custom component class>;
Use HttpUtility.HtmlAttributeEncode(...) and WebUtility.HtmlEncode(...) for that.
It's not hard to integrate encoding in the same way as the AttributeExtensions.SingleQuoted(...) and AttributeExtensions.NoQuotes(...) extensions.
using System.Net;
using System.Web;
using Cubist.Helium;
namespace My.Encoders;
public static class EncodingExtensions
{
public static HtmlEncoder HtmlEncoded(this string text)
=> new HtmlEncoder(text);
public static HtmlAttributeEncoder AttrEncoded(this string text)
=> new HtmlAttributeEncoder(text);
}
public sealed class HtmlEncoder : Node
{
public string Value { get; }
public HtmlEncoder(string value) => Value = value;
// HttpUtility.HtmlEncode calls WebUtility.HtmlEncode under the hood,
// so we use WebUtility.HtmlEncode directly.
public override void WriteTo(TextWriter w) => WebUtility.HtmlEncode(Value, w);
}
public sealed class HtmlAttributeEncoder : Node
{
public string Value { get; }
public HtmlAttributeEncoder(string value) => Value = value;
public override void WriteTo(TextWriter w) => HttpUtility.HtmlAttributeEncode(Value, w);
}using My.Encoders;
using static Cubist.Helium.He;
var html = Div(
("data-value", "<'&>".AttrEncoded()),
"A custom element like this: <todo-list>".HtmlEncoded()).ToString();output:
<div data-value="<'&>">A custom element like this: <todo-list></div>
If you want that, ASP.NET has you covered.
Nope, isn't a goal. Although it should be as fast as possible while still using
the TextWriter class as the output mechanism this library will never be the fastest.
Creating the element tree and then rendering it can generate quite a bit of work for the garbage collector. Lucky for this library the .NET garbage collector is excellent at cleaning up lots of small objects that don't live long.
Creating the element tree and then post-process that tree extensively will not be a supported use-case.
Bug reports and pull requests are encouraged!
Please read the above goals and non-goals to get a feel for the spirit of this library. This will help get your pull request accepted quickly ( as will new unit tests! )
Code that does not have the MIT license will not be accepted.
This seems to be all the rage these days, so here you go.
DBAA also known as Don't be a posterior orifice. We're all people here. Thank you!
