Vrmac.HtmlCompiler
1.0.0
dotnet add package Vrmac.HtmlCompiler --version 1.0.0
NuGet\Install-Package Vrmac.HtmlCompiler -Version 1.0.0
<PackageReference Include="Vrmac.HtmlCompiler" Version="1.0.0" />
<PackageVersion Include="Vrmac.HtmlCompiler" Version="1.0.0" />
<PackageReference Include="Vrmac.HtmlCompiler" />
paket add Vrmac.HtmlCompiler --version 1.0.0
#r "nuget: Vrmac.HtmlCompiler, 1.0.0"
#:package Vrmac.HtmlCompiler@1.0.0
#addin nuget:?package=Vrmac.HtmlCompiler&version=1.0.0
#tool nuget:?package=Vrmac.HtmlCompiler&version=1.0.0
Vrmac.HtmlCompiler
This project builds an incremental code generator to compile HTML templates into C# partial types.
The template generator validates template syntax.
When the syntax is invalid, you should get compilation errors.
The template generator doesn’t make any assumptions about the content of the input files except the elements it parses for i.e. directives, placeholders, and blocks. You will see no errors if the HTML is invalid. The placeholders are just inserting dynamic pieces of UTF-8 text, and they work anywhere in the HTML including comments, JavaScript, CSS, SVG or XML.
Template Syntax
The templates are *.html files included in your project.
The build action for them should be “C# analyzer additional file”
Here's how to set the correct build action recursively
for all files in the Html subfolder of the project:
<AdditionalFiles Include="Html\**\*.html" />
The code generator doesn’t bother parsing HTML DOM.
Instead, it treats input files as a sequence of lines.
Directives
Each template may start with the following directives. All of them are optional.
@! indent <int32> - prepend specified count of '\t' indentation characters at the start of each line.
@! route <string> - when specified, the code generator will emit partial struct implementing a dynamic web page with the template.
@! route <string> cached - when specified, the code generator will emit partial struct implementing a cached i.e. rarely changing web page with the template.
When the @! route directive is missing, the code generator will emit partial class instead of partial struct.
This is useful for templates which aren’t web pages: layout templates, page fragments, dynamic HTML documents in desktop apps.
@! singleLine - when specified, the code generator will filter out newlines while compiling the template.
Markup
The code generator only supports two things: placeholders, and conditional blocks.
- Placeholders are identifiers prefixed with
@, for example@mailAddress - Conditional blocks are sections of lines between
@{and@}markers.
A conditional block is opened with a line which starts with@{followed with an identifier.
A conditional block is closed with a line which starts with@}
No other stuff is allowed in these lines.
Example
Example of a page template which contains all of the above.
@! route /signin
@! indent 1
<h1>Sign In Form</h1>
@{ errorPanel
<div class="fail">@failMessage</div>
@}
<form method="post" action="/signin">
<table>
<tr>
<td><label for="email">Email address:</label></td>
<td><input type="email" id="email" name="email" required autocomplete="email" maxlength="254" value="@mailAddress"></td>
</tr>
<tr>
<td><label for="password">Password:</label></td>
<td><input type="password" id="password" name="password" required autocomplete="new-password" minlength="8"></td>
</tr>
<tr>
<td colspan="2"><button type="submit">Submit</button></td>
</tr>
</table>
<input type="hidden" name="csrf" value="@csrfToken">
</form>
C# code generated from that template:
#nullable enable
// This source file is generated by a tool
using Vrmac.Html;
namespace Html;
/// <summary>Source data for the <c>Html/SignInForm.html</c> template</summary>
interface iSignInForm
{
/// <summary>Write content of the <c>@failMessage</c> placeholder</summary>
void failMessage( Stream stream );
/// <summary>Write content of the <c>@mailAddress</c> placeholder</summary>
void mailAddress( Stream stream );
/// <summary>Write content of the <c>@csrfToken</c> placeholder</summary>
void csrfToken( Stream stream );
/// <summary>Return false to skip the <c>errorPanel</c> conditional block of the template</summary>
bool errorPanel();
}
/// <summary>Partial structure implementing the <c>Html/SignInForm.html</c> dynamic web page</summary>
partial struct SignInForm: iDynamicPage
{
/// <summary>The route pattern</summary>
internal const string routePattern = "/signin";
/// <summary>Handler for GET method</summary>
static Task renderPage( HttpContext context, object? state = null ) => DynamicPage.render<SignInForm>( context, state );
/// <summary>Handler for GET method</summary>
static Task renderSecure( HttpContext context, object? state = null ) => DynamicPage.renderSecure<SignInForm>( context, state );
/// <summary>Write HTML into the stream of UTF-8 bytes</summary>
static void applyTemplate<T>( Stream response, in T model ) where T: iSignInForm, allows ref struct
{
response.Write( s_text0 );
if( model.errorPanel() )
{
response.Write( s_text1 );
model.failMessage( response );
response.Write( s_text2 );
}
response.Write( s_text3 );
model.mailAddress( response );
response.Write( s_text4 );
model.csrfToken( response );
response.Write( s_text5 );
}
static ReadOnlySpan<byte> s_text0 =>
"\t<h1>Sign In Form</h1>\n"u8;
static ReadOnlySpan<byte> s_text1 =>
"\t<div class=\"fail\">"u8;
static ReadOnlySpan<byte> s_text2 =>
"</div>\n"u8;
static ReadOnlySpan<byte> s_text3 =>
"\t<form method=\"post\" action=\"/signin\">\n\t\t<table>\n\t\t\t<tr>\n\t\t\t\t<td><label for=\"email\">Email address:</label></td>\n\t\t\t\t<td><input type=\"email\" id=\"email\" name=\"email\" required autocomplete=\"email\" maxlength=\"254\" value=\""u8;
static ReadOnlySpan<byte> s_text4 =>
"\"></td>\n\t\t\t</tr>\n\t\t\t<tr>\n\t\t\t\t<td><label for=\"password\">Password:</label></td>\n\t\t\t\t<td><input type=\"password\" id=\"password\" name=\"password\" required autocomplete=\"new-password\" minlength=\"8\"></td>\n\t\t\t</tr>\n\t\t\t<tr>\n\t\t\t\t<td colspan=\"2\"><button type=\"submit\">Submit</button></td>\n\t\t\t</tr>\n\t\t</table>\n\t\t<input type=\"hidden\" name=\"csrf\" value=\""u8;
static ReadOnlySpan<byte> s_text5 =>
"\">\n\t</form>"u8;
}
As you see, the compiler has consolidated all placeholders into an interface.
Each placeholder became a method of the model interface which takes a stream of bytes, and is expected to write something there.
Placeholder names don’t have to be unique; the following template is good:
<div>
<input type="radio" id="@id" name="xxx" value="@id"@isChecked>
<label for="@id" class="radio">@label</label>
</div>
Compiles into the following C# interface:
interface iRadioFragment
{
void id( Stream stream );
void isChecked( Stream stream );
void label( Stream stream );
}
Conditional blocks are translated into methods of the model interface
which take no arguments and return bool.
When the method returns false, the generated applyTemplate function
skips the entire content of the block, including both static and dynamic pieces.
Integration
If you don’t use @! route directives in your templates,
implement the generated interface in a class or struct,
and call applyTemplate generated method passing the destination stream and model instance.
In this case you don’t need to reference the accompanying Vrmac.Html.dll runtime library.
The generated codes for these partial classes only use C# standard library,
you can use it in any .NET project which target .NET 9 or newer.
If some of your templates are pages,
the generated codes will depend on the Vrmac.Html.dll library,
and transitively on the entire asp.net core runtime.
You can only use these pages in asp.net projects.
See DynamicPageRender.sendPage<T> and DynamicPageRender.renderCached<T>
generic methods in that library for the integration.
Known Issues
The template parser does not support escaping.
This makes it impossible for the static parts of the template to contain an email address or other strings that include the '@' character.
To work around this, use a placeholder in the template and write the string from C# code.
For example, define a @contactEmail placeholder and implement it with the following method:
public void contactEmail( Stream stream ) => stream.write( "recipient@mycompany.com"u8 );
If you want a proper fix not just a workaround, consider forking this library and implementing escaping.
Personally, I would probably unescape \\ into \, and \@ into @.
The templates are parsed by the SourceParser class, which you would need to modify.
Random Tips
The magic strings for the @!route directive may contain placeholders, e.g. @!route /something/{id}
The template parser doesn’t do anything special to these patterns,
but if you pass the const string routePattern string
into WebApplication.MapGet or similar, the asp.net core runtime will handle them correctly.
You have probably noticed the template engine neglected to implement loops.
The reason for that, loops would inflate the complexity beyond reasonable,
also make the codes very hard to debug.
There’s an easy workaround.
Make another template for the item, instantiate items in a loop.
Here’s an example:
// The base interface automatically generated from VersionHistoryEntry.html
readonly struct Entry: iVersionHistoryEntry
{
readonly ReadOnlyMemory<byte> version;
readonly ReadOnlyMemory<byte> publishDate;
public Entry( in Metadata meta )
{
version = meta.version;
publishDate = meta.date;
}
public void ver( Stream stream ) => stream.write( version );
public void date( Stream stream ) => stream.write( publishDate );
}
// The base interface automatically generated from VersionHistory.html
readonly struct EntriesList: iVersionHistory
{
readonly ReleaseState releases;
public EntriesList( ReleaseState releases ) => this.releases = releases;
public void historyList( Stream stream )
{
foreach( Metadata meta in releases.enumeratePublic() )
{
// That applyTemplate function is generated from VersionHistoryEntry.html
VersionHistoryEntry.applyTemplate( stream, new Entry( meta ) );
// Template parser trims trailing whitespaces from HTML, including newline. Insert one manually.
stream.newLine();
}
}
}
Learn more about Target Frameworks and .NET Standard.
-
.NETStandard 2.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0 | 85 | 9/30/2026 |