diff --git a/src/Analyzers/DotVVM.Analyzers.SourceGenerators/DotVVM.Analyzers.SourceGenerators.csproj b/src/Analyzers/DotVVM.Analyzers.SourceGenerators/DotVVM.Analyzers.SourceGenerators.csproj new file mode 100644 index 000000000..6ed5d391a --- /dev/null +++ b/src/Analyzers/DotVVM.Analyzers.SourceGenerators/DotVVM.Analyzers.SourceGenerators.csproj @@ -0,0 +1,28 @@ + + + + netstandard2.0 + + + + + + + + + + + + + + + + + + all + + + all + + + diff --git a/src/Analyzers/DotVVM.Analyzers.SourceGenerators/DothtmlTokenizerErrors.cs b/src/Analyzers/DotVVM.Analyzers.SourceGenerators/DothtmlTokenizerErrors.cs new file mode 100644 index 000000000..7db88a4e9 --- /dev/null +++ b/src/Analyzers/DotVVM.Analyzers.SourceGenerators/DothtmlTokenizerErrors.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace DotVVM.Framework.Resources +{ + public class DothtmlTokenizerErrors + { + internal static string BindingNotClosed; + internal static string DoubleBraceBindingNotClosed; + internal static string BindingInvalidFormat; + internal static string AttributeValueNotClosed; + internal static string MissingAttributeValue; + internal static string MissingTagName; + internal static string MissingTagPrefix; + internal static string XmlProcessingInstructionNotClosed; + internal static string DoctypeNotClosed; + internal static string CommentNotClosed; + internal static string CDataNotClosed; + internal static string TagNotClosed; + internal static string InvalidCharactersInTag; + internal static string TagNameExpected; + internal static string DirectiveValueExpected; + internal static string DirectiveNameExpected; + } +} diff --git a/src/Analyzers/DotVVM.Analyzers.SourceGenerators/DotvvmRoutesSourceGenerator.cs b/src/Analyzers/DotVVM.Analyzers.SourceGenerators/DotvvmRoutesSourceGenerator.cs new file mode 100644 index 000000000..4951af3cb --- /dev/null +++ b/src/Analyzers/DotVVM.Analyzers.SourceGenerators/DotvvmRoutesSourceGenerator.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using DotVVM.Framework.Compilation.Parser.Dothtml.Tokenizer; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace DotVVM.Analyzers.SourceGenerators +{ + [Generator] + public class DotvvmRoutesSourceGenerator : ISourceGenerator + { + public void Initialize(GeneratorInitializationContext context) + { + + } + + public void Execute(GeneratorExecutionContext context) + { + if (!context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.projectdir", out var projectDirectory)) + { + throw new Exception("Unable to find project directory."); + } + projectDirectory = Path.GetFullPath(projectDirectory); + + if (!context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.rootnamespace", out var rootNamespace)) + { + throw new Exception("Unable to find root namespace"); + } + + var routes = new List<(string routeName, string url, string virtualPath)>(); + + var potentialMarkupFiles = context.AdditionalFiles.Where(f => f.Path.EndsWith(".dothtml", StringComparison.OrdinalIgnoreCase)); + foreach (var file in potentialMarkupFiles) + { + try + { + var content = file.GetText(context.CancellationToken); + if (TryExtractRouteDirective(content) is { } url) + { + var virtualPath = GetRelativePath(projectDirectory, file.Path).Replace("\\", "/"); + + var routeName = virtualPath.Replace("/", "_"); + routeName = routeName.Substring(0, routeName.LastIndexOf(".")); + + if (routeName.StartsWith("Views_", StringComparison.OrdinalIgnoreCase)) + { + routeName = routeName.Substring("Views_".Length); + } + + routes.Add((routeName, url, virtualPath)); + } + } + catch (Exception ex) + { + context.ReportDiagnostic(Diagnostic.Create("DG0001", "DotVVM routing", ex.Message, DiagnosticSeverity.Warning, DiagnosticSeverity.Warning, true, 1)); + } + } + + var result = $$""" + using DotVVM.Framework.Routing; + + namespace {{rootNamespace}} + { + public static class DotvvmRoutes + { + {{ string.Join("\n ", routes.Select(r => $"public const string {r.routeName} = nameof({r.routeName});")) }} + + public static void RegisterRoutes(DotvvmRouteTable routes) { + {{ string.Join("\n ", routes.Select(r => $"routes.Add(\"{r.routeName}\", \"{r.url}\", \"{r.virtualPath}\");")) }} + } + } + } + """; + context.AddSource("DotvvmRoutes.cs", result); + } + + private string GetRelativePath(string projectDirectory, string path) + { + path = Path.GetFullPath(path); + if (!path.StartsWith(projectDirectory)) + { + throw new Exception($"File {path} is outside the project directory!"); + } + return path.Substring(projectDirectory.Length).TrimStart('/', '\\'); + } + + private static string? TryExtractRouteDirective(SourceText content) + { + var tokenizer = new DothtmlTokenizer(); + tokenizer.Tokenize(content.ToString()); + + for (var i = 0; i < tokenizer.Tokens.Count - 3; i++) + { + var token = tokenizer.Tokens[i]; + if (token.Type == DothtmlTokenType.DirectiveStart) + { + i++; + token = tokenizer.Tokens[i]; + + if (token is { Type: DothtmlTokenType.DirectiveName, Text: "route" }) + { + i += 2; + token = tokenizer.Tokens[i]; + + return token.Text; + } + } + } + + return null; + } + } +} + +namespace DotVVM.Framework.Utils +{ + public static class StringUtils + { + public static string DotvvmInternString(this string s) => s; + public static string DotvvmInternString(this char s) => s.ToString(); + public static string DotvvmInternString(this ReadOnlySpan s) => s.ToString(); + } +} diff --git a/src/DotVVM.sln b/src/DotVVM.sln index 1b49797aa..626564782 100644 --- a/src/DotVVM.sln +++ b/src/DotVVM.sln @@ -131,6 +131,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotVVM.Adapters.WebForms.Te EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotVVM.Adapters.WebForms", "Adapters\WebForms\DotVVM.Adapters.WebForms.csproj", "{25442AA8-7E4D-47EC-8CCB-F9E2B45EB998}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotVVM.Analyzers.SourceGenerators", "Analyzers\DotVVM.Analyzers.SourceGenerators\DotVVM.Analyzers.SourceGenerators.csproj", "{556CF9DA-CD82-4DFF-90E2-BD0698359DF4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -717,6 +719,18 @@ Global {25442AA8-7E4D-47EC-8CCB-F9E2B45EB998}.Release|x64.Build.0 = Release|Any CPU {25442AA8-7E4D-47EC-8CCB-F9E2B45EB998}.Release|x86.ActiveCfg = Release|Any CPU {25442AA8-7E4D-47EC-8CCB-F9E2B45EB998}.Release|x86.Build.0 = Release|Any CPU + {556CF9DA-CD82-4DFF-90E2-BD0698359DF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {556CF9DA-CD82-4DFF-90E2-BD0698359DF4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {556CF9DA-CD82-4DFF-90E2-BD0698359DF4}.Debug|x64.ActiveCfg = Debug|Any CPU + {556CF9DA-CD82-4DFF-90E2-BD0698359DF4}.Debug|x64.Build.0 = Debug|Any CPU + {556CF9DA-CD82-4DFF-90E2-BD0698359DF4}.Debug|x86.ActiveCfg = Debug|Any CPU + {556CF9DA-CD82-4DFF-90E2-BD0698359DF4}.Debug|x86.Build.0 = Debug|Any CPU + {556CF9DA-CD82-4DFF-90E2-BD0698359DF4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {556CF9DA-CD82-4DFF-90E2-BD0698359DF4}.Release|Any CPU.Build.0 = Release|Any CPU + {556CF9DA-CD82-4DFF-90E2-BD0698359DF4}.Release|x64.ActiveCfg = Release|Any CPU + {556CF9DA-CD82-4DFF-90E2-BD0698359DF4}.Release|x64.Build.0 = Release|Any CPU + {556CF9DA-CD82-4DFF-90E2-BD0698359DF4}.Release|x86.ActiveCfg = Release|Any CPU + {556CF9DA-CD82-4DFF-90E2-BD0698359DF4}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -776,6 +790,7 @@ Global {05A3401A-C541-4F7C-AAD8-02A23648CD27} = {42513853-3772-46D2-94C2-965101E2406D} {A6A8451E-99D8-4296-BBA9-69E1E289270A} = {05A3401A-C541-4F7C-AAD8-02A23648CD27} {25442AA8-7E4D-47EC-8CCB-F9E2B45EB998} = {42513853-3772-46D2-94C2-965101E2406D} + {556CF9DA-CD82-4DFF-90E2-BD0698359DF4} = {D10C02E0-DB0B-49F2-8D2E-BA3B5ED4654C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {61F8A195-365E-47B1-A6F2-CD3534E918F8} diff --git a/src/Samples/Common/DotVVM.Samples.Common.csproj b/src/Samples/Common/DotVVM.Samples.Common.csproj index a5cb54ad4..2ca37006c 100644 --- a/src/Samples/Common/DotVVM.Samples.Common.csproj +++ b/src/Samples/Common/DotVVM.Samples.Common.csproj @@ -2,6 +2,7 @@ $(DefaultTargetFrameworks) DotVVM.Samples.Common + true @@ -53,6 +54,12 @@ + + + + diff --git a/src/Samples/Common/Views/Default.dothtml b/src/Samples/Common/Views/Default.dothtml index 1d0f17565..ad5f67047 100644 --- a/src/Samples/Common/Views/Default.dothtml +++ b/src/Samples/Common/Views/Default.dothtml @@ -1,6 +1,6 @@ @viewModel DotVVM.Samples.BasicSamples.ViewModels.DefaultViewModel, DotVVM.Samples.Common @masterPage Views/Samples.dotmaster - +@route test
  • {{value: RouteName}}