RSCG – RazorBlade
| name | RazorBlade |
| nuget | https://www.nuget.org/packages/RazorBlade/ |
| link | https://github.com/ltrzesniewski/RazorBlade |
| author | Lucas Trzesniewski |
Fast templating with Razor syntax
Do not forget to put into AdditionalFiles section of csproj file
This is how you can use RazorBlade .
The code that you start with is
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net7.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup> <ItemGroup> <PackageReference Include="RazorBlade" Version="0.4.3" PrivateAssets="all" ReferenceOutputAssembly="false" OutputItemType="Analyzer" /> </ItemGroup> <ItemGroup> <AdditionalFiles Include="PersonDisplay.cshtml" /> </ItemGroup> <PropertyGroup> <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> <CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GX</CompilerGeneratedFilesOutputPath> </PropertyGroup> </Project>
The code that you will use is
using RazorBladeDemo; Console.WriteLine("Hello,World!"); Person p = new(); p.FirstName= "Andrei"; p.LastName = "Ignat"; var template = new PersonDisplay(p); var result = template.Render(); Console.WriteLine(result);
namespace RazorBladeDemo;
public class Person
{
public string? FirstName { get; set; }
public string? LastName { get; set; }
public string FullName()
{
return FirstName + " "+LastName;
}
}
@using RazorBladeDemo; @inherits RazorBlade.HtmlTemplate<Person>; This is the @Model.FirstName @Model.LastName <br /> This should be full name of @Model.FullName()
The code that is generated is
// This file is part of the RazorBlade library.
#nullable enable
using System;
namespace RazorBlade.Support;
/// <summary>
/// Specifies that this constructor needs to be provided by the generated template class.
/// </summary>
[AttributeUsage(AttributeTargets.Constructor)]
internal sealed class TemplateConstructorAttribute : Attribute
{
}
/// <summary>
/// Specifies if a method should be used depending on the template being sync or async.
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
internal sealed class ConditionalOnAsyncAttribute : Attribute
{
/// <summary>
/// The message to display.
/// </summary>
public string? Message { get; set; }
/// <summary>
/// Marks a method as meant to be used in a sync or async template.
/// </summary>
/// <param name="async">True for methods meant to be used in async templates,and false for methods meant to be used for sync templates.</param>
public ConditionalOnAsyncAttribute(bool async)
{
}
}
// This file is part of the RazorBlade library.
#nullable enable
using System;
using System.Diagnostics.CodeAnalysis;
using System.Text;
namespace RazorBlade;
// ReSharper disable once RedundantDisableWarningComment
#pragma warning disable CA1822
/// <summary>
/// Utilities for HTML Razor templates.
/// </summary>
[SuppressMessage("ReSharper","MemberCanBeMadeStatic.Global")]
internal sealed class HtmlHelper
{
internal static HtmlHelper Instance { get; } = new();
/// <summary>
/// Returns markup that is not HTML encoded.
/// </summary>
/// <param name="value">The HTML markup.</param>
public HtmlString Raw(object? value)
=> new(value?.ToString());
/// <summary>
/// HTML-encodes the provided value.
/// </summary>
/// <param name="value">Value to HTML-encode.</param>
public string Encode(object? value)
{
var valueString = value?.ToString();
if (valueString is null or "")
return string.Empty;
#if NET6_0_OR_GREATER
var valueSpan = valueString.AsSpan();
var sb = new StringBuilder();
while (true)
{
var idx = valueSpan.IndexOfAny("&<>\"\'");
if (idx < 0)
break;
if (idx != 0)
sb.Append(valueSpan[..idx]);
sb.Append(valueSpan[idx] switch
{
'&' => "&amp;",
'<' => "&lt;",
'>' => "&gt;",
'"' => "&quot;",
'\'' => "&#x27;",
var c => c.ToString() // Won't happen
});
valueSpan = valueSpan[(idx + 1)..];
}
if (valueSpan.Length != 0)
sb.Append(valueSpan);
return sb.ToString();
#else
return valueString.Replace("&","&amp;")
.Replace("<","&lt;")
.Replace(">","&gt;")
.Replace("\"","&quot;")
.Replace("\'","&#x27;");
#endif
}
}
// This file is part of the RazorBlade library.
#nullable enable
using System.IO;
namespace RazorBlade;
/// <summary>
/// Represents an HTML-encoded string that should not be encoded again.
/// </summary>
internal sealed class HtmlString : IEncodedContent
{
private readonly string _value;
/// <summary>
/// Creates a HTML-encoded string.
/// </summary>
public HtmlString(string? value)
=> _value = value ?? string.Empty;
/// <inheritdoc />
public override string ToString()
=> _value;
void IEncodedContent.WriteTo(TextWriter textWriter)
=> textWriter.Write(_value);
}
// This file is part of the RazorBlade library.
#nullable enable
using System;
using System.Diagnostics.CodeAnalysis;
using RazorBlade.Support;
namespace RazorBlade;
/// <summary>
/// Base class for HTML templates.
/// </summary>
/// <remarks>
/// Special HTML characters will be escaped.
/// </remarks>
internal abstract class HtmlTemplate : RazorTemplate
{
private AttributeInfo _currentAttribute;
// ReSharper disable once RedundantDisableWarningComment
#pragma warning disable CA1822
/// <inheritdoc cref="HtmlHelper"/>
[SuppressMessage("ReSharper","MemberCanBeMadeStatic.Global")]
protected HtmlHelper Html => HtmlHelper.Instance;
/// <inheritdoc cref="HtmlHelper.Raw"/>
[SuppressMessage("ReSharper","MemberCanBeMadeStatic.Global")]
protected HtmlString Raw(object? value)
=> HtmlHelper.Instance.Raw(value);
#pragma warning restore CA1822
/// <inheritdoc />
protected override void Write(object? value)
{
if (value is IEncodedContent encodedContent)
{
encodedContent.WriteTo(Output);
return;
}
var valueString = value?.ToString();
if (valueString is null or "")
return;
#if NET6_0_OR_GREATER
var valueSpan = valueString.AsSpan();
while (true)
{
var idx = valueSpan.IndexOfAny("&<>\"\'");
if (idx < 0)
break;
if (idx != 0)
Output.Write(valueSpan[..idx]);
Output.Write(valueSpan[idx] switch
{
'&' => "&amp;",
'<' => "&lt;",
'>' => "&gt;",
'"' => "&quot;",
'\'' => "&#x27;",
var c => c.ToString() // Won't happen
});
valueSpan = valueSpan[(idx + 1)..];
}
if (valueSpan.Length != 0)
Output.Write(valueSpan);
#else
Output.Write(
valueString.Replace("&","&amp;")
.Replace("<","&lt;")
.Replace(">","&gt;")
.Replace("\"","&quot;")
.Replace("\'","&#x27;")
);
#endif
}
/// <inheritdoc />
protected override void BeginWriteAttribute(string name,string prefix,int prefixOffset,string suffix,int suffixOffset,int attributeValuesCount)
{
_currentAttribute = new(name,prefix,suffix,attributeValuesCount);
if (_currentAttribute.AttributeValuesCount != 1)
WriteLiteral(prefix);
}
/// <inheritdoc />
protected override void WriteAttributeValue(string prefix,int prefixOffset,object? value,int valueOffset,int valueLength,bool isLiteral)
{
// This implements the Razor semantics of ASP.NET (conditional attributes):
// When an attribute consists of a single value part (without whitespace): foo="@bar"
// - if bar evaluates to false or null,omit the attribute entirely
// - if bar evaluates to true,write the attribute name as the value: foo="foo"
// - otherwise,write the value of bar as usual
// When an attribute contains multiple value parts: class="foo @bar"
// - if bar evaluates to null,omit it and its whitespace prefix: class="foo"
// - otherwise,write the value of bar as usual (even if it evaluates to a boolean)
// Note that if an attribute name starts with "data-",these attribute-specific methods are not called,
// and Write is used instead,effectively bypassing these rules and always writing the attribute value as-is.
if (_currentAttribute.AttributeValuesCount == 1)
{
if (string.IsNullOrEmpty(prefix))
{
if (value is bool boolValue)
value = boolValue ? _currentAttribute.Name : null;
if (value is null)
{
_currentAttribute.Suppressed = true;
return;
}
}
WriteLiteral(_currentAttribute.Prefix);
}
if (value is not null)
{
WriteLiteral(prefix);
if (isLiteral)
WriteLiteral(value.ToString());
else
Write(value);
}
}
/// <inheritdoc />
protected override void EndWriteAttribute()
{
if (!_currentAttribute.Suppressed)
WriteLiteral(_currentAttribute.Suffix);
}
private struct AttributeInfo
{
public readonly string? Name;
public readonly string? Prefix;
public readonly string? Suffix;
public readonly int AttributeValuesCount;
public bool Suppressed;
public AttributeInfo(string name,string prefix,string suffix,int attributeValuesCount)
{
Name = name;
Prefix = prefix;
Suffix = suffix;
AttributeValuesCount = attributeValuesCount;
Suppressed = false;
}
}
}
/// <summary>
/// Base class for HTML templates with a model.
/// </summary>
/// <remarks>
/// Special HTML characters will be escaped.
/// </remarks>
/// <typeparam name="TModel">The model type.</typeparam>
internal abstract class HtmlTemplate<TModel> : HtmlTemplate
{
/// <summary>
/// The model for the template.
/// </summary>
public TModel Model { get; }
/// <summary>
/// Initializes a new instance of the template.
/// </summary>
/// <param name="model">The model for the template.</param>
[TemplateConstructor]
protected HtmlTemplate(TModel model)
{
Model = model;
}
/// <summary>
/// This constructor is provided for the designer only. Do not use.
/// </summary>
protected HtmlTemplate()
{
throw new NotSupportedException("Use the constructor overload that takes a model.");
}
}
// This file is part of the RazorBlade library.
#nullable enable
using System.IO;
namespace RazorBlade;
/// <summary>
/// Encoded content to we written to the output as-is.
/// </summary>
internal interface IEncodedContent
{
/// <summary>
/// Writes the content to the provided <see cref="TextWriter"/>.
/// </summary>
/// <param name="textWriter"><see cref="TextWriter"/> to write the content to.</param>
void WriteTo(TextWriter textWriter);
}
// This file is part of the RazorBlade library.
#nullable enable
using System;
using RazorBlade.Support;
namespace RazorBlade;
/// <summary>
/// Base class for plain text templates.
/// </summary>
/// <remarks>
/// Values will be written as-is,without escaping.
/// </remarks>
internal abstract class PlainTextTemplate : RazorTemplate
{
private string? _currentAttributeSuffix;
/// <inheritdoc />
protected override void Write(object? value)
{
if (value is IEncodedContent encodedContent)
encodedContent.WriteTo(Output);
else
Output.Write(value);
}
/// <inheritdoc />
protected override void BeginWriteAttribute(string name,string prefix,int prefixOffset,string suffix,int suffixOffset,int attributeValuesCount)
{
WriteLiteral(prefix);
_currentAttributeSuffix = suffix;
}
/// <inheritdoc />
protected override void WriteAttributeValue(string prefix,int prefixOffset,object? value,int valueOffset,int valueLength,bool isLiteral)
{
WriteLiteral(prefix);
if (isLiteral)
WriteLiteral(value?.ToString());
else
Write(value);
}
/// <inheritdoc />
protected override void EndWriteAttribute()
{
WriteLiteral(_currentAttributeSuffix);
_currentAttributeSuffix = null;
}
}
/// <summary>
/// Base class for plain text templates with a model.
/// </summary>
/// <remarks>
/// Values will be written as-is,without escaping.
/// </remarks>
/// <typeparam name="TModel">The model type.</typeparam>
internal abstract class PlainTextTemplate<TModel> : PlainTextTemplate
{
/// <summary>
/// The model for the template.
/// </summary>
public TModel Model { get; }
/// <summary>
/// Initializes a new instance of the template.
/// </summary>
/// <param name="model">The model for the template.</param>
[TemplateConstructor]
protected PlainTextTemplate(TModel model)
{
Model = model;
}
/// <summary>
/// This constructor is provided for the designer only. Do not use.
/// </summary>
protected PlainTextTemplate()
{
throw new NotSupportedException("Use the constructor overload that takes a model.");
}
}
// This file is part of the RazorBlade library.
#nullable enable
using System.ComponentModel;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using RazorBlade.Support;
namespace RazorBlade;
/// <summary>
/// Base class for Razor templates.
/// </summary>
internal abstract class RazorTemplate : IEncodedContent
{
/// <summary>
/// The <see cref="TextWriter"/> which receives the output.
/// </summary>
protected TextWriter Output { get; set; } = new StreamWriter(Stream.Null);
/// <summary>
/// The cancellation token.
/// </summary>
protected CancellationToken CancellationToken { get; private set; }
/// <summary>
/// Renders the template synchronously and returns the result as a string.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <remarks>
/// Use this only if the template does not use <c>@async</c> directives.
/// </remarks>
[ConditionalOnAsync(false,Message = $"The generated template is async. Use {nameof(RenderAsync)} instead.")]
public string Render(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var renderTask = RenderAsync(cancellationToken);
if (renderTask.IsCompleted)
return renderTask.Result;
return Task.Run(async () => await renderTask.ConfigureAwait(false),CancellationToken.None).GetAwaiter().GetResult();
}
/// <summary>
/// Renders the template synchronously to the given <see cref="TextWriter"/>.
/// </summary>
/// <param name="textWriter">The <see cref="TextWriter"/> to write to.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <remarks>
/// Use this only if the template does not use <c>@async</c> directives.
/// </remarks>
[ConditionalOnAsync(false,Message = $"The generated template is async. Use {nameof(RenderAsync)} instead.")]
public void Render(TextWriter textWriter,CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var renderTask = RenderAsync(textWriter,cancellationToken);
if (renderTask.IsCompleted)
{
renderTask.GetAwaiter().GetResult();
return;
}
Task.Run(async () => await renderTask.ConfigureAwait(false),CancellationToken.None).GetAwaiter().GetResult();
}
/// <summary>
/// Renders the template asynchronously and returns the result as a string.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <remarks>
/// Use this if the template uses <c>@async</c> directives.
/// </remarks>
public async Task<string> RenderAsync(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var output = new StringWriter();
await RenderAsync(output,cancellationToken).ConfigureAwait(false);
return output.ToString();
}
/// <summary>
/// Renders the template asynchronously to the given <see cref="TextWriter"/>.
/// </summary>
/// <param name="textWriter">The <see cref="TextWriter"/> to write to.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <remarks>
/// Use this if the template uses <c>@async</c> directives.
/// </remarks>
public async Task RenderAsync(TextWriter textWriter,CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var previousState = (Output,CancellationToken);
try
{
Output = textWriter;
CancellationToken = cancellationToken;
await ExecuteAsync().ConfigureAwait(false);
}
finally
{
(Output,CancellationToken) = previousState;
}
}
/// <summary>
/// Executes the template and appends the result to <see cref="Output"/>.
/// </summary>
protected virtual Task ExecuteAsync()
=> Task.CompletedTask; // The IDE complains when this method is abstract :(
/// <summary>
/// Writes a literal value to the output.
/// </summary>
/// <param name="value">The value to write.</param>
protected void WriteLiteral(string? value)
=> Output.Write(value);
/// <summary>
/// Write a value to the output.
/// </summary>
/// <param name="value">The value to write.</param>
protected abstract void Write(object? value);
/// <summary>
/// Write already encoded content to the output.
/// </summary>
/// <param name="content">The template to render.</param>
protected void Write(IEncodedContent? content)
=> content?.WriteTo(Output);
/// <summary>
/// Begins writing an attribute.
/// </summary>
/// <param name="name">The attribute name.</param>
/// <param name="prefix">The attribute prefix,which is the text from the whitespace preceding the attribute name to the quote before the attribute value.</param>
/// <param name="prefixOffset">The prefix offset in the Razor file.</param>
/// <param name="suffix">The suffix,consisting of the end quote.</param>
/// <param name="suffixOffset">The suffix offset in the Razor file.</param>
/// <param name="attributeValuesCount">The count of attribute value parts,which is the count of subsequent <see cref="WriteAttributeValue"/> calls.</param>
[EditorBrowsable(EditorBrowsableState.Never)]
protected abstract void BeginWriteAttribute(string name,string prefix,int prefixOffset,string suffix,int suffixOffset,int attributeValuesCount);
/// <summary>
/// Writes part of an attribute value.
/// </summary>
/// <param name="prefix">The value prefix,consisting of the whitespace preceding the value.</param>
/// <param name="prefixOffset">The prefix offset in the Razor file.</param>
/// <param name="value">The value to write.</param>
/// <param name="valueOffset">The value offset in the Razor file.</param>
/// <param name="valueLength">The value length in the Razor file.</param>
/// <param name="isLiteral">Whether the value is a literal.</param>
[EditorBrowsable(EditorBrowsableState.Never)]
protected abstract void WriteAttributeValue(string prefix,int prefixOffset,object? value,int valueOffset,int valueLength,bool isLiteral);
/// <summary>
/// Ends writing an attribute.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
protected abstract void EndWriteAttribute();
void IEncodedContent.WriteTo(TextWriter textWriter)
=> Render(textWriter,CancellationToken.None);
}
#pragma checksum "C:\test\RSCG_Examples\v2\rscg_examples\RazorBlade\src\RazorBladeDemo\PersonDisplay.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "0ee9a5bcc623252570e9d97efdeb7e3c5a8d6350"
// <auto-generated/>
#pragma warning disable 1591
namespace RazorBladeDemo
{
#line hidden
#nullable restore
#line 1 "C:\test\RSCG_Examples\v2\rscg_examples\RazorBlade\src\RazorBladeDemo\PersonDisplay.cshtml"
using RazorBladeDemo;
#line default
#line hidden
#nullable disable
#nullable restore
internal partial class PersonDisplay : RazorBlade.HtmlTemplate<Person>
#nullable disable
{
#pragma warning disable 1998
protected async override global::System.Threading.Tasks.Task ExecuteAsync()
{
WriteLiteral("\r\nThis is the ");
#nullable restore
#line (4,14)-(4,29) 6 "C:\test\RSCG_Examples\v2\rscg_examples\RazorBlade\src\RazorBladeDemo\PersonDisplay.cshtml"
Write(Model.FirstName);
#line default
#line hidden
#nullable disable
WriteLiteral(" ");
#nullable restore
#line (4,31)-(4,45) 6 "C:\test\RSCG_Examples\v2\rscg_examples\RazorBlade\src\RazorBladeDemo\PersonDisplay.cshtml"
Write(Model.LastName);
#line default
#line hidden
#nullable disable
WriteLiteral("\r\n\r\n<br />\r\n\r\nThis should be full name of ");
#nullable restore
#line (8,30)-(8,46) 6 "C:\test\RSCG_Examples\v2\rscg_examples\RazorBlade\src\RazorBladeDemo\PersonDisplay.cshtml"
Write(Model.FullName());
#line default
#line hidden
#nullable disable
}
#pragma warning restore 1998
}
}
#pragma warning restore 1591
// <auto-generated/>
#nullable restore
namespace RazorBladeDemo
{
partial class PersonDisplay
{
/// <inheritdoc cref="M:RazorBlade.HtmlTemplate`1.#ctor(`0)" />
public PersonDisplay(global::RazorBladeDemo.Person model)
: base(model)
{
}
}
}
Code and pdf at
https://ignatandrei.github.io/RSCG_Examples/v2/docs/RazorBlade
Leave a Reply