Add @page directive

Adds the @page directive and support for specifying routes in components
at compile time.

For now the route is required and must begin with a leading /.
This commit is contained in:
Ryan Nowak 2018-03-12 14:22:54 -07:00 committed by Steve Sanderson
parent 700c2203c6
commit 9549dccc54
9 changed files with 305 additions and 19 deletions

View File

@ -59,6 +59,11 @@ namespace Microsoft.AspNetCore.Blazor.Razor
public static readonly string ChildContent = nameof(ChildContent);
}
public static class RouteAttribute
{
public static readonly string FullTypeName = "Microsoft.AspNetCore.Blazor.Components.RouteAttribute";
}
public static class BindMethods
{
public static readonly string GetValue = "Microsoft.AspNetCore.Blazor.Components.BindMethods.GetValue";

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.IO;
using System.Linq;
using AngleSharp;
using Microsoft.AspNetCore.Razor.Language;
@ -62,26 +63,30 @@ namespace Microsoft.AspNetCore.Blazor.Razor
return RazorDiagnostic.Create(UnsupportedComplexContent, node.Source ?? SourceSpan.Undefined, attributeName, content);
}
private static SourceSpan? CalculateSourcePosition(
SourceSpan? razorTokenPosition,
TextPosition htmlNodePosition)
public static readonly RazorDiagnosticDescriptor PageDirective_CannotBeImported =
new RazorDiagnosticDescriptor(
"BL9987",
() => Resources.PageDirectiveCannotBeImported,
RazorDiagnosticSeverity.Error);
public static RazorDiagnostic CreatePageDirective_CannotBeImported(SourceSpan source)
{
if (razorTokenPosition.HasValue)
{
var razorPos = razorTokenPosition.Value;
return new SourceSpan(
razorPos.FilePath,
razorPos.AbsoluteIndex + htmlNodePosition.Position,
razorPos.LineIndex + htmlNodePosition.Line - 1,
htmlNodePosition.Line == 1
? razorPos.CharacterIndex + htmlNodePosition.Column - 1
: htmlNodePosition.Column - 1,
length: 1);
}
else
{
return null;
}
var fileName = Path.GetFileName(source.FilePath);
var diagnostic = RazorDiagnostic.Create(PageDirective_CannotBeImported, source, PageDirective.Directive.Directive, fileName);
return diagnostic;
}
public static readonly RazorDiagnosticDescriptor PageDirective_MustSpecifyRoute =
new RazorDiagnosticDescriptor(
"BL9988",
() => "The @page directive must specify a route template. The route template must be enclosed in quotes and begin with the '/' character.",
RazorDiagnosticSeverity.Error);
public static RazorDiagnostic CreatePageDirective_MustSpecifyRoute(SourceSpan? source)
{
var diagnostic = RazorDiagnostic.Create(PageDirective_MustSpecifyRoute, source ?? SourceSpan.Undefined);
return diagnostic;
}
}
}

View File

@ -55,6 +55,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
InheritsDirective.Register(builder);
InjectDirective.Register(builder);
LayoutDirective.Register(builder);
PageDirective.Register(builder);
builder.Features.Remove(builder.Features.OfType<IImportProjectFeature>().Single());
builder.Features.Add(new BlazorImportProjectFeature());

View File

@ -0,0 +1,44 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
namespace Microsoft.AspNetCore.Blazor.Razor
{
public class PageDirective
{
public static readonly DirectiveDescriptor Directive = DirectiveDescriptor.CreateDirective(
"page",
DirectiveKind.SingleLine,
builder =>
{
builder.AddStringToken(Resources.PageDirective_RouteToken_Name, Resources.PageDirective_RouteToken_Description);
builder.Usage = DirectiveUsage.FileScopedMultipleOccurring;
builder.Description = Resources.PageDirective_Description;
});
private PageDirective(string routeTemplate, IntermediateNode directiveNode)
{
RouteTemplate = routeTemplate;
DirectiveNode = directiveNode;
}
public string RouteTemplate { get; }
public IntermediateNode DirectiveNode { get; }
public static RazorProjectEngineBuilder Register(RazorProjectEngineBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
builder.AddDirective(Directive);
builder.Features.Add(new PageDirectivePass());
return builder;
}
}
}

View File

