Implement layout and implements with directives

- Remove haxxxx
- Add proper directives with tooling support
This commit is contained in:
Ryan Nowak 2018-02-22 14:35:21 -08:00 committed by Steve Sanderson
parent 57a04fb178
commit a053155ab4
17 changed files with 465 additions and 324 deletions

View File

@ -1 +1 @@
@(Layout<MainLayout>())
@layout MainLayout

View File

@ -1,4 +1,4 @@
@(Implements<ILayoutComponent>())
@implements ILayoutComponent
<div class='container-fluid'>
<div class='row'>

View File

@ -14,6 +14,11 @@ namespace Microsoft.AspNetCore.Blazor.Razor
public static readonly string BuildRenderTree = nameof(BuildRenderTree);
}
public static class LayoutAttribute
{
public static readonly string FullTypeName = "Microsoft.AspNetCore.Blazor.Layouts.LayoutAttribute";
}
public static class RenderFragment
{
public static readonly string FullTypeName = "Microsoft.AspNetCore.Blazor.RenderFragment";

View File

@ -18,10 +18,10 @@ namespace Microsoft.AspNetCore.Blazor.Razor
}
FunctionsDirective.Register(builder);
InjectDirective.Register(builder);
ImplementsDirective.Register(builder);
InheritsDirective.Register(builder);
TemporaryLayoutPass.Register(builder);
TemporaryImplementsPass.Register(builder);
InjectDirective.Register(builder);
LayoutDirective.Register(builder);
builder.Features.Remove(builder.Features.OfType<IImportProjectFeature>().Single());
builder.Features.Add(new BlazorImportProjectFeature());
@ -41,10 +41,10 @@ namespace Microsoft.AspNetCore.Blazor.Razor
}
FunctionsDirective.Register(builder);
InjectDirective.Register(builder);
ImplementsDirective.Register(builder);
InheritsDirective.Register(builder);
TemporaryLayoutPass.Register(builder);
TemporaryImplementsPass.Register(builder);
InjectDirective.Register(builder);
LayoutDirective.Register(builder);
builder.Features.Add(new ConfigureBlazorCodeGenerationOptions());

View File

@ -0,0 +1,43 @@
// 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;
namespace Microsoft.AspNetCore.Blazor.Razor
{
internal static class ImplementsDirective
{
public static readonly DirectiveDescriptor Directive = DirectiveDescriptor.CreateDirective(
"implements",
DirectiveKind.SingleLine,
builder =>
{
builder.AddTypeToken(Resources.ImplementsDirective_TypeToken_Name, Resources.ImplementsDirective_TypeToken_Description);
builder.Usage = DirectiveUsage.FileScopedMultipleOccurring;
builder.Description = Resources.ImplementsDirective_Description;
});
public static void Register(RazorProjectEngineBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
builder.AddDirective(Directive);
builder.Features.Add(new ImplementsDirectivePass());
}
public static void Register(IRazorEngineBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
builder.AddDirective(Directive);
builder.Features.Add(new ImplementsDirectivePass());
}
}
}

View File

@ -0,0 +1,38 @@
// 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.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Extensions;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
namespace Microsoft.AspNetCore.Blazor.Razor
{
internal class ImplementsDirectivePass : IntermediateNodePassBase, IRazorDirectiveClassifierPass
{
protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
{
var @class = documentNode.FindPrimaryClass();
if (@class == null)
{
return;
}
if (@class.Interfaces == null)
{
@class.Interfaces = new List<string>();
}
foreach (var implements in documentNode.FindDirectiveReferences(ImplementsDirective.Directive))
{
var token = ((DirectiveIntermediateNode)implements.Node).Tokens.FirstOrDefault();
if (token != null)
{
@class.Interfaces.Add(token.Content);
break;
}
}
}
}
}

View File

@ -0,0 +1,43 @@
// 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;
namespace Microsoft.AspNetCore.Blazor.Razor
{
internal static class LayoutDirective
{
public static readonly DirectiveDescriptor Directive = DirectiveDescriptor.CreateDirective(
"layout",
DirectiveKind.SingleLine,
builder =>
{
builder.AddTypeToken(Resources.LayoutDirective_TypeToken_Name, Resources.LayoutDirective_TypeToken_Description);
builder.Usage = DirectiveUsage.FileScopedSinglyOccurring;
builder.Description = Resources.LayoutDirective_Description;
});
public static void Register(RazorProjectEngineBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
builder.AddDirective(Directive);
builder.Features.Add(new LayoutDirectivePass());
}
public static void Register(IRazorEngineBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
builder.AddDirective(Directive);
builder.Features.Add(new LayoutDirectivePass());
}
}
}

View File

