Init
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
using System;
|
||||
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 = $@"// <auto-generated/>
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace {opt.RootNamespace};
|
||||
|
||||
public readonly struct HtmxRenderContext
|
||||
{{
|
||||
public readonly IBufferWriter<byte> Writer;
|
||||
public readonly int Depth;
|
||||
private const int MaxDepth = 512;
|
||||
|
||||
public HtmxRenderContext(IBufferWriter<byte> 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<byte> writer, IHtmxComponent component)
|
||||
{{
|
||||
component.Render(new HtmxRenderContext(writer));
|
||||
}}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void WriteUtf8(this IBufferWriter<byte> writer, ReadOnlySpan<byte> data)
|
||||
{{
|
||||
var buffer = writer.GetSpan(data.Length);
|
||||
data.CopyTo(buffer);
|
||||
writer.Advance(data.Length);
|
||||
}}
|
||||
|
||||
public static void WriteHtmxBody(this HttpContext context, IHtmxComponent component)
|
||||
{{
|
||||
context.Response.ContentType = ""text/html; charset=utf-8"";
|
||||
var writerContext = new HtmxRenderContext(context.Response.BodyWriter);
|
||||
component.Render(writerContext);
|
||||
}}
|
||||
}}";
|
||||
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("// <auto-generated/>");
|
||||
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)
|
||||
{
|
||||
// Passing context allows user to write direct spans OR nested components
|
||||
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<byte> _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
|
||||
// context.Next() enforces the depth limit before calling the user implementation
|
||||
sb.AppendLine($" Render{segment.Value}(context.Next());");
|
||||
}
|
||||
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine("}");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static ImmutableArray<TemplateSegment> ParseTemplate(string content)
|
||||
{
|
||||
var builder = ImmutableArray.CreateBuilder<TemplateSegment>();
|
||||
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<TemplateSegment> Segments);
|
||||
}
|
||||
}
|
||||
|
||||
namespace System.Runtime.CompilerServices
|
||||
{
|
||||
// Required for records/init properties in netstandard2.0
|
||||
internal static class IsExternalInit { }
|
||||
}
|
||||
Reference in New Issue
Block a user