diff --git a/Fluid.Benchmarks/BaseBenchmarks.cs b/Fluid.Benchmarks/BaseBenchmarks.cs index 4c3b00bb..0ee74f07 100644 --- a/Fluid.Benchmarks/BaseBenchmarks.cs +++ b/Fluid.Benchmarks/BaseBenchmarks.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.IO; -using System.Reflection; namespace Fluid.Benchmarks { diff --git a/Fluid.Generator/Fluid.Generator.csproj b/Fluid.Generator/Fluid.Generator.csproj new file mode 100644 index 00000000..de77a15b --- /dev/null +++ b/Fluid.Generator/Fluid.Generator.csproj @@ -0,0 +1,31 @@ + + + + netstandard2.0 + enable + enable + latest + true + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Fluid.Generator/LiquidGenerator.cs b/Fluid.Generator/LiquidGenerator.cs new file mode 100644 index 00000000..0f9fbc53 --- /dev/null +++ b/Fluid.Generator/LiquidGenerator.cs @@ -0,0 +1,216 @@ +using Fluid; +using Fluid.Compilation; +using Fluid.Parser; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Diagnostics; +using System.Reflection; +using System.Text; + +#nullable enable + +[Generator] +public class LiquidGenerator : ISourceGenerator +{ + public void Execute(GeneratorExecutionContext context) + { + Debug.WriteLine("Execute code generator"); + +#if DEBUG + //if (!Debugger.IsAttached) + //{ + // Debugger.Launch(); + //} +#endif + + var receiver = context.SyntaxReceiver as LiquidRenderReceiver; + if (receiver is null) return; + + StringBuilder sb = new(); + sb.AppendLine($@"// Source Generated at {DateTimeOffset.Now:R} +using System; +using System.Buffers; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Fluid; +using Fluid.Values; + +public class LiquidTemplates +{{ +"); + sb.AppendLine("// Seeking for additional files..."); + foreach (var file in context.AdditionalFiles) + { + sb.AppendLine($"// Processing '{file.Path}'"); + + var isLiquidTemplate = string.Equals( + context.AnalyzerConfigOptions.GetOptions(file).TryGetAdditionalFileMetadataValue("IsLiquidTemplate"), + "true", + StringComparison.OrdinalIgnoreCase + ); + + if (!isLiquidTemplate) + continue; + + var content = file.GetText(context.CancellationToken)?.ToString(); + + if (string.IsNullOrWhiteSpace(content)) + continue; + + ProcessFile(context, file.Path, content!, receiver, sb); + } + + sb.AppendLine(@" +} // end +"); + context.AddSource("LiquidTemplates", sb.ToString()); + } + + private static void ProcessFile(in GeneratorExecutionContext context, string filePath, string content, LiquidRenderReceiver? receiver, StringBuilder builder) + { + // Generate class name from file name + var templateName = SanitizeIdentifier(Path.GetFileNameWithoutExtension(filePath)); + + // Always output non-specific writer + builder.AppendLine(@$" + public static void Render{templateName}(T model, TextWriter writer) + {{ + // Emitted as an initial call site for the template, + // when actually called a specific call site for the exact model will be additionally be emitted. + throw new NotImplementedException(); + }} +"); + + List? invocations = null; + if (receiver?.Invocations?.TryGetValue(templateName, out invocations) ?? false) + { + Debug.Assert(invocations != null); + + foreach (var invocation in invocations!) + { + var arguments = invocation.ArgumentList.Arguments; + if (arguments.Count != 2) continue; + + var semanticModel = context.Compilation.GetSemanticModel(invocation.SyntaxTree); + var modelType = semanticModel.GetTypeInfo(arguments[0].Expression).Type; + + var parser = new FluidParser(); + + //var template = parser.Parse(content) as FluidTemplate; + + //var compiler = new AstCompiler(); + + //compiler.RenderTemplate(modelType!, templateName, template!, builder); + } + } + } + + private static string SanitizeIdentifier(string symbolName) + { + if (string.IsNullOrWhiteSpace(symbolName)) return string.Empty; + + var sb = new StringBuilder(symbolName.Length); + if (!char.IsLetter(symbolName[0])) + { + // Must start with a letter or an underscore + sb.Append('_'); + } + + var capitalize = true; + foreach (var ch in symbolName) + { + if (!char.IsLetterOrDigit(ch)) + { + capitalize = true; + continue; + } + + sb.Append(capitalize ? char.ToUpper(ch) : ch); + capitalize = false; + } + + return sb.ToString(); + } + + public void Initialize(GeneratorInitializationContext context) + => context.RegisterForSyntaxNotifications(() => new LiquidRenderReceiver()); + + class LiquidRenderReceiver : ISyntaxReceiver + { + public Dictionary>? Invocations { get; private set; } + + public void OnVisitSyntaxNode(SyntaxNode node) + { + if (node.IsKind(SyntaxKind.InvocationExpression) && + node is InvocationExpressionSyntax invocation) + { + var expression = invocation.Expression; + if (expression is MemberAccessExpressionSyntax member) + { + var isLiquid = false; + string? template = null; + if (member.IsKind(SyntaxKind.SimpleMemberAccessExpression)) + { + foreach (SyntaxNode child in expression.ChildNodes()) + { + if (!isLiquid) + { + if (child is IdentifierNameSyntax classIdent) + { + var valueText = classIdent.Identifier.ValueText; + // Console.Error.WriteLine(valueText); + if (classIdent.Identifier.ValueText == "LiquidTemplates") + { + isLiquid = true; + continue; + } + else + { + break; + } + } + else + { + break; + } + } + + if (child is IdentifierNameSyntax methodIdent) + { + var valueText = methodIdent.Identifier.ValueText; + if (valueText.IndexOf("Render", StringComparison.Ordinal) == 0) + { + template = valueText.Substring("Render".Length); + } + break; + } + } + + if (isLiquid && template is not null) + { + if ((Invocations ??= new()).TryGetValue(template, out var list)) + { + list.Add(invocation); + } + else + { + Invocations.Add(template, new() { invocation }); + } + } + } + } + } + } + } +} + +internal static class SourceGeneratorExtensions +{ + public static string? TryGetValue(this AnalyzerConfigOptions options, string key) => + options.TryGetValue(key, out var value) ? value : null; + + public static string? TryGetAdditionalFileMetadataValue(this AnalyzerConfigOptions options, string propertyName) => + options.TryGetValue($"build_metadata.AdditionalFiles.{propertyName}"); +} \ No newline at end of file diff --git a/Fluid.MvcViewEngine/Fluid.MvcViewEngine.csproj b/Fluid.MvcViewEngine/Fluid.MvcViewEngine.csproj index d6b2bb0a..a62198e8 100644 --- a/Fluid.MvcViewEngine/Fluid.MvcViewEngine.csproj +++ b/Fluid.MvcViewEngine/Fluid.MvcViewEngine.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1;net5.0;net6.0;net7.0 + netcoreapp3.1 latest logo_64x64.png true diff --git a/Fluid.SandBox/Fluid.SandBox.csproj b/Fluid.SandBox/Fluid.SandBox.csproj new file mode 100644 index 00000000..6f6b8bd0 --- /dev/null +++ b/Fluid.SandBox/Fluid.SandBox.csproj @@ -0,0 +1,15 @@ + + + + Exe + net7.0 + enable + enable + 11 + + + + + + + diff --git a/Fluid.SandBox/Program.cs b/Fluid.SandBox/Program.cs new file mode 100644 index 00000000..2455b573 --- /dev/null +++ b/Fluid.SandBox/Program.cs @@ -0,0 +1,161 @@ +using Fluid; +using System.Diagnostics; +using System.Text; +using System.Text.Encodings.Web; + +var source = @" +{%- for f in fortunes -%} + +{%- endfor -%} +
{{ f.Id }}{{ f.Message }}
"; + +var templates = new Dictionary(); + +var sw = Stopwatch.StartNew(); + +var parser = new FluidParser(); + +var sb = new StringBuilder(2048); +var writer = new StringWriter(sb); + +var fortunes = new Fortune[] { + new (0, "Additional fortune added at request time."), + new (1, "fortune: No such file or directory"), + new (2, "A computer scientist is someone who fixes things that aren't broken."), + new (3, "After enough decimal places, nobody gives a damn."), + new (4, "A bad random number generator: 1, 1, 1, 1, 1, 4.33e+67, 1, 1, 1"), + new (5, "A computer program does what you tell it to do, not what you want it to do."), + new (6, "Emacs is a nice operating system, but I prefer UNIX. — Tom Christaensen"), + new (7, "Any program that runs right is obsolete."), + new (8, "A list is only as strong as its weakest link. — Donald Knuth"), + new (9, "Feature: A bug with seniority."), + new (10, "Computers make very fast, very accurate mistakes."), + new (11, "