@ -0,0 +1,52 @@
// 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.Intermediate;
namespace Microsoft.AspNetCore.Blazor.Razor
{
internal class LayoutDirectivePass : IntermediateNodePassBase, IRazorDirectiveClassifierPass
{
protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
{
var @namespace = documentNode.FindPrimaryNamespace();
var @class = documentNode.FindPrimaryClass();
if (@namespace == null || @class == null)
{
return;
}
var directives = documentNode.FindDirectiveReferences(LayoutDirective.Directive);
if (directives.Count == 0)
{
return;
}
var token = ((DirectiveIntermediateNode)directives[0].Node).Tokens.FirstOrDefault();
if (token == null)
{
return;
}
var attributeNode = new CSharpCodeIntermediateNode();
attributeNode.Children.Add(new IntermediateToken()
{
Kind = TokenKind.CSharp,
Content = $"[{BlazorApi.LayoutAttribute.FullTypeName}(typeof({token.Content}))]" + Environment.NewLine,
});
// Insert the new attribute on top of the class
for (var i = 0; i < @namespace.Children.Count; i++)
{
if (object.ReferenceEquals(@namespace.Children[i], @class))
{
@namespace.Children.Insert(i, attributeNode);
break;
}
}
}
}
}

View File

@ -23,4 +23,19 @@
</ProjectReference>
<Reference Include="..\anglesharp\AngleSharpBuilder\dist\Microsoft.AspNetCore.Blazor.AngleSharp.dll" />
</ItemGroup>
<ItemGroup>
<Compile Update="Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
</Project>

View File

