using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Text;
namespace Htmx.SourceGenerator
{
[Generator]
public class HtmxGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var globalOptions = context.AnalyzerConfigOptionsProvider.Select((opt, _) =>
(RootNamespace: GetOption(opt.GlobalOptions, "build_property.RootNamespace", "HtmxTemplates"),
ProjectDir: GetOption(opt.GlobalOptions, "build_property.MSBuildProjectDirectory", "")));
context.RegisterSourceOutput(globalOptions, (spc, opt) =>
{
var infrastructureSource = $@"//
using System;
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace {opt.RootNamespace};
public readonly struct HtmxRenderContext
{{
public readonly IBufferWriter Writer;
public readonly int Depth;
private const int MaxDepth = 512;
public HtmxRenderContext(IBufferWriter writer, int depth = 0)
{{
if (depth > MaxDepth) throw new InvalidOperationException(""Template recursion limit exceeded."");
Writer = writer;
Depth = depth;
}}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public HtmxRenderContext Next() => new(Writer, Depth + 1);
}}
public interface IHtmxComponent
{{
void Render(HtmxRenderContext context);
}}
public static class HtmxGeneratedExtensions
{{
public static byte[] ToUtf8Bytes(this string str) => System.Text.Encoding.UTF8.GetBytes(str);
public static void WriteHtmx(this IBufferWriter writer, IHtmxComponent component)
{{
component.Render(new HtmxRenderContext(writer));
}}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void WriteUtf8(this IBufferWriter writer, ReadOnlySpan data)
{{
var buffer = writer.GetSpan(data.Length);
data.CopyTo(buffer);
writer.Advance(data.Length);
}}
public static HtmxResult WriteHtmxBody(this HttpContext context, IHtmxComponent component)
=> new HtmxResult(component);
}}";
spc.AddSource("HtmxInfrastructure.g.cs", SourceText.From(infrastructureSource, Encoding.UTF8));
});
var htmxFiles = context.AdditionalTextsProvider
.Where(file => file.Path.EndsWith(".htmx"))
.Combine(globalOptions);
var templateData = htmxFiles.Select((combined, ct) =>
{
var (file, opt) = combined;
var content = file.GetText(ct)?.ToString() ?? string.Empty;
string relativePath = Path.GetDirectoryName(file.Path) ?? "";
string subNamespace = "";
if (!string.IsNullOrEmpty(opt.ProjectDir) && relativePath.StartsWith(opt.ProjectDir))
{
subNamespace = relativePath.Substring(opt.ProjectDir.Length)
.Trim(Path.DirectorySeparatorChar)
.Replace(Path.DirectorySeparatorChar, '.');
}
string fullNamespace = string.IsNullOrEmpty(subNamespace)
? opt.RootNamespace
: $"{opt.RootNamespace}.{subNamespace}";
return new TemplateInfo(
Path.GetFileNameWithoutExtension(file.Path),
fullNamespace,
opt.RootNamespace,
ParseTemplate(content)
);
});
context.RegisterSourceOutput(templateData, (spc, data) =>
{
spc.AddSource($"{data.ClassName}.g.cs", GenerateSource(data));
});
}
private static string GetOption(AnalyzerConfigOptions options, string key, string fallback)
=> options.TryGetValue(key, out var value) ? value : fallback;
private static string GenerateSource(TemplateInfo data)
{
var sb = new StringBuilder();
sb.AppendLine("// ");
sb.AppendLine("using System;");
sb.AppendLine("using System.Buffers;");
sb.AppendLine("using System.Runtime.CompilerServices;");
if (data.Namespace != data.RootNamespace)
{
sb.AppendLine($"using {data.RootNamespace};");
}
sb.AppendLine($"namespace {data.Namespace};");
sb.AppendLine();
sb.AppendLine($"public abstract partial class {data.ClassName}Base : IHtmxComponent");
sb.AppendLine("{");
var variables = data.Segments.Where(s => !s.IsStatic).Select(s => s.Value).Distinct();
foreach (var v in variables)
{
sb.AppendLine($" protected abstract void Render{v}(HtmxRenderContext context);");
}
int staticCount = 0;
foreach (var s in data.Segments.Where(s => s.IsStatic))
{
var bytes = Encoding.UTF8.GetBytes(s.Value);
var byteString = string.Join(", ", bytes.Select(b => $"0x{b:X2}"));
sb.AppendLine($" private static ReadOnlySpan _part{staticCount++} => new byte[] {{ {byteString} }};");
}
sb.AppendLine();
sb.AppendLine(" public void Render(HtmxRenderContext context)");
sb.AppendLine(" {");
int sIdx = 0;
foreach (var segment in data.Segments)
{
if (segment.IsStatic)
sb.AppendLine($" context.Writer.WriteUtf8(_part{sIdx++});");
else
sb.AppendLine($" Render{segment.Value}(context.Next());");
}
sb.AppendLine(" }");
sb.AppendLine("}");
return sb.ToString();
}
private static ImmutableArray ParseTemplate(string content)
{
var builder = ImmutableArray.CreateBuilder();
var variableRegex = new Regex(@"\$\$([a-zA-Z0-9_]+)\$\$", RegexOptions.Compiled);
var matches = variableRegex.Matches(content);
int lastPos = 0;
foreach (Match match in matches)
{
if (match.Index > lastPos)
builder.Add(new(content.Substring(lastPos, match.Index - lastPos), true));
builder.Add(new(match.Groups[1].Value, false));
lastPos = match.Index + match.Length;
}
if (lastPos < content.Length)
builder.Add(new(content.Substring(lastPos), true));
return builder.ToImmutable();
}
private record TemplateSegment(string Value, bool IsStatic);
private record TemplateInfo(string ClassName, string Namespace, string RootNamespace, ImmutableArray Segments);
}
}
namespace System.Runtime.CompilerServices
{
internal static class IsExternalInit { }
}