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}}