diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BindLoweringPass.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BindLoweringPass.cs index ce0ab1026e..00ed66858d 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BindLoweringPass.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BindLoweringPass.cs @@ -1,6 +1,7 @@ // 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.Blazor.Shared; using Microsoft.AspNetCore.Razor.Language; @@ -11,7 +12,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor internal class BindLoweringPass : IntermediateNodePassBase, IRazorOptimizationPass { // Run after event handler pass - public override int Order => base.Order + 50; + public override int Order => 100; protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) { @@ -24,28 +25,45 @@ namespace Microsoft.AspNetCore.Blazor.Razor } // For each bind *usage* we need to rewrite the tag helper node to map to basic constructs. - var nodes = documentNode.FindDescendantNodes(); - for (var i = 0; i < nodes.Count; i++) + var references = documentNode.FindDescendantReferences(); + + var parents = new HashSet(); + for (var i = 0; i < references.Count; i++) { - var node = nodes[i]; + parents.Add(references[i].Parent); + } - ProcessDuplicates(node); + foreach (var parent in parents) + { + ProcessDuplicates(parent); + } - for (var j = node.Children.Count - 1; j >= 0; j--) + for (var i = 0; i < references.Count; i++) + { + var reference = references[i]; + var node = (TagHelperPropertyIntermediateNode)reference.Node; + + if (!reference.Parent.Children.Contains(node)) { - var attributeNode = node.Children[j] as ComponentAttributeExtensionNode; - if (attributeNode != null && - attributeNode.TagHelper != null && - attributeNode.TagHelper.IsBindTagHelper() && - attributeNode.AttributeName.StartsWith("bind")) + // This node was removed as a duplicate, skip it. + continue; + } + + if (node.TagHelper.IsBindTagHelper() && node.AttributeName.StartsWith("bind")) + { + // Workaround for https://github.com/aspnet/Blazor/issues/703 + var rewritten = RewriteUsage(reference.Parent, node); + reference.Remove(); + + for (var j = 0; j < rewritten.Length; j++) { - RewriteUsage(node, j, attributeNode); + reference.Parent.Children.Add(rewritten[j]); } } } } - private void ProcessDuplicates(TagHelperIntermediateNode node) + private void ProcessDuplicates(IntermediateNode node) { // Reverse order because we will remove nodes. // @@ -56,24 +74,23 @@ namespace Microsoft.AspNetCore.Blazor.Razor { // For each usage of the general 'fallback' bind tag helper, it could duplicate // the usage of a more specific one. Look for duplicates and remove the fallback. - var attributeNode = node.Children[i] as ComponentAttributeExtensionNode; - if (attributeNode != null && - attributeNode.TagHelper != null && - attributeNode.TagHelper.IsFallbackBindTagHelper()) + var attribute = node.Children[i] as TagHelperPropertyIntermediateNode; + if (attribute != null && + attribute.TagHelper != null && + attribute.TagHelper.IsFallbackBindTagHelper()) { for (var j = 0; j < node.Children.Count; j++) { - var duplicate = node.Children[j] as ComponentAttributeExtensionNode; + var duplicate = node.Children[j] as TagHelperPropertyIntermediateNode; if (duplicate != null && duplicate.TagHelper != null && duplicate.TagHelper.IsBindTagHelper() && - duplicate.AttributeName == attributeNode.AttributeName && - !object.ReferenceEquals(attributeNode, duplicate)) + duplicate.AttributeName == attribute.AttributeName && + !object.ReferenceEquals(attribute, duplicate)) { // Found a duplicate - remove the 'fallback' in favor of the // more specific tag helper. node.Children.RemoveAt(i); - node.TagHelpers.Remove(attributeNode.TagHelper); break; } } @@ -82,23 +99,22 @@ namespace Microsoft.AspNetCore.Blazor.Razor // Also treat the general as a 'fallback' for that case and remove it. // This is a workaround for a limitation where you can't write a tag helper that binds only // when a specific attribute is **not** present. - if (attributeNode != null && - attributeNode.TagHelper != null && - attributeNode.TagHelper.IsInputElementFallbackBindTagHelper()) + if (attribute != null && + attribute.TagHelper != null && + attribute.TagHelper.IsInputElementFallbackBindTagHelper()) { for (var j = 0; j < node.Children.Count; j++) { - var duplicate = node.Children[j] as ComponentAttributeExtensionNode; + var duplicate = node.Children[j] as TagHelperPropertyIntermediateNode; if (duplicate != null && duplicate.TagHelper != null && duplicate.TagHelper.IsInputElementBindTagHelper() && - duplicate.AttributeName == attributeNode.AttributeName && - !object.ReferenceEquals(attributeNode, duplicate)) + duplicate.AttributeName == attribute.AttributeName && + !object.ReferenceEquals(attribute, duplicate)) { // Found a duplicate - remove the 'fallback' input tag helper in favor of the // more specific tag helper. node.Children.RemoveAt(i); - node.TagHelpers.Remove(attributeNode.TagHelper); break; } } @@ -107,7 +123,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor // If we still have duplicates at this point then they are genuine conflicts. var duplicates = node.Children - .OfType() + .OfType() .GroupBy(p => p.AttributeName) .Where(g => g.Count() > 1); @@ -124,7 +140,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor } } - private void RewriteUsage(TagHelperIntermediateNode node, int index, ComponentAttributeExtensionNode attributeNode) + private IntermediateNode[] RewriteUsage(IntermediateNode parent, TagHelperPropertyIntermediateNode node) { // Bind works similarly to a macro, it always expands to code that the user could have written. // @@ -146,8 +162,9 @@ namespace Microsoft.AspNetCore.Blazor.Razor // multiple passes handle 'special' tag helpers. We have another pass that translates // a tag helper node back into 'regular' element when it doesn't have an associated component if (!TryComputeAttributeNames( + parent, node, - attributeNode.AttributeName, + node.AttributeName, out var valueAttributeName, out var changeAttributeName, out var valueAttribute, @@ -156,83 +173,60 @@ namespace Microsoft.AspNetCore.Blazor.Razor // Skip anything we can't understand. It's important that we don't crash, that will bring down // the build. node.Diagnostics.Add(BlazorDiagnosticFactory.CreateBindAttribute_InvalidSyntax( - attributeNode.Source, - attributeNode.AttributeName)); - return; + node.Source, + node.AttributeName)); + return new[] { node }; } - var original = GetAttributeContent(attributeNode); + var original = GetAttributeContent(node); if (string.IsNullOrEmpty(original.Content)) { // This can happen in error cases, the parser will already have flagged this // as an error, so ignore it. - return; + return new[] { node }; } // Look for a matching format node. If we find one then we need to pass the format into the // two nodes we generate. IntermediateToken format = null; - if (TryGetFormatNode(node, - attributeNode, + if (TryGetFormatNode( + parent, + node, valueAttributeName, out var formatNode)) { // Don't write the format out as its own attribute, just capture it as a string // or expression. - node.Children.Remove(formatNode); + parent.Children.Remove(formatNode); format = GetAttributeContent(formatNode); } - var valueAttributeNode = new ComponentAttributeExtensionNode(attributeNode) - { - AttributeName = valueAttributeName, - BoundAttribute = valueAttribute, // Might be null if it doesn't match a component attribute - PropertyName = valueAttribute?.GetPropertyName(), - TagHelper = valueAttribute == null ? null : attributeNode.TagHelper, - }; - node.Children.Insert(index, valueAttributeNode); - // Now rewrite the content of the value node to look like: // // BindMethods.GetValue() OR // BindMethods.GetValue(, ) - valueAttributeNode.Children.Clear(); - - var expression = new CSharpExpressionIntermediateNode(); - valueAttributeNode.Children.Add(expression); - - expression.Children.Add(new IntermediateToken() + var valueExpressionTokens = new List(); + valueExpressionTokens.Add(new IntermediateToken() { Content = $"{BlazorApi.BindMethods.GetValue}(", Kind = TokenKind.CSharp }); - expression.Children.Add(original); - + valueExpressionTokens.Add(original); if (!string.IsNullOrEmpty(format?.Content)) { - expression.Children.Add(new IntermediateToken() + valueExpressionTokens.Add(new IntermediateToken() { Content = ", ", Kind = TokenKind.CSharp, }); - expression.Children.Add(format); + valueExpressionTokens.Add(format); } - - expression.Children.Add(new IntermediateToken() + valueExpressionTokens.Add(new IntermediateToken() { Content = ")", Kind = TokenKind.CSharp, }); - var changeAttributeNode = new ComponentAttributeExtensionNode(attributeNode) - { - AttributeName = changeAttributeName, - BoundAttribute = changeAttribute, // Might be null if it doesn't match a component attribute - PropertyName = changeAttribute?.GetPropertyName(), - TagHelper = changeAttribute == null ? null : attributeNode.TagHelper, - }; - node.Children[index + 1] = changeAttributeNode; - // Now rewrite the content of the change-handler node. There are two cases we care about // here. If it's a component attribute, then don't use the 'BindMethods wrapper. We expect // component attributes to always 'match' on type. @@ -246,32 +240,102 @@ namespace Microsoft.AspNetCore.Blazor.Razor // BindMethods.SetValueHandler(__value => = __value, , ) // // Note that the linemappings here are applied to the value attribute, not the change attribute. - string changeAttributeContent = null; - if (changeAttributeNode.BoundAttribute == null && format == null) + + string changeExpressionContent = null; + if (changeAttribute == null && format == null) { - changeAttributeContent = $"{BlazorApi.BindMethods.SetValueHandler}(__value => {original.Content} = __value, {original.Content})"; + changeExpressionContent = $"{BlazorApi.BindMethods.SetValueHandler}(__value => {original.Content} = __value, {original.Content})"; } - else if (changeAttributeNode.BoundAttribute == null && format != null) + else if (changeAttribute == null && format != null) { - changeAttributeContent = $"{BlazorApi.BindMethods.SetValueHandler}(__value => {original.Content} = __value, {original.Content}, {format.Content})"; + changeExpressionContent = $"{BlazorApi.BindMethods.SetValueHandler}(__value => {original.Content} = __value, {original.Content}, {format.Content})"; } else { - changeAttributeContent = $"__value => {original.Content} = __value"; + changeExpressionContent = $"__value => {original.Content} = __value"; } - - changeAttributeNode.Children.Clear(); - changeAttributeNode.Children.Add(new CSharpExpressionIntermediateNode() + var changeExpressionTokens = new List() { - Children = + new IntermediateToken() { - new IntermediateToken() - { - Content = changeAttributeContent, - Kind = TokenKind.CSharp - }, - }, - }); + Content = changeExpressionContent, + Kind = TokenKind.CSharp + } + }; + + if (parent is HtmlElementIntermediateNode) + { + var valueNode = new HtmlAttributeIntermediateNode() + { + AttributeName = valueAttributeName, + Source = node.Source, + + Prefix = valueAttributeName + "=\"", + Suffix = "\"", + }; + + for (var i = 0; i < node.Diagnostics.Count; i++) + { + valueNode.Diagnostics.Add(node.Diagnostics[i]); + } + + valueNode.Children.Add(new CSharpExpressionAttributeValueIntermediateNode()); + for (var i = 0; i < valueExpressionTokens.Count; i++) + { + valueNode.Children[0].Children.Add(valueExpressionTokens[i]); + } + + var changeNode = new HtmlAttributeIntermediateNode() + { + AttributeName = changeAttributeName, + Source = node.Source, + + Prefix = changeAttributeName + "=\"", + Suffix = "\"", + }; + + changeNode.Children.Add(new CSharpExpressionAttributeValueIntermediateNode()); + for (var i = 0; i < changeExpressionTokens.Count; i++) + { + changeNode.Children[0].Children.Add(changeExpressionTokens[i]); + } + + return new[] { valueNode, changeNode }; + } + else + { + var valueNode = new ComponentAttributeExtensionNode(node) + { + AttributeName = valueAttributeName, + BoundAttribute = valueAttribute, // Might be null if it doesn't match a component attribute + PropertyName = valueAttribute?.GetPropertyName(), + TagHelper = valueAttribute == null ? null : node.TagHelper, + }; + + valueNode.Children.Clear(); + valueNode.Children.Add(new CSharpExpressionIntermediateNode()); + for (var i = 0; i < valueExpressionTokens.Count; i++) + { + valueNode.Children[0].Children.Add(valueExpressionTokens[i]); + } + + var changeNode = new ComponentAttributeExtensionNode(node) + { + AttributeName = changeAttributeName, + BoundAttribute = changeAttribute, // Might be null if it doesn't match a component attribute + PropertyName = changeAttribute?.GetPropertyName(), + TagHelper = changeAttribute == null ? null : node.TagHelper, + }; + + changeNode.Children.Clear(); + changeNode.Children.Add(new CSharpExpressionIntermediateNode()); + for (var i = 0; i < changeExpressionTokens.Count; i++) + { + changeNode.Children[0].Children.Add(changeExpressionTokens[i]); + } + + return new[] { valueNode, changeNode }; + } } private bool TryParseBindAttribute( @@ -319,7 +383,8 @@ namespace Microsoft.AspNetCore.Blazor.Razor // Attempts to compute the attribute names that should be used for an instance of 'bind'. private bool TryComputeAttributeNames( - TagHelperIntermediateNode node, + IntermediateNode parent, + TagHelperPropertyIntermediateNode node, string attributeName, out string valueAttributeName, out string changeAttributeName, @@ -342,12 +407,11 @@ namespace Microsoft.AspNetCore.Blazor.Razor // generated to match a specific tag and has metadata that identify the attributes. // // We expect 1 bind tag helper per-node. - var bindTagHelper = node.TagHelpers.Single(t => t.IsBindTagHelper()); - valueAttributeName = bindTagHelper.GetValueAttributeName() ?? valueAttributeName; - changeAttributeName = bindTagHelper.GetChangeAttributeName() ?? changeAttributeName; + valueAttributeName = node.TagHelper.GetValueAttributeName() ?? valueAttributeName; + changeAttributeName = node.TagHelper.GetChangeAttributeName() ?? changeAttributeName; // We expect 0-1 components per-node. - var componentTagHelper = node.TagHelpers.FirstOrDefault(t => t.IsComponentTagHelper()); + var componentTagHelper = (parent as ComponentExtensionNode)?.Component; if (componentTagHelper == null) { // If it's not a component node then there isn't too much else to figure out. @@ -386,14 +450,14 @@ namespace Microsoft.AspNetCore.Blazor.Razor } private bool TryGetFormatNode( - TagHelperIntermediateNode node, - ComponentAttributeExtensionNode attributeNode, + IntermediateNode node, + TagHelperPropertyIntermediateNode attributeNode, string valueAttributeName, - out ComponentAttributeExtensionNode formatNode) + out TagHelperPropertyIntermediateNode formatNode) { for (var i = 0; i < node.Children.Count; i++) { - var child = node.Children[i] as ComponentAttributeExtensionNode; + var child = node.Children[i] as TagHelperPropertyIntermediateNode; if (child != null && child.TagHelper != null && child.TagHelper == attributeNode.TagHelper && @@ -408,7 +472,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor return false; } - private static IntermediateToken GetAttributeContent(ComponentAttributeExtensionNode node) + private static IntermediateToken GetAttributeContent(TagHelperPropertyIntermediateNode node) { if (node.Children[0] is HtmlContentIntermediateNode htmlContentNode) { diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorDesignTimeNodeWriter.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorDesignTimeNodeWriter.cs index 8b90ffcabf..ffb96ef5b0 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorDesignTimeNodeWriter.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorDesignTimeNodeWriter.cs @@ -17,6 +17,21 @@ namespace Microsoft.AspNetCore.Blazor.Razor private readonly static string DesignTimeVariable = "__o"; + public override void WriteHtmlElement(CodeRenderingContext context, HtmlElementIntermediateNode node) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + context.RenderChildren(node); + } + public override void WriteUsingDirective(CodeRenderingContext context, UsingDirectiveIntermediateNode node) { if (context == null) @@ -194,7 +209,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor throw new ArgumentNullException(nameof(node)); } - context.RenderChildren(node); + // Do nothing, this can't contain code. } public override void WriteCSharpExpressionAttributeValue(CodeRenderingContext context, CSharpExpressionAttributeValueIntermediateNode node) @@ -264,60 +279,6 @@ namespace Microsoft.AspNetCore.Blazor.Razor } } - public override void WriteCSharpCodeAttributeValue(CodeRenderingContext context, CSharpCodeAttributeValueIntermediateNode node) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (node == null) - { - throw new ArgumentNullException(nameof(node)); - } - - for (var i = 0; i < node.Children.Count; i++) - { - if (node.Children[i] is IntermediateToken token && token.IsCSharp) - { - IDisposable linePragmaScope = null; - var isWhitespaceStatement = string.IsNullOrWhiteSpace(token.Content); - - if (token.Source != null) - { - if (!isWhitespaceStatement) - { - linePragmaScope = context.CodeWriter.BuildLinePragma(token.Source.Value); - } - - context.CodeWriter.WritePadding(0, token.Source.Value, context); - } - else if (isWhitespaceStatement) - { - // Don't write whitespace if there is no line mapping for it. - continue; - } - - context.AddSourceMappingFor(token); - context.CodeWriter.Write(token.Content); - - if (linePragmaScope != null) - { - linePragmaScope.Dispose(); - } - else - { - context.CodeWriter.WriteLine(); - } - } - else - { - // There may be something else inside the statement like an extension node. - context.RenderNode(node.Children[i]); - } - } - } - public override void WriteHtmlContent(CodeRenderingContext context, HtmlContentIntermediateNode node) { if (context == null) @@ -335,6 +296,16 @@ namespace Microsoft.AspNetCore.Blazor.Razor public override void BeginWriteAttribute(CodeWriter codeWriter, string key) { + if (codeWriter == null) + { + throw new ArgumentNullException(nameof(codeWriter)); + } + + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + codeWriter .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(BlazorApi.RenderTreeBuilder.AddAttribute)}") .Write("-1") @@ -343,7 +314,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor .WriteParameterSeparator(); } - public override void WriteComponentOpen(CodeRenderingContext context, ComponentOpenExtensionNode node) + public override void WriteComponent(CodeRenderingContext context, ComponentExtensionNode node) { if (context == null) { @@ -355,34 +326,9 @@ namespace Microsoft.AspNetCore.Blazor.Razor throw new ArgumentNullException(nameof(node)); } - // Do nothing - } - - public override void WriteComponentClose(CodeRenderingContext context, ComponentCloseExtensionNode node) - { - if (context == null) + foreach (var attribute in node.Attributes) { - throw new ArgumentNullException(nameof(context)); - } - - if (node == null) - { - throw new ArgumentNullException(nameof(node)); - } - - // Do nothing - } - - public override void WriteComponentBody(CodeRenderingContext context, ComponentBodyExtensionNode node) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (node == null) - { - throw new ArgumentNullException(nameof(node)); + context.RenderNode(attribute); } // We need to be aware of the blazor scope-tracking concept in design-time code generation @@ -391,8 +337,17 @@ namespace Microsoft.AspNetCore.Blazor.Razor // We're hacking it a bit here by just forcing every component to have an empty lambda _scopeStack.OpenScope(node.TagName, isComponent: true); _scopeStack.IncrementCurrentScopeChildCount(context); - context.RenderChildren(node); - _scopeStack.CloseScope(context, node.TagName, isComponent: true, source: node.Source); + + foreach (var child in node.Body) + { + context.RenderNode(child); + } + _scopeStack.CloseScope(context); + + foreach (var capture in node.Captures) + { + context.RenderNode(capture); + } } public override void WriteComponentAttribute(CodeRenderingContext context, ComponentAttributeExtensionNode node) @@ -440,7 +395,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor context.CodeWriter.Write(node.BoundAttribute.TypeName); context.CodeWriter.Write("("); context.CodeWriter.WriteLine(); - + for (var i = 0; i < tokens.Count; i++) { WriteCSharpToken(context, (IntermediateToken)tokens[i]); diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorDiagnosticFactory.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorDiagnosticFactory.cs index fc92a6c7df..fcf3e1f288 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorDiagnosticFactory.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorDiagnosticFactory.cs @@ -30,23 +30,14 @@ namespace Microsoft.AspNetCore.Blazor.Razor return diagnostic; } - public static readonly RazorDiagnosticDescriptor ExpressionInAttributeList = - new RazorDiagnosticDescriptor( + public static readonly RazorDiagnosticDescriptor UnclosedTag = new RazorDiagnosticDescriptor( "BL9980", - () => - "Expressions like '{0}' inside of a tag must be part of an attribute. " + - "Previous releases of Blazor supported constructs like '@onclick(...)' or '@bind(...)'." + - "These features have been changed to use attribute syntax. " + - "Use 'onclick=\"@...\"' or 'bind=\"...\" respectively.", + () => "Unclosed tag '{0}' with no matching end tag.", RazorDiagnosticSeverity.Error); - public static RazorDiagnostic Create_ExpressionInAttributeList(SourceSpan? source, string expression) + public static RazorDiagnostic Create_UnclosedTag(SourceSpan? span, string tagName) { - var diagnostic = RazorDiagnostic.Create( - ExpressionInAttributeList, - source ?? SourceSpan.Undefined, - expression); - return diagnostic; + return RazorDiagnostic.Create(UnclosedTag, span ?? SourceSpan.Undefined, tagName); } public static readonly RazorDiagnosticDescriptor UnexpectedClosingTag = new RazorDiagnosticDescriptor( @@ -69,14 +60,14 @@ namespace Microsoft.AspNetCore.Blazor.Razor return RazorDiagnostic.Create(MismatchedClosingTag, span ?? SourceSpan.Undefined, expectedTagName, tagName); } - public static readonly RazorDiagnosticDescriptor MismatchedClosingTagKind = new RazorDiagnosticDescriptor( + public static readonly RazorDiagnosticDescriptor InvalidHtmlContent = new RazorDiagnosticDescriptor( "BL9984", - () => "Mismatching closing tag. Found '{0}' of type '{1}' but expected type '{2}'.", + () => "Found invalid HTML content. Text '{0}'", RazorDiagnosticSeverity.Error); - public static RazorDiagnostic Create_MismatchedClosingTagKind(SourceSpan? span, string tagName, string kind, string expectedKind) + public static RazorDiagnostic Create_InvalidHtmlContent(SourceSpan? span, string text) { - return RazorDiagnostic.Create(MismatchedClosingTagKind, span ?? SourceSpan.Undefined, tagName, kind, expectedKind); + return RazorDiagnostic.Create(InvalidHtmlContent, span ?? SourceSpan.Undefined, text); } public static readonly RazorDiagnosticDescriptor MultipleComponents = new RazorDiagnosticDescriptor( @@ -132,7 +123,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor () => "The attribute '{0}' was matched by multiple bind attributes. Duplicates:{1}", RazorDiagnosticSeverity.Error); - public static RazorDiagnostic CreateBindAttribute_Duplicates(SourceSpan? source, string attribute, ComponentAttributeExtensionNode[] attributes) + public static RazorDiagnostic CreateBindAttribute_Duplicates(SourceSpan? source, string attribute, TagHelperPropertyIntermediateNode[] attributes) { var diagnostic = RazorDiagnostic.Create( BindAttribute_Duplicates, @@ -148,7 +139,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor () => "The attribute '{0}' was matched by multiple event handlers attributes. Duplicates:{1}", RazorDiagnosticSeverity.Error); - public static RazorDiagnostic CreateEventHandler_Duplicates(SourceSpan? source, string attribute, ComponentAttributeExtensionNode[] attributes) + public static RazorDiagnostic CreateEventHandler_Duplicates(SourceSpan? source, string attribute, TagHelperPropertyIntermediateNode[] attributes) { var diagnostic = RazorDiagnostic.Create( EventHandler_Duplicates, diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorExtensionInitializer.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorExtensionInitializer.cs index 4e9d526ca8..86290e1d18 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorExtensionInitializer.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorExtensionInitializer.cs @@ -72,12 +72,13 @@ namespace Microsoft.AspNetCore.Blazor.Razor builder.Features.Add(new TrimWhitespacePass()); } builder.Features.Add(new ComponentDocumentClassifierPass()); + builder.Features.Add(new ComponentDocumentRewritePass()); + builder.Features.Add(new ScriptTagPass()); builder.Features.Add(new ComplexAttributeContentPass()); - builder.Features.Add(new BindLoweringPass()); - builder.Features.Add(new EventHandlerLoweringPass()); builder.Features.Add(new ComponentLoweringPass()); + builder.Features.Add(new EventHandlerLoweringPass()); builder.Features.Add(new RefLoweringPass()); - builder.Features.Add(new OrphanTagHelperLoweringPass()); + builder.Features.Add(new BindLoweringPass()); builder.Features.Add(new ComponentTagHelperDescriptorProvider()); builder.Features.Add(new BindTagHelperDescriptorProvider()); diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorNodeWriter.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorNodeWriter.cs index 198a6e6a77..aa03c46b6e 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorNodeWriter.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorNodeWriter.cs @@ -2,12 +2,24 @@ // 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.CodeGeneration; +using Microsoft.AspNetCore.Razor.Language.Intermediate; namespace Microsoft.AspNetCore.Blazor.Razor { internal abstract class BlazorNodeWriter : IntermediateNodeWriter { + public abstract void BeginWriteAttribute(CodeWriter codeWriter, string key); + + public abstract void WriteComponent(CodeRenderingContext context, ComponentExtensionNode node); + + public abstract void WriteComponentAttribute(CodeRenderingContext context, ComponentAttributeExtensionNode node); + + public abstract void WriteHtmlElement(CodeRenderingContext context, HtmlElementIntermediateNode node); + + public abstract void WriteReferenceCapture(CodeRenderingContext context, RefExtensionNode node); + public sealed override void BeginWriterScope(CodeRenderingContext context, string writer) { throw new NotImplementedException(nameof(BeginWriterScope)); @@ -18,16 +30,15 @@ namespace Microsoft.AspNetCore.Blazor.Razor throw new NotImplementedException(nameof(EndWriterScope)); } - public abstract void BeginWriteAttribute(CodeWriter codeWriter, string key); - - public abstract void WriteComponentOpen(CodeRenderingContext context, ComponentOpenExtensionNode node); - - public abstract void WriteComponentClose(CodeRenderingContext context, ComponentCloseExtensionNode node); - - public abstract void WriteComponentBody(CodeRenderingContext context, ComponentBodyExtensionNode node); - - public abstract void WriteComponentAttribute(CodeRenderingContext context, ComponentAttributeExtensionNode node); - - public abstract void WriteReferenceCapture(CodeRenderingContext context, RefExtensionNode node); + public sealed override void WriteCSharpCodeAttributeValue(CodeRenderingContext context, CSharpCodeAttributeValueIntermediateNode node) + { + // We used to support syntaxes like but this is no longer the + // case. + // + // We provide an error for this case just to be friendly. + var content = string.Join("", node.Children.OfType().Select(t => t.Content)); + context.Diagnostics.Add(BlazorDiagnosticFactory.Create_CodeBlockInAttribute(node.Source, content)); + return; + } } } diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorRuntimeNodeWriter.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorRuntimeNodeWriter.cs index aab6d37186..ab28e3ee55 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorRuntimeNodeWriter.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorRuntimeNodeWriter.cs @@ -3,11 +3,9 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Text; -using AngleSharp; -using AngleSharp.Html; -using AngleSharp.Parser.Html; using Microsoft.AspNetCore.Blazor.Shared; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.CodeGeneration; @@ -20,25 +18,10 @@ namespace Microsoft.AspNetCore.Blazor.Razor /// internal class BlazorRuntimeNodeWriter : BlazorNodeWriter { - // Per the HTML spec, the following elements are inherently self-closing - // For example, is the same as (and therefore it cannot contain descendants) - private readonly static HashSet htmlVoidElementsLookup - = new HashSet( - new[] { "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr" }, - StringComparer.OrdinalIgnoreCase); - + private readonly List _currentAttributeValues = new List(); private readonly ScopeStack _scopeStack = new ScopeStack(); - private string _unconsumedHtml; - private List _currentAttributeValues; - private IDictionary _currentElementAttributes = new Dictionary(); - private List _currentElementRefCaptures = new List(); private int _sourceSequence = 0; - private struct PendingAttribute - { - public List Values { get; set; } - } - public override void WriteCSharpCode(CodeRenderingContext context, CSharpCodeIntermediateNode node) { if (context == null) @@ -102,34 +85,16 @@ namespace Microsoft.AspNetCore.Blazor.Razor } } - public override void WriteCSharpCodeAttributeValue(CodeRenderingContext context, CSharpCodeAttributeValueIntermediateNode node) - { - if (_currentAttributeValues == null) - { - throw new InvalidOperationException($"Invoked {nameof(WriteCSharpCodeAttributeValue)} while {nameof(_currentAttributeValues)} was null."); - } - - // We used to support syntaxes like but this is no longer the - // case. - // - // We provide an error for this case just to be friendly. - var content = string.Join("", node.Children.OfType().Select(t => t.Content)); - context.Diagnostics.Add(BlazorDiagnosticFactory.Create_CodeBlockInAttribute(node.Source, content)); - return; - } - public override void WriteCSharpExpression(CodeRenderingContext context, CSharpExpressionIntermediateNode node) { - // We used to support syntaxes like but this is no longer the case. - // The APIs that a user would need to do this correctly aren't accessible outside of Blazor's core - // anyway. - // - // We provide an error for this case just to be friendly. - if (_unconsumedHtml != null) + if (context == null) { - var content = string.Join("", node.Children.OfType().Select(t => t.Content)); - context.Diagnostics.Add(BlazorDiagnosticFactory.Create_ExpressionInAttributeList(node.Source, content)); - return; + throw new ArgumentNullException(nameof(context)); + } + + if (node == null) + { + throw new ArgumentNullException(nameof(node)); } // Since we're not in the middle of writing an element, this must evaluate as some @@ -153,15 +118,19 @@ namespace Microsoft.AspNetCore.Blazor.Razor } } - context.CodeWriter - .WriteEndMethodInvocation(); + context.CodeWriter.WriteEndMethodInvocation(); } public override void WriteCSharpExpressionAttributeValue(CodeRenderingContext context, CSharpExpressionAttributeValueIntermediateNode node) { - if (_currentAttributeValues == null) + if (context == null) { - throw new InvalidOperationException($"Invoked {nameof(WriteCSharpCodeAttributeValue)} while {nameof(_currentAttributeValues)} was null."); + throw new ArgumentNullException(nameof(context)); + } + + if (node == null) + { + throw new ArgumentNullException(nameof(node)); } // In cases like "somestring @variable", Razor tokenizes it as: @@ -179,22 +148,80 @@ namespace Microsoft.AspNetCore.Blazor.Razor } } + public override void WriteHtmlElement(CodeRenderingContext context, HtmlElementIntermediateNode node) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + context.CodeWriter + .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(BlazorApi.RenderTreeBuilder.OpenElement)}") + .Write((_sourceSequence++).ToString()) + .WriteParameterSeparator() + .WriteStringLiteral(node.TagName) + .WriteEndMethodInvocation(); + + // Render Attributes before creating the scope. + foreach (var attribute in node.Attributes) + { + context.RenderNode(attribute); + } + + foreach (var capture in node.Captures) + { + context.RenderNode(capture); + } + + _scopeStack.OpenScope(tagName: node.TagName, isComponent: false); + + // Render body of the tag inside the scope + foreach (var child in node.Body) + { + context.RenderNode(child); + } + + _scopeStack.CloseScope(context); + + context.CodeWriter + .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{BlazorApi.RenderTreeBuilder.CloseElement}") + .WriteEndMethodInvocation(); + } + public override void WriteHtmlAttribute(CodeRenderingContext context, HtmlAttributeIntermediateNode node) { - _currentAttributeValues = new List(); - context.RenderChildren(node); - _currentElementAttributes[node.AttributeName] = new PendingAttribute + if (context == null) { - Values = _currentAttributeValues, - }; - _currentAttributeValues = null; + throw new ArgumentNullException(nameof(context)); + } + + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + Debug.Assert(_currentAttributeValues.Count == 0); + context.RenderChildren(node); + + WriteAttribute(context.CodeWriter, node.AttributeName, _currentAttributeValues); + _currentAttributeValues.Clear(); } public override void WriteHtmlAttributeValue(CodeRenderingContext context, HtmlAttributeValueIntermediateNode node) { - if (_currentAttributeValues == null) + if (context == null) { - throw new InvalidOperationException($"Invoked {nameof(WriteHtmlAttributeValue)} while {nameof(_currentAttributeValues)} was null."); + throw new ArgumentNullException(nameof(context)); + } + + if (node == null) + { + throw new ArgumentNullException(nameof(node)); } var stringContent = ((IntermediateToken)node.Children.Single()).Content; @@ -203,179 +230,43 @@ namespace Microsoft.AspNetCore.Blazor.Razor public override void WriteHtmlContent(CodeRenderingContext context, HtmlContentIntermediateNode node) { - var originalHtmlContent = GetContent(node); - if (_unconsumedHtml != null) + if (context == null) { - originalHtmlContent = _unconsumedHtml + originalHtmlContent; - _unconsumedHtml = null; + throw new ArgumentNullException(nameof(context)); } - var tokenizer = new HtmlTokenizer( - new TextSource(originalHtmlContent), - HtmlEntityService.Resolver); - var codeWriter = context.CodeWriter; - - // TODO: As an optimization, identify static subtrees (i.e., HTML elements in the Razor source - // that contain no C#) and represent them as a new RenderTreeFrameType called StaticElement or - // similar. This means you can have arbitrarily deep static subtrees without paying any per- - // node cost during rendering or diffing. - HtmlToken nextToken; - while ((nextToken = tokenizer.Get()).Type != HtmlTokenType.EndOfFile) + if (node == null) { - switch (nextToken.Type) - { - case HtmlTokenType.Character: - { - // Text node - _scopeStack.IncrementCurrentScopeChildCount(context); - codeWriter - .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(BlazorApi.RenderTreeBuilder.AddContent)}") - .Write((_sourceSequence++).ToString()) - .WriteParameterSeparator() - .WriteStringLiteral(nextToken.Data) - .WriteEndMethodInvocation(); - break; - } - - case HtmlTokenType.StartTag: - case HtmlTokenType.EndTag: - { - var nextTag = nextToken.AsTag(); - var tagNameOriginalCase = GetTagNameWithOriginalCase(originalHtmlContent, nextTag); - - if (nextToken.Type == HtmlTokenType.StartTag) - { - RejectDisallowedHtmlTags(node, nextTag); - - _scopeStack.IncrementCurrentScopeChildCount(context); - - codeWriter - .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(BlazorApi.RenderTreeBuilder.OpenElement)}") - .Write((_sourceSequence++).ToString()) - .WriteParameterSeparator() - .WriteStringLiteral(nextTag.Data) - .WriteEndMethodInvocation(); - - foreach (var attribute in nextTag.Attributes) - { - var token = new IntermediateToken() { Kind = TokenKind.Html, Content = attribute.Value }; - WriteAttribute(codeWriter, attribute.Key, new[] { token }); - } - - if (_currentElementAttributes.Count > 0) - { - foreach (var pair in _currentElementAttributes) - { - WriteAttribute(codeWriter, pair.Key, pair.Value.Values); - } - _currentElementAttributes.Clear(); - } - - if (_currentElementRefCaptures.Count > 0) - { - foreach (var refNode in _currentElementRefCaptures) - { - WriteAddReferenceCaptureCall(context, refNode); - } - _currentElementRefCaptures.Clear(); - } - - _scopeStack.OpenScope( tagName: nextTag.Data, isComponent: false); - } - - if (nextToken.Type == HtmlTokenType.EndTag - || nextTag.IsSelfClosing - || htmlVoidElementsLookup.Contains(nextTag.Data)) - { - _scopeStack.CloseScope( - context: context, - tagName: nextTag.Data, - isComponent: false, - source: CalculateSourcePosition(node.Source, nextToken.Position)); - codeWriter - .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{BlazorApi.RenderTreeBuilder.CloseElement}") - .WriteEndMethodInvocation(); - } - break; - } - - case HtmlTokenType.Comment: - break; - - default: - throw new InvalidCastException($"Unsupported token type: {nextToken.Type.ToString()}"); - } + throw new ArgumentNullException(nameof(node)); } - // If we got an EOF in the middle of an HTML element, it's probably because we're - // about to receive some attribute name/value pairs. Store the unused HTML content - // so we can prepend it to the part that comes after the attributes to make - // complete valid markup. - if (originalHtmlContent.Length > nextToken.Position.Position) - { - _unconsumedHtml = originalHtmlContent.Substring(nextToken.Position.Position - 1); - } - } - - private void WriteAddReferenceCaptureCall(CodeRenderingContext context, RefExtensionNode refNode) - { - var codeWriter = context.CodeWriter; - - var methodName = refNode.IsComponentCapture - ? nameof(BlazorApi.RenderTreeBuilder.AddComponentReferenceCapture) - : nameof(BlazorApi.RenderTreeBuilder.AddElementReferenceCapture); - codeWriter - .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{methodName}") + // Text node + var content = GetHtmlContent(node); + _scopeStack.IncrementCurrentScopeChildCount(context); + context.CodeWriter + .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(BlazorApi.RenderTreeBuilder.AddContent)}") .Write((_sourceSequence++).ToString()) - .WriteParameterSeparator(); - - const string refCaptureParamName = "__value"; - using (var lambdaScope = codeWriter.BuildLambda(refCaptureParamName)) - { - var typecastIfNeeded = refNode.IsComponentCapture ? $"({refNode.ComponentCaptureTypeName})" : string.Empty; - WriteCSharpCode(context, new CSharpCodeIntermediateNode - { - Source = refNode.Source, - Children = - { - refNode.IdentifierToken, - new IntermediateToken { - Kind = TokenKind.CSharp, - Content = $" = {typecastIfNeeded}{refCaptureParamName};" - } - } - }); - } - - codeWriter.WriteEndMethodInvocation(); - } - - private void RejectDisallowedHtmlTags(IntermediateNode node, HtmlTagToken tagToken) - { - // Disallow - IntermediateToken - (136:4,13 [2] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n - IntermediateToken - (138:5,0 [6] x:\dir\subdir\Test\TestComponent.cshtml) - Html - + HtmlElement - (0:0,0 [144] x:\dir\subdir\Test\TestComponent.cshtml) - div + HtmlContent - (5:0,5 [6] x:\dir\subdir\Test\TestComponent.cshtml) + IntermediateToken - (5:0,5 [6] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n + HtmlElement - (11:1,4 [125] x:\dir\subdir\Test\TestComponent.cshtml) - script + HtmlAttribute - - - + HtmlAttributeValue - - + IntermediateToken - - Html - some/url.js + HtmlAttribute - - - + HtmlAttributeValue - - + IntermediateToken - - Html - + HtmlContent - (78:1,71 [49] x:\dir\subdir\Test\TestComponent.cshtml) + IntermediateToken - (78:1,71 [49] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n some text\n some more text\n + HtmlContent - (136:4,13 [2] x:\dir\subdir\Test\TestComponent.cshtml) + IntermediateToken - (136:4,13 [2] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithCSharpExpression/TestComponent.ir.txt b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithCSharpExpression/TestComponent.ir.txt index 51819fa264..9e4744ba27 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithCSharpExpression/TestComponent.ir.txt +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithCSharpExpression/TestComponent.ir.txt @@ -8,10 +8,10 @@ Document - MethodDeclaration - - protected override - void - BuildRenderTree CSharpCode - IntermediateToken - - CSharp - base.BuildRenderTree(builder); - HtmlContent - (0:0,0 [18] x:\dir\subdir\Test\TestComponent.cshtml) - IntermediateToken - (0:0,0 [4] x:\dir\subdir\Test\TestComponent.cshtml) - Html -

