From a053155ab4660d78f0f32ba5ad7f70c8e05d699a Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Thu, 22 Feb 2018 14:35:21 -0800 Subject: [PATCH] Implement layout and implements with directives - Remove haxxxx - Add proper directives with tooling support --- .../StandaloneApp/Pages/_ViewImports.cshtml | 2 +- .../StandaloneApp/Shared/MainLayout.cshtml | 2 +- .../BlazorApi.cs | 5 + .../BlazorExtensionInitializer.cs | 12 +- .../ImplementsDirective.cs | 43 ++++++ .../ImplementsDirectivePass.cs | 38 +++++ .../LayoutDirective.cs | 43 ++++++ .../LayoutDirectivePass.cs | 52 +++++++ ....AspNetCore.Blazor.Razor.Extensions.csproj | 15 ++ .../Resources.Designer.cs | 117 +++++++++++++++ .../Resources.resx | 138 ++++++++++++++++++ .../Temporary/SourceLinesEnumerator.cs | 77 ---------- .../Temporary/SourceLinesVisitor.cs | 37 ----- .../Temporary/TemporaryFakeDirectivePass.cs | 82 ----------- .../Temporary/TemporaryImplementsPass.cs | 53 ------- .../Temporary/TemporaryLayoutPass.cs | 61 -------- .../RazorCompilerTest.cs | 12 +- 17 files changed, 465 insertions(+), 324 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ImplementsDirective.cs create mode 100644 src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ImplementsDirectivePass.cs create mode 100644 src/Microsoft.AspNetCore.Blazor.Razor.Extensions/LayoutDirective.cs create mode 100644 src/Microsoft.AspNetCore.Blazor.Razor.Extensions/LayoutDirectivePass.cs create mode 100644 src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Resources.Designer.cs create mode 100644 src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Resources.resx delete mode 100644 src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/SourceLinesEnumerator.cs delete mode 100644 src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/SourceLinesVisitor.cs delete mode 100644 src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/TemporaryFakeDirectivePass.cs delete mode 100644 src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/TemporaryImplementsPass.cs delete mode 100644 src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/TemporaryLayoutPass.cs diff --git a/samples/StandaloneApp/Pages/_ViewImports.cshtml b/samples/StandaloneApp/Pages/_ViewImports.cshtml index 7dabd59f01..5e11c2a20c 100644 --- a/samples/StandaloneApp/Pages/_ViewImports.cshtml +++ b/samples/StandaloneApp/Pages/_ViewImports.cshtml @@ -1 +1 @@ -@(Layout()) +@layout MainLayout diff --git a/samples/StandaloneApp/Shared/MainLayout.cshtml b/samples/StandaloneApp/Shared/MainLayout.cshtml index 0d01343695..1280aa6651 100644 --- a/samples/StandaloneApp/Shared/MainLayout.cshtml +++ b/samples/StandaloneApp/Shared/MainLayout.cshtml @@ -1,4 +1,4 @@ -@(Implements()) +@implements ILayoutComponent
diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorApi.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorApi.cs index ae9cfa0e47..5e8da5d613 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorApi.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorApi.cs @@ -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"; diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorExtensionInitializer.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorExtensionInitializer.cs index 8d0ba17784..64f74d8b57 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorExtensionInitializer.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorExtensionInitializer.cs @@ -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().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()); diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ImplementsDirective.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ImplementsDirective.cs new file mode 100644 index 0000000000..014f4987fd --- /dev/null +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ImplementsDirective.cs @@ -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()); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ImplementsDirectivePass.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ImplementsDirectivePass.cs new file mode 100644 index 0000000000..f503af50da --- /dev/null +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ImplementsDirectivePass.cs @@ -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(); + } + + foreach (var implements in documentNode.FindDirectiveReferences(ImplementsDirective.Directive)) + { + var token = ((DirectiveIntermediateNode)implements.Node).Tokens.FirstOrDefault(); + if (token != null) + { + @class.Interfaces.Add(token.Content); + break; + } + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/LayoutDirective.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/LayoutDirective.cs new file mode 100644 index 0000000000..2721b0981b --- /dev/null +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/LayoutDirective.cs @@ -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()); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/LayoutDirectivePass.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/LayoutDirectivePass.cs new file mode 100644 index 0000000000..cc66cf171d --- /dev/null +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/LayoutDirectivePass.cs @@ -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; + } + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Microsoft.AspNetCore.Blazor.Razor.Extensions.csproj b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Microsoft.AspNetCore.Blazor.Razor.Extensions.csproj index ded47d31b5..a13c2e59b1 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Microsoft.AspNetCore.Blazor.Razor.Extensions.csproj +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Microsoft.AspNetCore.Blazor.Razor.Extensions.csproj @@ -23,4 +23,19 @@ + + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Resources.Designer.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Resources.Designer.cs new file mode 100644 index 0000000000..d14a4346f5 --- /dev/null +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Resources.Designer.cs @@ -0,0 +1,117 @@ +//------------------------------------------------------------------------------ +// +// 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. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.AspNetCore.Blazor.Razor { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // 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() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [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; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Declares an interface implementation for the current document.. + /// + internal static string ImplementsDirective_Description { + get { + return ResourceManager.GetString("ImplementsDirective_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The interface type implemented by the current document.. + /// + internal static string ImplementsDirective_TypeToken_Description { + get { + return ResourceManager.GetString("ImplementsDirective_TypeToken_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to TypeName. + /// + internal static string ImplementsDirective_TypeToken_Name { + get { + return ResourceManager.GetString("ImplementsDirective_TypeToken_Name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Declares a layout type for the current document.. + /// + internal static string LayoutDirective_Description { + get { + return ResourceManager.GetString("LayoutDirective_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The interface type implemented by the current document.. + /// + internal static string LayoutDirective_TypeToken_Description { + get { + return ResourceManager.GetString("LayoutDirective_TypeToken_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to TypeName. + /// + internal static string LayoutDirective_TypeToken_Name { + get { + return ResourceManager.GetString("LayoutDirective_TypeToken_Name", resourceCulture); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Resources.resx b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Resources.resx new file mode 100644 index 0000000000..77b30d74e5 --- /dev/null +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Resources.resx @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Declares an interface implementation for the current document. + + + The interface type implemented by the current document. + + + TypeName + + + Declares a layout type for the current document. + + + The interface type implemented by the current document. + + + TypeName + + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/SourceLinesEnumerator.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/SourceLinesEnumerator.cs deleted file mode 100644 index a4d6a468b4..0000000000 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/SourceLinesEnumerator.cs +++ /dev/null @@ -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 - { - private RazorSourceDocument _source; - - public SourceLinesEnumerable(RazorSourceDocument source) - => _source = source; - - public IEnumerator GetEnumerator() - => new SourceLinesEnumerator(_source); - - IEnumerator IEnumerable.GetEnumerator() - => new SourceLinesEnumerator(_source); - - private class SourceLinesEnumerator : IEnumerator - { - 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; - } - } - } -} diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/SourceLinesVisitor.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/SourceLinesVisitor.cs deleted file mode 100644 index ae336c8f13..0000000000 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/SourceLinesVisitor.cs +++ /dev/null @@ -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 - { - /// - /// Visits each line in the document's imports (in order), followed by - /// each line in the document's primary syntax tree. - /// - 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); - } - } - } -} diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/TemporaryFakeDirectivePass.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/TemporaryFakeDirectivePass.cs deleted file mode 100644 index 6755234a26..0000000000 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/TemporaryFakeDirectivePass.cs +++ /dev/null @@ -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 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 _matchedContent = new List(); - - public IEnumerable 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); - } - } - } - } -} diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/TemporaryImplementsPass.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/TemporaryImplementsPass.cs deleted file mode 100644 index 731961ef75..0000000000 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/TemporaryImplementsPass.cs +++ /dev/null @@ -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 -{ - /// - /// This code is temporary. It finds top-level expressions of the form - /// @Implements() - /// ... 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. - /// - internal class TemporaryImplementsPass : TemporaryFakeDirectivePass - { - // Example: "Implements>()" - // Captures: MyApp.Namespace.ISomeType - 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 matchedContent) - { - var classNode = codeDocument.GetDocumentIntermediateNode().FindPrimaryClass(); - if (classNode.Interfaces == null) - { - classNode.Interfaces = new List(); - } - - foreach (var implementsType in matchedContent) - { - classNode.Interfaces.Add(implementsType); - } - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/TemporaryLayoutPass.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/TemporaryLayoutPass.cs deleted file mode 100644 index 69ec91f3ed..0000000000 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/TemporaryLayoutPass.cs +++ /dev/null @@ -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 -{ - /// - /// This code is temporary. It finds source code lines of the form - /// @Layout() - /// ... 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. - /// - internal class TemporaryLayoutPass : TemporaryFakeDirectivePass - { - // Example: "Layout>()" - // Captures: MyApp.Namespace.SomeType - 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 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); - } - } -} diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorCompilerTest.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorCompilerTest.cs index 2464f92610..92d6a38032 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorCompilerTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorCompilerTest.cs @@ -663,12 +663,12 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test } [Fact] - public void SupportsLayoutDeclarationsViaTemporarySyntax() + public void SupportsLayoutDeclarations() { // Arrange/Act var testComponentTypeName = FullTypeName(); 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(); var component = CompileToComponent( - $"@(Implements<{testInterfaceTypeName}>())\n" + + $"@implements {testInterfaceTypeName}\n" + $"Hello"); var frames = GetRenderTree(component); // Assert Assert.IsAssignableFrom(component); Assert.Collection(frames, - frame => AssertFrame.Text(frame, "\nHello")); + frame => AssertFrame.Text(frame, "Hello")); } [Fact]