@ -0,0 +1,117 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Microsoft.AspNetCore.Blazor.Razor {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNetCore.Blazor.Razor.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to Declares an interface implementation for the current document..
/// </summary>
internal static string ImplementsDirective_Description {
get {
return ResourceManager.GetString("ImplementsDirective_Description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The interface type implemented by the current document..
/// </summary>
internal static string ImplementsDirective_TypeToken_Description {
get {
return ResourceManager.GetString("ImplementsDirective_TypeToken_Description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to TypeName.
/// </summary>
internal static string ImplementsDirective_TypeToken_Name {
get {
return ResourceManager.GetString("ImplementsDirective_TypeToken_Name", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Declares a layout type for the current document..
/// </summary>
internal static string LayoutDirective_Description {
get {
return ResourceManager.GetString("LayoutDirective_Description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The interface type implemented by the current document..
/// </summary>
internal static string LayoutDirective_TypeToken_Description {
get {
return ResourceManager.GetString("LayoutDirective_TypeToken_Description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to TypeName.
/// </summary>
internal static string LayoutDirective_TypeToken_Name {
get {
return ResourceManager.GetString("LayoutDirective_TypeToken_Name", resourceCulture);
}
}
}
}

View File

@ -0,0 +1,138 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="ImplementsDirective_Description" xml:space="preserve">
<value>Declares an interface implementation for the current document.</value>
</data>
<data name="ImplementsDirective_TypeToken_Description" xml:space="preserve">
<value>The interface type implemented by the current document.</value>
</data>
<data name="ImplementsDirective_TypeToken_Name" xml:space="preserve">
<value>TypeName</value>
</data>
<data name="LayoutDirective_Description" xml:space="preserve">
<value>Declares a layout type for the current document.</value>
</data>
<data name="LayoutDirective_TypeToken_Description" xml:space="preserve">
<value>The interface type implemented by the current document.</value>
</data>
<data name="LayoutDirective_TypeToken_Name" xml:space="preserve">
<value>TypeName</value>
</data>
</root>

View File

@ -1,77 +0,0 @@
// 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 Microsoft.AspNetCore.Razor.Language;
using System;
using System.Collections;
using System.Collections.Generic;
namespace Microsoft.AspNetCore.Blazor.Razor
{
// This only exists to support SourceLinesVisitor and can be removed once
// we are able to implement proper Blazor-specific directives
internal class SourceLinesEnumerable : IEnumerable<string>
{
private RazorSourceDocument _source;
public SourceLinesEnumerable(RazorSourceDocument source)
=> _source = source;
public IEnumerator<string> GetEnumerator()
=> new SourceLinesEnumerator(_source);
IEnumerator IEnumerable.GetEnumerator()
=> new SourceLinesEnumerator(_source);
private class SourceLinesEnumerator : IEnumerator<string>
{
private readonly RazorSourceDocument _sourceDocument;
private readonly RazorSourceLineCollection _lines;
private int _currentLineIndex;
private int _cumulativeLengthOfPrecedingLines;
private char[] _currentLineBuffer = new char[200]; // Grows if needed
private string _currentLineText;
public SourceLinesEnumerator(RazorSourceDocument sourceDocument)
{
_sourceDocument = sourceDocument ?? throw new ArgumentNullException(nameof(sourceDocument));
_lines = _sourceDocument.Lines;
_currentLineIndex = -1;
}
public string Current => _currentLineText;
object IEnumerator.Current => _currentLineText;
public void Dispose()
{
}
public bool MoveNext()
{
_currentLineIndex++;
if (_currentLineIndex >= _lines.Count)
{
return false;
}
var lineLength = _lines.GetLineLength(_currentLineIndex);
if (_currentLineBuffer.Length < lineLength)
{
_currentLineBuffer = new char[lineLength];
}
_sourceDocument.CopyTo(_cumulativeLengthOfPrecedingLines, _currentLineBuffer, 0, lineLength);
_currentLineText = new string(_currentLineBuffer, 0, lineLength);
_cumulativeLengthOfPrecedingLines += lineLength;
return true;
}
public void Reset()
{
_currentLineIndex = -1;
_cumulativeLengthOfPrecedingLines = 0;
}
}
}
}

View File

@ -1,37 +0,0 @@
// 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 Microsoft.AspNetCore.Razor.Language;
namespace Microsoft.AspNetCore.Blazor.Razor
{
// This only exists to support the temporary fake Blazor directives and can
// be removed once we are able to implement proper Blazor-specific directives
internal abstract class SourceLinesVisitor
{
/// <summary>
/// Visits each line in the document's imports (in order), followed by
/// each line in the document's primary syntax tree.
/// </summary>
public void Visit(RazorCodeDocument codeDocument)
{
foreach (var import in codeDocument.GetImportSyntaxTrees())
{
VisitSyntaxTree(import);
}
VisitSyntaxTree(codeDocument.GetSyntaxTree());
}
protected abstract void VisitLine(string line);
private void VisitSyntaxTree(RazorSyntaxTree syntaxTree)
{
var sourceDocument = syntaxTree.Source;
foreach (var line in new SourceLinesEnumerable(sourceDocument))
{
VisitLine(line);
}
}
}
}

View File

@ -1,82 +0,0 @@
// 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 Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
namespace Microsoft.AspNetCore.Blazor.Razor
{
// Until we're able to add real directives, this implements a temporary mechanism whereby we
// search for lines of the form "@({regex})" (including in the imports sources) and do something
// with the regex matches. Also we remove the corresponding tokens from the intermediate
// representation to stop them from interfering with the compiled output on its own.
internal abstract class TemporaryFakeDirectivePass : IntermediateNodePassBase, IRazorDirectiveClassifierPass
{
private readonly Regex _sourceLineRegex;
private readonly Regex _tokenRegex;
protected TemporaryFakeDirectivePass(string syntaxRegexPattern)
{
_sourceLineRegex = new Regex($@"^\s*@\({syntaxRegexPattern}\)\s*$");
_tokenRegex = new Regex($@"^{syntaxRegexPattern}$");
}
protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
{
// First, remove any matching lines from the intermediate representation
// in the primary document. Don't need to remove them from imports as they
// have no effect there anyway.
var methodNode = documentNode.FindPrimaryMethod();
var methodNodeChildren = methodNode.Children.ToList();
foreach (var node in methodNodeChildren)
{
if (IsMatchingNode(node))
{
methodNode.Children.Remove(node);
}
}
// Now find the matching lines in the source code (including imports)
// Need to do this on source, because the imports aren't in the intermediate representation
var linesVisitor = new RegexSourceLinesVisitor(_sourceLineRegex);
linesVisitor.Visit(codeDocument);
if (linesVisitor.MatchedContent.Any())
{
HandleMatchedContent(codeDocument, linesVisitor.MatchedContent);
}
}
protected abstract void HandleMatchedContent(RazorCodeDocument codeDocument, IEnumerable<string> matchedContent);
private bool IsMatchingNode(IntermediateNode node)
=> node.Children.Count == 1
&& node.Children[0] is IntermediateToken intermediateToken
&& _tokenRegex.IsMatch(intermediateToken.Content);
private class RegexSourceLinesVisitor : SourceLinesVisitor
{
private Regex _searchRegex;
private readonly List<string> _matchedContent = new List<string>();
public IEnumerable<string> MatchedContent => _matchedContent;
public RegexSourceLinesVisitor(Regex searchRegex)
{
_searchRegex = searchRegex;
}
protected override void VisitLine(string line)
{
// Pick the most specific by looking for the final one in the sources
var match = _searchRegex.Match(line);
if (match.Success)
{
_matchedContent.Add(match.Groups[1].Value);
}
}
}
}
}

View File

@ -1,53 +0,0 @@
// 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 Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
namespace Microsoft.AspNetCore.Blazor.Razor
{
/// <summary>
/// This code is temporary. It finds top-level expressions of the form
/// @Implements<SomeInterfaceType>()
/// ... and converts them into interface declarations on the class.
/// Once we're able to add Blazor-specific directives and have them show up in tooling,
/// we'll replace this with a simpler and cleaner "@implements SomeInterfaceType" directive.
/// </summary>
internal class TemporaryImplementsPass : TemporaryFakeDirectivePass
{
// Example: "Implements<MyApp.Namespace.ISomeType<T1, T2>>()"
// Captures: MyApp.Namespace.ISomeType<T1, T2>
private const string ImplementsTokenPattern = @"\s*Implements\s*<(.+)\>\s*\(\s*\)\s*";
public static void Register(IRazorEngineBuilder builder)
{
builder.Features.Add(new TemporaryImplementsPass());
}
public static void Register(RazorProjectEngineBuilder builder)
{
builder.Features.Add(new TemporaryImplementsPass());
}
private TemporaryImplementsPass() : base(ImplementsTokenPattern)
{
}
protected override void HandleMatchedContent(RazorCodeDocument codeDocument, IEnumerable<string> matchedContent)
{
var classNode = codeDocument.GetDocumentIntermediateNode().FindPrimaryClass();
if (classNode.Interfaces == null)
{
classNode.Interfaces = new List<string>();
}
foreach (var implementsType in matchedContent)
{
classNode.Interfaces.Add(implementsType);
}
}
}
}

View File

@ -1,61 +0,0 @@
// 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 Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Microsoft.AspNetCore.Blazor.Razor
{
/// <summary>
/// This code is temporary. It finds source code lines of the form
/// @Layout<SomeType>()
/// ... and converts them into [Layout(typeof(SomeType))] attributes on the class.
/// Once we're able to add Blazor-specific directives and have them show up in tooling,
/// we'll replace this with a simpler and cleaner "@Layout SomeType" directive.
/// </summary>
internal class TemporaryLayoutPass : TemporaryFakeDirectivePass
{
// Example: "Layout<MyApp.Namespace.SomeType<T1, T2>>()"
// Captures: MyApp.Namespace.SomeType<T1, T2>
private const string LayoutTokenPattern = @"\s*Layout\s*<(.+)\>\s*\(\s*\)\s*";
private const string LayoutAttributeTypeName
= "Microsoft.AspNetCore.Blazor.Layouts.LayoutAttribute";
public static void Register(IRazorEngineBuilder builder)
{
builder.Features.Add(new TemporaryLayoutPass());
}
public static void Register(RazorProjectEngineBuilder builder)
{
builder.Features.Add(new TemporaryLayoutPass());
}
private TemporaryLayoutPass() : base(LayoutTokenPattern)
{
}
protected override void HandleMatchedContent(RazorCodeDocument codeDocument, IEnumerable<string> matchedContent)
{
var chosenLayoutType = matchedContent.Last();
var attributeNode = new CSharpCodeIntermediateNode();
attributeNode.Children.Add(new IntermediateToken()
{
Kind = TokenKind.CSharp,
Content = $"[{LayoutAttributeTypeName}(typeof ({chosenLayoutType}))]" + Environment.NewLine,
});
var docNode = codeDocument.GetDocumentIntermediateNode();
var namespaceNode = docNode.FindPrimaryNamespace();
var classNode = docNode.FindPrimaryClass();
var classNodeIndex = namespaceNode
.Children
.IndexOf(classNode);
namespaceNode.Children.Insert(classNodeIndex, attributeNode);
}
}
}

View File

@ -663,12 +663,12 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
}
[Fact]
public void SupportsLayoutDeclarationsViaTemporarySyntax()
public void SupportsLayoutDeclarations()
{
// Arrange/Act
var testComponentTypeName = FullTypeName<TestLayout>();
var component = CompileToComponent(
$"@(Layout<{testComponentTypeName}>())\n" +
$"@layout {testComponentTypeName}\n" +
$"Hello");
var frames = GetRenderTree(component);
@ -677,23 +677,23 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
Assert.NotNull(layoutAttribute);
Assert.Equal(typeof(TestLayout), layoutAttribute.LayoutType);
Assert.Collection(frames,
frame => AssertFrame.Text(frame, "\nHello"));
frame => AssertFrame.Text(frame, "Hello"));
}
[Fact]
public void SupportsImplementsDeclarationsViaTemporarySyntax()
public void SupportsImplementsDeclarations()
{
// Arrange/Act
var testInterfaceTypeName = FullTypeName<ITestInterface>();
var component = CompileToComponent(
$"@(Implements<{testInterfaceTypeName}>())\n" +
$"@implements {testInterfaceTypeName}\n" +
$"Hello");
var frames = GetRenderTree(component);
// Assert
Assert.IsAssignableFrom<ITestInterface>(component);
Assert.Collection(frames,
frame => AssertFrame.Text(frame, "\nHello"));
frame => AssertFrame.Text(frame, "Hello"));
}
[Fact]