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