- IntermediateToken - (4:0,4 [5] x:\dir\subdir\Test\TestComponent.cshtml) - Html - Hello - IntermediateToken - (9:0,9 [5] x:\dir\subdir\Test\TestComponent.cshtml) - Html -

+ HtmlElement - (0:0,0 [14] x:\dir\subdir\Test\TestComponent.cshtml) - h1 + HtmlContent - (4:0,4 [5] x:\dir\subdir\Test\TestComponent.cshtml) + IntermediateToken - (4:0,4 [5] x:\dir\subdir\Test\TestComponent.cshtml) - Html - Hello + HtmlContent - (14:0,14 [4] x:\dir\subdir\Test\TestComponent.cshtml) IntermediateToken - (14:0,14 [4] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n\n CSharpExpression - (20:2,2 [10] x:\dir\subdir\Test\TestComponent.cshtml) IntermediateToken - (20:2,2 [10] x:\dir\subdir\Test\TestComponent.cshtml) - CSharp - "My value" diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithComponent/TestComponent.ir.txt b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithComponent/TestComponent.ir.txt index e912a49e9f..e0bfe1b5ee 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithComponent/TestComponent.ir.txt +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithComponent/TestComponent.ir.txt @@ -8,12 +8,9 @@ Document - MethodDeclaration - - protected override - void - BuildRenderTree CSharpCode - IntermediateToken - - CSharp - base.BuildRenderTree(builder); - HtmlContent - (31:1,0 [18] x:\dir\subdir\Test\TestComponent.cshtml) - IntermediateToken - (31:1,0 [4] x:\dir\subdir\Test\TestComponent.cshtml) - Html -

- IntermediateToken - (35:1,4 [5] x:\dir\subdir\Test\TestComponent.cshtml) - Html - Hello - IntermediateToken - (40:1,9 [5] x:\dir\subdir\Test\TestComponent.cshtml) - Html -

+ HtmlElement - (31:1,0 [14] x:\dir\subdir\Test\TestComponent.cshtml) - h1 + HtmlContent - (35:1,4 [5] x:\dir\subdir\Test\TestComponent.cshtml) + IntermediateToken - (35:1,4 [5] x:\dir\subdir\Test\TestComponent.cshtml) - Html - Hello + HtmlContent - (45:1,14 [4] x:\dir\subdir\Test\TestComponent.cshtml) IntermediateToken - (45:1,14 [4] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n\n - TagHelper - (49:3,0 [22] x:\dir\subdir\Test\TestComponent.cshtml) - SomeOtherComponent - TagMode.SelfClosing - ComponentOpenExtensionNode - - Test.SomeOtherComponent - ComponentBodyExtensionNode - - ComponentCloseExtensionNode - + ComponentExtensionNode - (49:3,0 [22] x:\dir\subdir\Test\TestComponent.cshtml) - SomeOtherComponent - Test.SomeOtherComponent diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithDirective/TestComponent.ir.txt b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithDirective/TestComponent.ir.txt index 3feef6521b..e140153564 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithDirective/TestComponent.ir.txt +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithDirective/TestComponent.ir.txt @@ -9,7 +9,6 @@ Document - MethodDeclaration - - protected override - void - BuildRenderTree CSharpCode - IntermediateToken - - CSharp - base.BuildRenderTree(builder); - HtmlContent - (0:0,0 [18] x:\dir\subdir\Test\TestComponent.cshtml) - IntermediateToken - (0:0,0 [4] x:\dir\subdir\Test\TestComponent.cshtml) - Html -

- IntermediateToken - (4:0,4 [5] x:\dir\subdir\Test\TestComponent.cshtml) - Html - Hello - IntermediateToken - (9:0,9 [5] x:\dir\subdir\Test\TestComponent.cshtml) - Html -

+ HtmlElement - (0:0,0 [14] x:\dir\subdir\Test\TestComponent.cshtml) - h1 + HtmlContent - (4:0,4 [5] x:\dir\subdir\Test\TestComponent.cshtml) + IntermediateToken - (4:0,4 [5] x:\dir\subdir\Test\TestComponent.cshtml) - Html - Hello diff --git a/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/ComponentDocumentRewritePassTest.cs b/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/ComponentDocumentRewritePassTest.cs new file mode 100644 index 0000000000..6faab2841e --- /dev/null +++ b/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/ComponentDocumentRewritePassTest.cs @@ -0,0 +1,446 @@ +// 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.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Intermediate; +using Xunit; + + +namespace Microsoft.AspNetCore.Blazor.Razor +{ + public class ComponentDocumentRewritePassTest + { + public ComponentDocumentRewritePassTest() + { + var test = TagHelperDescriptorBuilder.Create("test", "test"); + test.TagMatchingRule(b => b.TagName = "test"); + + TagHelpers = new List() + { + test.Build(), + }; + + Pass = new ComponentDocumentRewritePass(); + Engine = RazorProjectEngine.Create( + BlazorExtensionInitializer.DefaultConfiguration, + RazorProjectFileSystem.Create(Environment.CurrentDirectory), + b => + { + b.Features.Add(new ComponentDocumentClassifierPass()); + b.Features.Add(Pass); + b.Features.Add(new StaticTagHelperFeature() { TagHelpers = TagHelpers, }); + }).Engine; + } + + private RazorEngine Engine { get; } + + private ComponentDocumentRewritePass Pass { get; } + + private List TagHelpers { get; } + + [Fact] + public void Execute_RewritesHtml_Basic() + { + // Arrange + var document = CreateDocument(@" + + + Hello, World! + +"); + + var documentNode = Lower(document); + + // Act + Pass.Execute(document, documentNode); + + // Assert + var method = documentNode.FindPrimaryMethod(); + Assert.Collection( + method.Children, + c => Assert.IsType(c), + c => NodeAssert.Whitespace(c), + c => NodeAssert.Element(c, "html")); + + var html = NodeAssert.Element(method.Children[2], "html"); + Assert.Equal(2, html.Source.Value.AbsoluteIndex); + Assert.Equal(1, html.Source.Value.LineIndex); + Assert.Equal(0, html.Source.Value.CharacterIndex); + Assert.Equal(68, html.Source.Value.Length); + Assert.Collection( + html.Children, + c => NodeAssert.Whitespace(c), + c => NodeAssert.Element(c, "head"), + c => NodeAssert.Whitespace(c)); + + var head = NodeAssert.Element(html.Children[1], "head"); + Assert.Equal(12, head.Source.Value.AbsoluteIndex); + Assert.Equal(2, head.Source.Value.LineIndex); + Assert.Equal(2, head.Source.Value.CharacterIndex); + Assert.Equal(49, head.Source.Value.Length); + Assert.Collection( + head.Children, + c => NodeAssert.Attribute(c, "cool", "beans"), + c => NodeAssert.Content(c, "Hello, World!")); + } + + [Fact] + public void Execute_RewritesHtml_Mixed() + { + // Arrange + var document = CreateDocument(@" + + + +"); + + var documentNode = Lower(document); + + // Act + Pass.Execute(document, documentNode); + + // Assert + var method = documentNode.FindPrimaryMethod(); + Assert.Collection( + method.Children, + c => Assert.IsType(c), + c => NodeAssert.Whitespace(c), + c => NodeAssert.Element(c, "html")); + + var html = NodeAssert.Element(method.Children[2], "html"); + Assert.Equal(2, html.Source.Value.AbsoluteIndex); + Assert.Equal(1, html.Source.Value.LineIndex); + Assert.Equal(0, html.Source.Value.CharacterIndex); + Assert.Equal(81, html.Source.Value.Length); + Assert.Collection( + html.Children, + c => NodeAssert.Whitespace(c), + c => NodeAssert.Element(c, "head"), + c => NodeAssert.Whitespace(c)); + + var head = NodeAssert.Element(html.Children[1], "head"); + Assert.Equal(12, head.Source.Value.AbsoluteIndex); + Assert.Equal(2, head.Source.Value.LineIndex); + Assert.Equal(2, head.Source.Value.CharacterIndex); + Assert.Equal(62, head.Source.Value.Length); + Assert.Collection( + head.Children, + c => NodeAssert.Attribute(c, "cool", "beans"), + c => NodeAssert.CSharpAttribute(c, "csharp", "yes"), + c => Assert.IsType(c), + c => NodeAssert.Whitespace(c)); + + var mixed = Assert.IsType(head.Children[2]); + Assert.Collection( + mixed.Children, + c => Assert.IsType(c), + c => Assert.IsType(c)); + } + + [Fact] + public void Execute_RewritesHtml_WithCode() + { + // Arrange + var document = CreateDocument(@" + + @if (some_bool) + { + + @hello + + } +"); + + var documentNode = Lower(document); + + // Act + Pass.Execute(document, documentNode); + + // Assert + var method = documentNode.FindPrimaryMethod(); + Assert.Collection( + method.Children, + c => Assert.IsType(c), + c => NodeAssert.Whitespace(c), + c => NodeAssert.Element(c, "html")); + + var html = NodeAssert.Element(method.Children[2], "html"); + Assert.Equal(2, html.Source.Value.AbsoluteIndex); + Assert.Equal(1, html.Source.Value.LineIndex); + Assert.Equal(0, html.Source.Value.CharacterIndex); + Assert.Equal(90, html.Source.Value.Length); + Assert.Collection( + html.Children, + c => NodeAssert.Whitespace(c), + c => Assert.IsType(c), + c => Assert.IsType(c), + c => NodeAssert.Whitespace(c), + c => NodeAssert.Element(c, "head"), + c => NodeAssert.Whitespace(c), + c => Assert.IsType(c)); + + var head = NodeAssert.Element(html.Children[4], "head"); + Assert.Equal(36, head.Source.Value.AbsoluteIndex); + Assert.Equal(4, head.Source.Value.LineIndex); + Assert.Equal(2, head.Source.Value.CharacterIndex); + Assert.Equal(42, head.Source.Value.Length); + Assert.Collection( + head.Children, + c => NodeAssert.Attribute(c, "cool", "beans"), + c => NodeAssert.Whitespace(c), + c => Assert.IsType(c), + c => NodeAssert.Whitespace(c)); + } + + [Fact] + public void Execute_RewritesHtml_TagHelper() + { + // Arrange + var document = CreateDocument(@" +@addTagHelper ""*, test"" + + + + Hello, World! + + +"); + + var documentNode = Lower(document); + + // Act + Pass.Execute(document, documentNode); + + // Assert + var method = documentNode.FindPrimaryMethod(); + Assert.Collection( + method.Children, + c => Assert.IsType(c), + c => NodeAssert.Whitespace(c), + c => Assert.IsType(c), + c => NodeAssert.Element(c, "html")); + + var html = NodeAssert.Element(method.Children[3], "html"); + Assert.Equal(27, html.Source.Value.AbsoluteIndex); + Assert.Equal(2, html.Source.Value.LineIndex); + Assert.Equal(0, html.Source.Value.CharacterIndex); + Assert.Equal(95, html.Source.Value.Length); + Assert.Collection( + html.Children, + c => NodeAssert.Whitespace(c), + c => Assert.IsType(c), + c => NodeAssert.Whitespace(c)); + + var body = html.Children + .OfType().Single().Children + .OfType().Single(); + + Assert.Collection( + body.Children, + c => NodeAssert.Whitespace(c), + c => NodeAssert.Element(c, "head"), + c => NodeAssert.Whitespace(c)); + + var head = body.Children[1]; + Assert.Equal(49, head.Source.Value.AbsoluteIndex); + Assert.Equal(4, head.Source.Value.LineIndex); + Assert.Equal(4, head.Source.Value.CharacterIndex); + Assert.Equal(53, head.Source.Value.Length); + Assert.Collection( + head.Children, + c => NodeAssert.Attribute(c, "cool", "beans"), + c => NodeAssert.Content(c, "Hello, World!")); + } + + [Fact] + public void Execute_RewritesHtml_UnbalancedClosingTagAtTopLevel() + { + // Arrange + var document = CreateDocument(@" +"); + + var documentNode = Lower(document); + + // Act + Pass.Execute(document, documentNode); + + // Assert + var method = documentNode.FindPrimaryMethod(); + Assert.Collection( + method.Children, + c => Assert.IsType(c), + c => NodeAssert.Whitespace(c), + c => NodeAssert.Element(c, "html")); + + var html = NodeAssert.Element(method.Children[2], "html"); + Assert.Equal(2, html.Source.Value.AbsoluteIndex); + Assert.Equal(1, html.Source.Value.LineIndex); + Assert.Equal(0, html.Source.Value.CharacterIndex); + Assert.Equal(7, html.Source.Value.Length); + + var diagnostic = Assert.Single(html.Diagnostics); + Assert.Same(BlazorDiagnosticFactory.UnexpectedClosingTag.Id, diagnostic.Id); + Assert.Equal(html.Source, diagnostic.Span); + } + + [Fact] + public void Execute_RewritesHtml_MismatchedClosingTag() + { + // Arrange + var document = CreateDocument(@" + +
+ +"); + + var documentNode = Lower(document); + + // Act + Pass.Execute(document, documentNode); + + // Assert + var method = documentNode.FindPrimaryMethod(); + Assert.Collection( + method.Children, + c => Assert.IsType(c), + c => NodeAssert.Whitespace(c), + c => NodeAssert.Element(c, "html")); + + var html = NodeAssert.Element(method.Children[2], "html"); + Assert.Collection( + html.Children, + c => NodeAssert.Whitespace(c), + c => NodeAssert.Element(c, "div"), + c => NodeAssert.Whitespace(c)); + + var div = NodeAssert.Element(html.Children[1], "div"); + Assert.Equal(12, div.Source.Value.AbsoluteIndex); + Assert.Equal(2, div.Source.Value.LineIndex); + Assert.Equal(2, div.Source.Value.CharacterIndex); + Assert.Equal(5, div.Source.Value.Length); + + var diagnostic = Assert.Single(div.Diagnostics); + Assert.Same(BlazorDiagnosticFactory.MismatchedClosingTag.Id, diagnostic.Id); + Assert.Equal(21,diagnostic.Span.AbsoluteIndex); + Assert.Equal(3, diagnostic.Span.LineIndex); + Assert.Equal(2, diagnostic.Span.CharacterIndex); + Assert.Equal(7, diagnostic.Span.Length); + } + + [Fact] + public void Execute_RewritesHtml_MalformedHtmlAtEnd() + { + // Arrange + var document = CreateDocument(@" + Assert.IsType(c), + c => NodeAssert.Whitespace(c), + c => NodeAssert.Content(c, " +
"); + + var documentNode = Lower(document); + + // Act + Pass.Execute(document, documentNode); + + // Assert + var method = documentNode.FindPrimaryMethod(); + Assert.Collection( + method.Children, + c => Assert.IsType(c), + c => NodeAssert.Whitespace(c), + c => NodeAssert.Element(c, "html")); + + var html = NodeAssert.Element(method.Children[2], "html"); + Assert.Collection( + html.Children, + c => NodeAssert.Whitespace(c), + c => NodeAssert.Element(c, "div")); + + var diagnostic = Assert.Single(html.Diagnostics); + Assert.Same(BlazorDiagnosticFactory.UnclosedTag.Id, diagnostic.Id); + Assert.Equal(2, diagnostic.Span.AbsoluteIndex); + Assert.Equal(1, diagnostic.Span.LineIndex); + Assert.Equal(0, diagnostic.Span.CharacterIndex); + Assert.Equal(6, diagnostic.Span.Length); + + var div = NodeAssert.Element(html.Children[1], "div"); + + diagnostic = Assert.Single(div.Diagnostics); + Assert.Same(BlazorDiagnosticFactory.UnclosedTag.Id, diagnostic.Id); + Assert.Equal(12, diagnostic.Span.AbsoluteIndex); + Assert.Equal(2, diagnostic.Span.LineIndex); + Assert.Equal(2, diagnostic.Span.CharacterIndex); + Assert.Equal(5, diagnostic.Span.Length); + } + + private RazorCodeDocument CreateDocument(string content) + { + // Normalize newlines since we are testing lengths of things. + content = content.Replace("\r", ""); + content = content.Replace("\n", "\r\n"); + + var source = RazorSourceDocument.Create(content, "test.cshtml"); + return RazorCodeDocument.Create(source); + } + + private DocumentIntermediateNode Lower(RazorCodeDocument codeDocument) + { + for (var i = 0; i < Engine.Phases.Count; i++) + { + var phase = Engine.Phases[i]; + if (phase is IRazorDocumentClassifierPhase) + { + break; + } + + phase.Execute(codeDocument); + } + + var document = codeDocument.GetDocumentIntermediateNode(); + Engine.Features.OfType().Single().Execute(codeDocument, document); + return document; + } + + private class StaticTagHelperFeature : ITagHelperFeature + { + public RazorEngine Engine { get; set; } + + public List TagHelpers { get; set; } + + public IReadOnlyList GetDescriptors() + { + return TagHelpers; + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test.csproj b/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test.csproj index f18032558d..9c9ce00ace 100644 --- a/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test.csproj +++ b/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test.csproj @@ -10,6 +10,7 @@ used to compile this assembly. --> true + 7.1 diff --git a/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/NodeAssert.cs b/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/NodeAssert.cs new file mode 100644 index 0000000000..f5b3a7b6fc --- /dev/null +++ b/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/NodeAssert.cs @@ -0,0 +1,126 @@ +// 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.Text; +using Microsoft.AspNetCore.Razor.Language.Intermediate; +using Xunit; + +namespace Microsoft.AspNetCore.Blazor.Razor +{ + internal static class NodeAssert + { + public static HtmlAttributeIntermediateNode Attribute(IntermediateNode node, string attributeName, string attributeValue) + { + Assert.NotNull(node); + + var attributeNode = Assert.IsType(node); + Assert.Equal(attributeName, attributeNode.AttributeName); + + var attributeValueNode = Assert.IsType(Assert.Single(attributeNode.Children)); + var actual = new StringBuilder(); + for (var i = 0; i < attributeValueNode.Children.Count; i++) + { + var token = Assert.IsType(attributeValueNode.Children[i]); + Assert.Equal(TokenKind.Html, token.Kind); + actual.Append(token.Content); + } + + Assert.Equal(attributeValue, actual.ToString()); + + return attributeNode; + } + + public static HtmlAttributeIntermediateNode Attribute(IntermediateNodeCollection nodes, string attributeName, string attributeValue) + { + Assert.NotNull(nodes); + return Attribute(Assert.Single(nodes), attributeName, attributeValue); + } + + public static HtmlContentIntermediateNode Content(IntermediateNode node, string content, bool trim = true) + { + Assert.NotNull(node); + + var contentNode = Assert.IsType(node); + + var actual = new StringBuilder(); + for (var i = 0; i < contentNode.Children.Count; i++) + { + var token = Assert.IsType(contentNode.Children[i]); + Assert.Equal(TokenKind.Html, token.Kind); + actual.Append(token.Content); + } + + Assert.Equal(content, trim ? actual.ToString().Trim() : actual.ToString()); + return contentNode; + } + + public static HtmlContentIntermediateNode Content(IntermediateNodeCollection nodes, string content, bool trim = true) + { + Assert.NotNull(nodes); + return Content(Assert.Single(nodes), content, trim); + } + + public static HtmlAttributeIntermediateNode CSharpAttribute(IntermediateNode node, string attributeName, string attributeValue) + { + Assert.NotNull(node); + + var attributeNode = Assert.IsType(node); + Assert.Equal(attributeName, attributeNode.AttributeName); + + var attributeValueNode = Assert.IsType(Assert.Single(attributeNode.Children)); + var actual = new StringBuilder(); + for (var i = 0; i < attributeValueNode.Children.Count; i++) + { + var token = Assert.IsType(attributeValueNode.Children[i]); + Assert.Equal(TokenKind.CSharp, token.Kind); + actual.Append(token.Content); + } + + Assert.Equal(attributeValue, actual.ToString()); + + return attributeNode; + } + + public static HtmlAttributeIntermediateNode CSharpAttribute(IntermediateNodeCollection nodes, string attributeName, string attributeValue) + { + Assert.NotNull(nodes); + return Attribute(Assert.Single(nodes), attributeName, attributeValue); + } + + public static HtmlElementIntermediateNode Element(IntermediateNode node, string tagName) + { + Assert.NotNull(node); + + var elementNode = Assert.IsType(node); + Assert.Equal(tagName, elementNode.TagName); + return elementNode; + } + + public static HtmlElementIntermediateNode Element(IntermediateNodeCollection nodes, string tagName) + { + Assert.NotNull(nodes); + return Element(Assert.Single(nodes), tagName); + } + + public static HtmlContentIntermediateNode Whitespace(IntermediateNode node) + { + Assert.NotNull(node); + + var contentNode = Assert.IsType(node); + for (var i = 0; i < contentNode.Children.Count; i++) + { + var token = Assert.IsType(contentNode.Children[i]); + Assert.Equal(TokenKind.Html, token.Kind); + Assert.True(string.IsNullOrWhiteSpace(token.Content)); + } + + return contentNode; + } + + public static HtmlContentIntermediateNode Whitespace(IntermediateNodeCollection nodes) + { + Assert.NotNull(nodes); + return Whitespace(Assert.Single(nodes)); + } + } +}