@ -0,0 +1,107 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Linq;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
namespace Microsoft.AspNetCore.Blazor.Razor
{
internal class PageDirectivePass : IntermediateNodePassBase, IRazorDirectiveClassifierPass
{
protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
{
if (codeDocument == null)
{
throw new ArgumentNullException(nameof(codeDocument));
}
if (documentNode == null)
{
throw new ArgumentNullException(nameof(documentNode));
}
var @namespace = documentNode.FindPrimaryNamespace();
var @class = documentNode.FindPrimaryClass();
if (@namespace == null || @class == null)
{
return;
}
var directives = documentNode.FindDirectiveReferences(PageDirective.Directive);
if (directives.Count == 0)
{
return;
}
// We don't allow @page directives in imports
for (var i = 0; i < directives.Count; i++)
{
var directive = directives[i];
if (directive.Node.IsImported())
{
directive.Node.Diagnostics.Add(BlazorDiagnosticFactory.CreatePageDirective_CannotBeImported(directive.Node.Source.Value));
}
}
// Insert the attributes 'on-top' of the class declaration, since classes don't directly support attributes.
var index = 0;
for (; index < @namespace.Children.Count; index++)
{
if (object.ReferenceEquals(@class, @namespace.Children[index]))
{
break;
}
}
for (var i = 0; i < directives.Count; i++)
{
var pageDirective = (DirectiveIntermediateNode)directives[i].Node;
// The parser also adds errors for invalid syntax, we just need to not crash.
var routeToken = pageDirective.Tokens.FirstOrDefault();
if (routeToken != null &&
routeToken.Content.Length >= 3 &&
routeToken.Content[0] == '\"' &&
routeToken.Content[1] == '/' &&
routeToken.Content[routeToken.Content.Length - 1] == '\"')
{
var template = routeToken.Content.Substring(1, routeToken.Content.Length - 2);
@namespace.Children.Insert(index++, new RouteAttributeExtensionNode(template));
}
else
{
pageDirective.Diagnostics.Add(BlazorDiagnosticFactory.CreatePageDirective_MustSpecifyRoute(pageDirective.Source));
}
}
}
private class RouteAttributeExtensionNode : ExtensionIntermediateNode
{
public RouteAttributeExtensionNode(string template)
{
Template = template;
}
public string Template { get; }
public override IntermediateNodeCollection Children => IntermediateNodeCollection.ReadOnly;
public override void Accept(IntermediateNodeVisitor visitor) => AcceptExtensionNode(this, visitor);
public override void WriteNode(CodeTarget target, CodeRenderingContext context)
{
context.CodeWriter.Write("[");
context.CodeWriter.Write(BlazorApi.RouteAttribute.FullTypeName);
context.CodeWriter.Write("(\"");
context.CodeWriter.Write(Template);
context.CodeWriter.Write("\")");
context.CodeWriter.Write("]");
context.CodeWriter.WriteLine();
}
}
}
}

View File

@ -113,5 +113,41 @@ namespace Microsoft.AspNetCore.Blazor.Razor {
return ResourceManager.GetString("LayoutDirective_TypeToken_Name", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Mark the page as a routable component..
/// </summary>
internal static string PageDirective_Description {
get {
return ResourceManager.GetString("PageDirective_Description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to An optional route template for the component..
/// </summary>
internal static string PageDirective_RouteToken_Description {
get {
return ResourceManager.GetString("PageDirective_RouteToken_Description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to route template.
/// </summary>
internal static string PageDirective_RouteToken_Name {
get {
return ResourceManager.GetString("PageDirective_RouteToken_Name", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The &apos;@{0}&apos; directive specified in {1} file will not be imported. The directive must appear at the top of each Razor cshtml file.
/// </summary>
internal static string PageDirectiveCannotBeImported {
get {
return ResourceManager.GetString("PageDirectiveCannotBeImported", resourceCulture);
}
}
}
}

View File

@ -135,4 +135,16 @@
<data name="LayoutDirective_TypeToken_Name" xml:space="preserve">
<value>TypeName</value>
</data>
<data name="PageDirectiveCannotBeImported" xml:space="preserve">
<value>The '@{0}' directive specified in {1} file will not be imported. The directive must appear at the top of each Razor cshtml file</value>
</data>
<data name="PageDirective_Description" xml:space="preserve">
<value>Mark the page as a routable component.</value>
</data>
<data name="PageDirective_RouteToken_Description" xml:space="preserve">
<value>An optional route template for the component.</value>
</data>
<data name="PageDirective_RouteToken_Name" xml:space="preserve">
<value>route template</value>
</data>
</root>

View File

@ -0,0 +1,23 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNetCore.Blazor.Components
{
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
public class RouteAttribute : Attribute
{
public RouteAttribute(string template)
{
if (template == null)
{
throw new ArgumentNullException(nameof(template));
}
Template = template;
}
public string Template { get; }
}
}

View File

@ -420,6 +420,59 @@ namespace Test
}
}
#pragma warning restore 1591
", generated);
}
[Fact]
public void CodeGeneration_ChildComponent_WithPageDirective()
{
// Arrange
AdditionalSyntaxTrees.Add(CSharpSyntaxTree.ParseText(@"
using Microsoft.AspNetCore.Blazor.Components;
namespace Test
{
public class MyComponent : BlazorComponent
{
}
}
"));
// Act
var generated = CompileToCSharp(@"
@addTagHelper *, TestAssembly
@page ""/MyPage""
@page ""/AnotherRoute/{id}""
<MyComponent />");
// Assert
CompileToAssembly(generated);
AssertSourceEquals(@"
// <auto-generated/>
#pragma warning disable 1591
namespace Test
{
#line hidden
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
[Microsoft.AspNetCore.Blazor.Components.RouteAttribute(""/MyPage"")]
[Microsoft.AspNetCore.Blazor.Components.RouteAttribute(""/AnotherRoute/{id}"")]
public class TestComponent : Microsoft.AspNetCore.Blazor.Components.BlazorComponent
{
#pragma warning disable 1998
protected override void BuildRenderTree(Microsoft.AspNetCore.Blazor.RenderTree.RenderTreeBuilder builder)
{
base.BuildRenderTree(builder);
builder.OpenComponent<Test.MyComponent>(0);
builder.CloseComponent();
}
#pragma warning restore 1998
}
}
#pragma warning restore 1591
", generated);
}
}