Make temporary "layout" and "implements" syntax work with _ViewImports hierarchies
This commit is contained in:
parent
f649de2976
commit
1e0836167d
|
|
@ -0,0 +1,77 @@
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
// 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 configuration)
|
||||||
|
{
|
||||||
|
configuration.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
// 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 configuration)
|
||||||
|
{
|
||||||
|
configuration.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,107 +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 : IntermediateNodePassBase, IRazorDirectiveClassifierPass
|
|
||||||
{
|
|
||||||
// Example: "Implements<MyApp.Namespace.ISomeType<T1, T2>>()"
|
|
||||||
// Captures: MyApp.Namespace.ISomeType<T1, T2>
|
|
||||||
private static readonly Regex ImplementsSourceRegex
|
|
||||||
= new Regex(@"^\s*Implements\s*<(.+)\>\s*\(\s*\)\s*$");
|
|
||||||
|
|
||||||
public static void Register(IRazorEngineBuilder configuration)
|
|
||||||
{
|
|
||||||
configuration.Features.Add(new TemporaryImplementsPass());
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
|
|
||||||
{
|
|
||||||
var visitor = new Visitor();
|
|
||||||
visitor.Visit(documentNode);
|
|
||||||
|
|
||||||
foreach (var implementsNode in visitor.ImplementsNodes)
|
|
||||||
{
|
|
||||||
visitor.MethodNode.Children.Remove(implementsNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (visitor.ClassNode.Interfaces == null)
|
|
||||||
{
|
|
||||||
visitor.ClassNode.Interfaces = new List<string>();
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var implementsType in visitor.ImplementsTypes)
|
|
||||||
{
|
|
||||||
visitor.ClassNode.Interfaces.Add(implementsType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class Visitor : IntermediateNodeWalker
|
|
||||||
{
|
|
||||||
public ClassDeclarationIntermediateNode ClassNode { get; private set; }
|
|
||||||
public MethodDeclarationIntermediateNode MethodNode { get; private set; }
|
|
||||||
public List<CSharpExpressionIntermediateNode> ImplementsNodes { get; private set; }
|
|
||||||
= new List<CSharpExpressionIntermediateNode>();
|
|
||||||
public List<string> ImplementsTypes { get; private set; }
|
|
||||||
= new List<string>();
|
|
||||||
|
|
||||||
public override void VisitClassDeclaration(ClassDeclarationIntermediateNode node)
|
|
||||||
{
|
|
||||||
ClassNode = node;
|
|
||||||
base.VisitClassDeclaration(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void VisitMethodDeclaration(MethodDeclarationIntermediateNode methodNode)
|
|
||||||
{
|
|
||||||
MethodNode = methodNode;
|
|
||||||
|
|
||||||
var topLevelExpressions = methodNode.Children.OfType<CSharpExpressionIntermediateNode>();
|
|
||||||
foreach (var csharpExpression in topLevelExpressions)
|
|
||||||
{
|
|
||||||
if (csharpExpression.Children.Count == 1)
|
|
||||||
{
|
|
||||||
var child = csharpExpression.Children[0];
|
|
||||||
if (child is IntermediateToken intermediateToken)
|
|
||||||
{
|
|
||||||
if (TryGetImplementsType(intermediateToken.Content, out string implementsType))
|
|
||||||
{
|
|
||||||
ImplementsNodes.Add(csharpExpression);
|
|
||||||
ImplementsTypes.Add(implementsType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
base.VisitMethodDeclaration(methodNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryGetImplementsType(string sourceCode, out string implementsType)
|
|
||||||
{
|
|
||||||
var match = ImplementsSourceRegex.Match(sourceCode);
|
|
||||||
if (match.Success)
|
|
||||||
{
|
|
||||||
implementsType = match.Groups[1].Value;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
implementsType = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,118 +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.Linq;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Blazor.Razor
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// This code is temporary. It finds top-level expressions 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 : IntermediateNodePassBase, IRazorDirectiveClassifierPass
|
|
||||||
{
|
|
||||||
// Example: "Layout<MyApp.Namespace.SomeType<T1, T2>>()"
|
|
||||||
// Captures: MyApp.Namespace.SomeType<T1, T2>
|
|
||||||
private static readonly Regex LayoutSourceRegex
|
|
||||||
= new Regex(@"^\s*Layout\s*<(.+)\>\s*\(\s*\)\s*$");
|
|
||||||
private const string LayoutAttributeTypeName
|
|
||||||
= "Microsoft.AspNetCore.Blazor.Layouts.LayoutAttribute";
|
|
||||||
|
|
||||||
public static void Register(IRazorEngineBuilder configuration)
|
|
||||||
{
|
|
||||||
configuration.Features.Add(new TemporaryLayoutPass());
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
|
|
||||||
{
|
|
||||||
var visitor = new Visitor();
|
|
||||||
visitor.Visit(documentNode);
|
|
||||||
|
|
||||||
if (visitor.DidFindLayoutDeclaration)
|
|
||||||
{
|
|
||||||
visitor.MethodNode.Children.Remove(visitor.LayoutNode);
|
|
||||||
|
|
||||||
var attributeNode = new CSharpCodeIntermediateNode();
|
|
||||||
attributeNode.Children.Add(new IntermediateToken()
|
|
||||||
{
|
|
||||||
Kind = TokenKind.CSharp,
|
|
||||||
Content = $"[{LayoutAttributeTypeName}(typeof ({visitor.LayoutType}))]" + Environment.NewLine,
|
|
||||||
});
|
|
||||||
|
|
||||||
var classNodeIndex = visitor
|
|
||||||
.NamespaceNode
|
|
||||||
.Children
|
|
||||||
.IndexOf(visitor.ClassNode);
|
|
||||||
visitor.NamespaceNode.Children.Insert(classNodeIndex, attributeNode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class Visitor : IntermediateNodeWalker
|
|
||||||
{
|
|
||||||
public bool DidFindLayoutDeclaration { get; private set; }
|
|
||||||
public NamespaceDeclarationIntermediateNode NamespaceNode { get; private set; }
|
|
||||||
public ClassDeclarationIntermediateNode ClassNode { get; private set; }
|
|
||||||
public MethodDeclarationIntermediateNode MethodNode { get; private set; }
|
|
||||||
public CSharpExpressionIntermediateNode LayoutNode { get; private set; }
|
|
||||||
public string LayoutType { get; private set; }
|
|
||||||
|
|
||||||
public override void VisitNamespaceDeclaration(NamespaceDeclarationIntermediateNode node)
|
|
||||||
{
|
|
||||||
NamespaceNode = node;
|
|
||||||
base.VisitNamespaceDeclaration(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void VisitClassDeclaration(ClassDeclarationIntermediateNode node)
|
|
||||||
{
|
|
||||||
ClassNode = node;
|
|
||||||
base.VisitClassDeclaration(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void VisitMethodDeclaration(MethodDeclarationIntermediateNode methodNode)
|
|
||||||
{
|
|
||||||
var topLevelExpressions = methodNode.Children.OfType<CSharpExpressionIntermediateNode>();
|
|
||||||
foreach (var csharpExpression in topLevelExpressions)
|
|
||||||
{
|
|
||||||
if (csharpExpression.Children.Count == 1)
|
|
||||||
{
|
|
||||||
var child = csharpExpression.Children[0];
|
|
||||||
if (child is IntermediateToken intermediateToken)
|
|
||||||
{
|
|
||||||
if (TryGetLayoutType(intermediateToken.Content, out string layoutType))
|
|
||||||
{
|
|
||||||
DidFindLayoutDeclaration = true;
|
|
||||||
MethodNode = methodNode;
|
|
||||||
LayoutNode = csharpExpression;
|
|
||||||
LayoutType = layoutType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
base.VisitMethodDeclaration(methodNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryGetLayoutType(string sourceCode, out string layoutType)
|
|
||||||
{
|
|
||||||
var match = LayoutSourceRegex.Match(sourceCode);
|
|
||||||
if (match.Success)
|
|
||||||
{
|
|
||||||
layoutType = match.Groups[1].Value;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
layoutType = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -377,7 +377,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
|
||||||
// Arrange/Act
|
// Arrange/Act
|
||||||
var testComponentTypeName = typeof(TestLayout).FullName.Replace('+', '.');
|
var testComponentTypeName = typeof(TestLayout).FullName.Replace('+', '.');
|
||||||
var component = CompileToComponent(
|
var component = CompileToComponent(
|
||||||
$"@(Layout<{testComponentTypeName}>())" +
|
$"@(Layout<{testComponentTypeName}>())\n" +
|
||||||
$"Hello");
|
$"Hello");
|
||||||
var frames = GetRenderTree(component);
|
var frames = GetRenderTree(component);
|
||||||
|
|
||||||
|
|
@ -386,7 +386,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
|
||||||
Assert.NotNull(layoutAttribute);
|
Assert.NotNull(layoutAttribute);
|
||||||
Assert.Equal(typeof(TestLayout), layoutAttribute.LayoutType);
|
Assert.Equal(typeof(TestLayout), layoutAttribute.LayoutType);
|
||||||
Assert.Collection(frames,
|
Assert.Collection(frames,
|
||||||
frame => AssertFrame.Text(frame, "Hello"));
|
frame => AssertFrame.Text(frame, "\nHello"));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -395,14 +395,14 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
|
||||||
// Arrange/Act
|
// Arrange/Act
|
||||||
var testInterfaceTypeName = typeof(ITestInterface).FullName.Replace('+', '.');
|
var testInterfaceTypeName = typeof(ITestInterface).FullName.Replace('+', '.');
|
||||||
var component = CompileToComponent(
|
var component = CompileToComponent(
|
||||||
$"@(Implements<{testInterfaceTypeName}>())" +
|
$"@(Implements<{testInterfaceTypeName}>())\n" +
|
||||||
$"Hello");
|
$"Hello");
|
||||||
var frames = GetRenderTree(component);
|
var frames = GetRenderTree(component);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.IsAssignableFrom<ITestInterface>(component);
|
Assert.IsAssignableFrom<ITestInterface>(component);
|
||||||
Assert.Collection(frames,
|
Assert.Collection(frames,
|
||||||
frame => AssertFrame.Text(frame, "Hello"));
|
frame => AssertFrame.Text(frame, "\nHello"));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue