|
| 1 | +using System.Collections.Frozen; |
| 2 | +using System.Collections.Generic; |
| 3 | +using System.Linq; |
| 4 | +using System.Text; |
| 5 | +using Microsoft.CodeAnalysis; |
| 6 | + |
| 7 | +namespace Bunit.Blazor; |
| 8 | + |
| 9 | +[Generator] |
| 10 | +public class EventDispatcherExtensionGenerator : IIncrementalGenerator |
| 11 | +{ |
| 12 | + private static readonly Dictionary<string, string> EventNameOverrides = new() |
| 13 | + { |
| 14 | + ["ondblclick"] = "DoubleClick", |
| 15 | + ["onmousewheel"] = "MouseWheel" |
| 16 | + }; |
| 17 | + |
| 18 | + private static readonly FrozenSet<string> WordBoundaries = |
| 19 | + [ |
| 20 | + "key", "mouse", "pointer", "touch", "drag", "drop", "focus", "blur", |
| 21 | + "click", "dbl", "context", "menu", "copy", "cut", "paste", |
| 22 | + "down", "up", "over", "out", "move", "enter", "leave", "start", "end", |
| 23 | + "cancel", "change", "input", "wheel", "got", "lost", "capture", |
| 24 | + "in", "before", "after", "load", "time", "abort", "progress", "error", |
| 25 | + "activate", "deactivate", "ended", "full", "screen", "data", "metadata", |
| 26 | + "lock", "ready", "state", "scroll", "toggle", "close", "seeking", "seeked", |
| 27 | + "loaded", "duration", "emptied", "stalled", "suspend", "volume", "waiting", |
| 28 | + "play", "playing", "pause", "press", "rate", "stop", "cue", "can", "through", "update" |
| 29 | + ]; |
| 30 | + |
| 31 | + public void Initialize(IncrementalGeneratorInitializationContext context) |
| 32 | + { |
| 33 | + var compilationProvider = context.CompilationProvider; |
| 34 | + |
| 35 | + context.RegisterSourceOutput(compilationProvider, GenerateEventDispatchExtensions); |
| 36 | + } |
| 37 | + |
| 38 | + private static void GenerateEventDispatchExtensions(SourceProductionContext context, Compilation compilation) |
| 39 | + { |
| 40 | + var eventHandlerAttributeSymbol = compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Components.EventHandlerAttribute"); |
| 41 | + if (eventHandlerAttributeSymbol is null) |
| 42 | + { |
| 43 | + return; |
| 44 | + } |
| 45 | + |
| 46 | + var eventHandlersSymbol = compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Components.Web.EventHandlers"); |
| 47 | + if (eventHandlersSymbol is null) |
| 48 | + { |
| 49 | + return; |
| 50 | + } |
| 51 | + |
| 52 | + var eventMappings = new Dictionary<string, List<(string EventName, string MethodName)>>(); |
| 53 | + |
| 54 | + foreach (var attribute in eventHandlersSymbol.GetAttributes()) |
| 55 | + { |
| 56 | + if (!SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, eventHandlerAttributeSymbol)) |
| 57 | + continue; |
| 58 | + |
| 59 | + if (attribute.ConstructorArguments.Length < 2) |
| 60 | + continue; |
| 61 | + |
| 62 | + var eventName = attribute.ConstructorArguments[0].Value?.ToString(); |
| 63 | + var eventArgsType = attribute.ConstructorArguments[1].Value as INamedTypeSymbol; |
| 64 | + |
| 65 | + if (string.IsNullOrEmpty(eventName) || eventArgsType == null) |
| 66 | + continue; |
| 67 | + |
| 68 | + var eventArgsTypeName = eventArgsType.Name; |
| 69 | + |
| 70 | + var methodName = GenerateMethodNameFromEventName(eventName); |
| 71 | + |
| 72 | + if (!eventMappings.ContainsKey(eventArgsTypeName)) |
| 73 | + { |
| 74 | + eventMappings[eventArgsTypeName] = []; |
| 75 | + } |
| 76 | + |
| 77 | + eventMappings[eventArgsTypeName].Add((eventName, methodName)); |
| 78 | + } |
| 79 | + |
| 80 | + if (eventMappings.Count == 0) |
| 81 | + { |
| 82 | + return; |
| 83 | + } |
| 84 | + |
| 85 | + var sourceBuilder = new StringBuilder(8000); |
| 86 | + sourceBuilder.AppendLine("#nullable enable"); |
| 87 | + sourceBuilder.AppendLine("using AngleSharp.Dom;"); |
| 88 | + sourceBuilder.AppendLine("using Microsoft.AspNetCore.Components.Web;"); |
| 89 | + sourceBuilder.AppendLine("using System.Threading.Tasks;"); |
| 90 | + sourceBuilder.AppendLine(); |
| 91 | + sourceBuilder.AppendLine("namespace Bunit;"); |
| 92 | + sourceBuilder.AppendLine(); |
| 93 | + |
| 94 | + sourceBuilder.AppendLine("/// <summary>"); |
| 95 | + sourceBuilder.AppendLine("/// Event dispatch helper extension methods."); |
| 96 | + sourceBuilder.AppendLine("/// </summary>"); |
| 97 | + sourceBuilder.AppendLine("public static partial class EventHandlerDispatchExtensions"); |
| 98 | + sourceBuilder.AppendLine("{"); |
| 99 | + |
| 100 | + foreach (var kvp in eventMappings.OrderBy(x => x.Key)) |
| 101 | + { |
| 102 | + GenerateExtensionsForEventArgsType(sourceBuilder, kvp.Key, kvp.Value); |
| 103 | + } |
| 104 | + |
| 105 | + sourceBuilder.AppendLine("}"); |
| 106 | + |
| 107 | + context.AddSource("EventHandlerDispatchExtensions.g.cs", sourceBuilder.ToString()); |
| 108 | + } |
| 109 | + |
| 110 | + private static string GenerateMethodNameFromEventName(string eventName) |
| 111 | + { |
| 112 | + if (EventNameOverrides.TryGetValue(eventName, out var overrideName)) |
| 113 | + { |
| 114 | + return overrideName; |
| 115 | + } |
| 116 | + |
| 117 | + if (eventName.StartsWith("on")) |
| 118 | + { |
| 119 | + eventName = eventName[2..]; |
| 120 | + } |
| 121 | + |
| 122 | + if (eventName.Length == 0) |
| 123 | + { |
| 124 | + return eventName; |
| 125 | + } |
| 126 | + |
| 127 | + var result = new StringBuilder(); |
| 128 | + |
| 129 | + var i = 0; |
| 130 | + while (i < eventName.Length) |
| 131 | + { |
| 132 | + var foundWord = false; |
| 133 | + |
| 134 | + foreach (var word in WordBoundaries.OrderByDescending(w => w.Length)) |
| 135 | + { |
| 136 | + if (i + word.Length <= eventName.Length && |
| 137 | + eventName.Substring(i, word.Length).Equals(word, System.StringComparison.OrdinalIgnoreCase)) |
| 138 | + { |
| 139 | + result.Append(char.ToUpper(word[0])); |
| 140 | + result.Append(word[1..].ToLower()); |
| 141 | + i += word.Length; |
| 142 | + foundWord = true; |
| 143 | + break; |
| 144 | + } |
| 145 | + } |
| 146 | + |
| 147 | + if (!foundWord) |
| 148 | + { |
| 149 | + result.Append(i == 0 ? char.ToUpper(eventName[i]) : eventName[i]); |
| 150 | + i++; |
| 151 | + } |
| 152 | + } |
| 153 | + |
| 154 | + return result.ToString(); |
| 155 | + } |
| 156 | + |
| 157 | + private static void GenerateExtensionsForEventArgsType(StringBuilder sourceBuilder, string eventArgsType, List<(string EventName, string MethodName)> events) |
| 158 | + { |
| 159 | + sourceBuilder.AppendLine($"\t// {eventArgsType} events"); |
| 160 | + |
| 161 | + foreach (var (eventName, methodName) in events) |
| 162 | + { |
| 163 | + var qualifiedEventArgsType = eventArgsType == "ErrorEventArgs" |
| 164 | + ? "Microsoft.AspNetCore.Components.Web.ErrorEventArgs" |
| 165 | + : eventArgsType; |
| 166 | + |
| 167 | + if (methodName == "Click") |
| 168 | + { |
| 169 | + GenerateAsyncEventMethodWithDefaultArgs(sourceBuilder, methodName, eventName, qualifiedEventArgsType, "new MouseEventArgs() { Detail = 1 }"); |
| 170 | + } |
| 171 | + else if (methodName == "DoubleClick") |
| 172 | + { |
| 173 | + GenerateAsyncEventMethodWithDefaultArgs(sourceBuilder, methodName, eventName, qualifiedEventArgsType, "new MouseEventArgs() { Detail = 2 }"); |
| 174 | + } |
| 175 | + else |
| 176 | + { |
| 177 | + GenerateAsyncEventMethodWithDefaultArgs(sourceBuilder, methodName, eventName, qualifiedEventArgsType, $"new {qualifiedEventArgsType}()"); |
| 178 | + } |
| 179 | + } |
| 180 | + |
| 181 | + sourceBuilder.AppendLine(); |
| 182 | + } |
| 183 | + |
| 184 | + private static void GenerateAsyncEventMethodWithDefaultArgs(StringBuilder sourceBuilder, string methodName, string eventName, string eventArgsType, string defaultArgs) |
| 185 | + { |
| 186 | + sourceBuilder.AppendLine("\t/// <summary>"); |
| 187 | + sourceBuilder.AppendLine($"\t/// Raises the <c>@{eventName}</c> event on <paramref name=\"element\"/>, passing the provided <paramref name=\"eventArgs\"/>"); |
| 188 | + sourceBuilder.AppendLine($"\t/// to the event handler. If <paramref name=\"eventArgs\"/> is null, a new instance of {eventArgsType} will be created."); |
| 189 | + sourceBuilder.AppendLine("\t/// </summary>"); |
| 190 | + sourceBuilder.AppendLine("\t/// <param name=\"element\">The element to raise the event on.</param>"); |
| 191 | + sourceBuilder.AppendLine("\t/// <param name=\"eventArgs\">The event arguments to pass to the event handler.</param>"); |
| 192 | + sourceBuilder.AppendLine($"\tpublic static void {methodName}(this IElement element, {eventArgsType}? eventArgs = null) => _ = {methodName}Async(element, eventArgs);"); |
| 193 | + sourceBuilder.AppendLine(); |
| 194 | + |
| 195 | + sourceBuilder.AppendLine("\t/// <summary>"); |
| 196 | + sourceBuilder.AppendLine($"\t/// Raises the <c>@{eventName}</c> event on <paramref name=\"element\"/>, passing the provided <paramref name=\"eventArgs\"/>"); |
| 197 | + sourceBuilder.AppendLine($"\t/// to the event handler. If <paramref name=\"eventArgs\"/> is null, a new instance of {eventArgsType} will be created."); |
| 198 | + sourceBuilder.AppendLine("\t/// </summary>"); |
| 199 | + sourceBuilder.AppendLine("\t/// <param name=\"element\">The element to raise the event on.</param>"); |
| 200 | + sourceBuilder.AppendLine("\t/// <param name=\"eventArgs\">The event arguments to pass to the event handler.</param>"); |
| 201 | + sourceBuilder.AppendLine("\t/// <returns>A task that completes when the event handler is done.</returns>"); |
| 202 | + sourceBuilder.AppendLine($"\tpublic static Task {methodName}Async(this IElement element, {eventArgsType}? eventArgs = null) => element.TriggerEventAsync(\"{eventName}\", eventArgs ?? {defaultArgs});"); |
| 203 | + sourceBuilder.AppendLine(); |
| 204 | + |
| 205 | + sourceBuilder.AppendLine("\t/// <summary>"); |
| 206 | + sourceBuilder.AppendLine($"\t/// Raises the <c>@{eventName}</c> event on the element returned by <paramref name=\"elementTask\"/>, passing the provided <paramref name=\"eventArgs\"/>"); |
| 207 | + sourceBuilder.AppendLine($"\t/// to the event handler. If <paramref name=\"eventArgs\"/> is null, a new instance of {eventArgsType} will be created."); |
| 208 | + sourceBuilder.AppendLine("\t/// </summary>"); |
| 209 | + sourceBuilder.AppendLine("\t/// <param name=\"elementTask\">A task that returns the element to raise the element on.</param>"); |
| 210 | + sourceBuilder.AppendLine("\t/// <param name=\"eventArgs\">The event arguments to pass to the event handler.</param>"); |
| 211 | + sourceBuilder.AppendLine("\t/// <returns>A task that completes when the event handler is done.</returns>"); |
| 212 | + sourceBuilder.AppendLine($"\tpublic static async Task {methodName}Async(this Task<IElement> elementTask, {eventArgsType}? eventArgs = null)"); |
| 213 | + sourceBuilder.AppendLine("\t{"); |
| 214 | + sourceBuilder.AppendLine("\t\tvar element = await elementTask;"); |
| 215 | + sourceBuilder.AppendLine($"\t\tawait {methodName}Async(element, eventArgs);"); |
| 216 | + sourceBuilder.AppendLine("\t}"); |
| 217 | + sourceBuilder.AppendLine(); |
| 218 | + } |
| 219 | +} |
0 commit comments