diff --git a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/IReadOnlyTagHelperAttribute.cs b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/IReadOnlyTagHelperAttribute.cs index 0114f895fb..cdf90a0dbd 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/IReadOnlyTagHelperAttribute.cs +++ b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/IReadOnlyTagHelperAttribute.cs @@ -19,5 +19,11 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers /// Gets the value of the attribute. /// object Value { get; } + + /// + /// Gets an indication whether the attribute is minimized or not. + /// + /// If true, will be ignored. + bool Minimized { get; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperAttribute.cs b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperAttribute.cs index a2f1e16133..96954d9346 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperAttribute.cs +++ b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperAttribute.cs @@ -41,6 +41,12 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers /// public object Value { get; set; } + /// + /// Gets or sets an indication whether the attribute is minimized or not. + /// + /// If true, will be ignored. + public bool Minimized { get; set; } + /// /// Converts the specified into a . /// @@ -61,7 +67,8 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers return other != null && string.Equals(Name, other.Name, StringComparison.OrdinalIgnoreCase) && - Equals(Value, other.Value); + Minimized == other.Minimized && + (Minimized || Equals(Value, other.Value)); } /// diff --git a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperExecutionContext.cs b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperExecutionContext.cs index d72e854f44..2fc571a4af 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperExecutionContext.cs +++ b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperExecutionContext.cs @@ -39,7 +39,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers /// The HTML tag name in the Razor source. /// /// indicating whether or not the tag in the Razor source was self-closing. - /// The collection of items used to communicate with other + /// The collection of items used to communicate with other /// s /// An identifier unique to the HTML element this context is for. /// A delegate used to execute the child content asynchronously. @@ -133,6 +133,26 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers _tagHelpers.Add(tagHelper); } + /// + /// Tracks the minimized HTML attribute in and . + /// + /// The minimized HTML attribute name. + public void AddMinimizedHtmlAttribute([NotNull] string name) + { + HTMLAttributes.Add( + new TagHelperAttribute + { + Name = name, + Minimized = true + }); + AllAttributes.Add( + new TagHelperAttribute + { + Name = name, + Minimized = true + }); + } + /// /// Tracks the HTML attribute in and . /// @@ -168,7 +188,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers /// /// A that on completion returns the rendered child content. /// - /// Child content is only executed once. Successive calls to this method or successive executions of the + /// Child content is only executed once. Successive calls to this method or successive executions of the /// returned return a cached result. /// public async Task GetChildContentAsync() diff --git a/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/CSharpTagHelperCodeRenderer.cs b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/CSharpTagHelperCodeRenderer.cs index 0de12eaece..910d806376 100644 --- a/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/CSharpTagHelperCodeRenderer.cs +++ b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/CSharpTagHelperCodeRenderer.cs @@ -203,6 +203,13 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp var firstAttribute = matchingAttributes.First(); var attributeValueChunk = firstAttribute.Value; + // Minimized attributes are not valid for bound attributes. There will be an error for the bound + // attribute logged by TagHelperBlockRewriter already so we can skip. + if (attributeValueChunk == null) + { + continue; + } + var attributeValueRecorded = htmlAttributeValues.ContainsKey(attributeDescriptor.Name); // Bufferable attributes are attributes that can have Razor code inside of them. @@ -323,15 +330,21 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp // Build out the unbound HTML attributes for the tag builder foreach (var htmlAttribute in unboundHTMLAttributes) { - string textValue; + string textValue = null; + var isPlainTextValue = false; var attributeValue = htmlAttribute.Value; - var isPlainTextValue = TryGetPlainTextValue(attributeValue, out textValue); - // HTML attributes are always strings. So if this value is not plain text i.e. if the value contains - // C# code, then we need to buffer it. - if (!isPlainTextValue) + // A null attribute value means the HTML attribute is minimized. + if (attributeValue != null) { - BuildBufferedWritingScope(attributeValue, htmlEncodeValues: true); + isPlainTextValue = TryGetPlainTextValue(attributeValue, out textValue); + + // HTML attributes are always strings. So if this value is not plain text i.e. if the value contains + // C# code, then we need to buffer it. + if (!isPlainTextValue) + { + BuildBufferedWritingScope(attributeValue, htmlEncodeValues: true); + } } // Execution contexts are a runtime feature, therefore no need to add anything to them. @@ -340,27 +353,39 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp continue; } - _writer - .WriteStartInstanceMethodInvocation( - ExecutionContextVariableName, - _tagHelperContext.ExecutionContextAddHtmlAttributeMethodName) - .WriteStringLiteral(htmlAttribute.Key) - .WriteParameterSeparator() - .WriteStartMethodInvocation(_tagHelperContext.MarkAsHtmlEncodedMethodName); - - // If it's a plain text value then we need to surround the value with quotes. - if (isPlainTextValue) + // If we have a minimized attribute there is no value + if (attributeValue == null) { - _writer.WriteStringLiteral(textValue); + _writer + .WriteStartInstanceMethodInvocation( + ExecutionContextVariableName, + _tagHelperContext.ExecutionContextAddMinimizedHtmlAttributeMethodName) + .WriteStringLiteral(htmlAttribute.Key) + .WriteEndMethodInvocation(); } else { - RenderBufferedAttributeValueAccessor(_writer); - } + _writer + .WriteStartInstanceMethodInvocation( + ExecutionContextVariableName, + _tagHelperContext.ExecutionContextAddHtmlAttributeMethodName) + .WriteStringLiteral(htmlAttribute.Key) + .WriteParameterSeparator() + .WriteStartMethodInvocation(_tagHelperContext.MarkAsHtmlEncodedMethodName); - _writer - .WriteEndMethodInvocation(endLine: false) - .WriteEndMethodInvocation(); + // If it's a plain text value then we need to surround the value with quotes. + if (isPlainTextValue) + { + _writer.WriteStringLiteral(textValue); + } + else + { + RenderBufferedAttributeValueAccessor(_writer); + } + + _writer.WriteEndMethodInvocation(endLine: false) + .WriteEndMethodInvocation(); + } } } diff --git a/src/Microsoft.AspNet.Razor/Generator/GeneratedTagHelperContext.cs b/src/Microsoft.AspNet.Razor/Generator/GeneratedTagHelperContext.cs index 4a566cd798..57eb270b56 100644 --- a/src/Microsoft.AspNet.Razor/Generator/GeneratedTagHelperContext.cs +++ b/src/Microsoft.AspNet.Razor/Generator/GeneratedTagHelperContext.cs @@ -19,6 +19,7 @@ namespace Microsoft.AspNet.Razor.Generator ScopeManagerEndMethodName = "End"; ExecutionContextAddMethodName = "Add"; ExecutionContextAddTagHelperAttributeMethodName = "AddTagHelperAttribute"; + ExecutionContextAddMinimizedHtmlAttributeMethodName = "AddMinimizedHtmlAttribute"; ExecutionContextAddHtmlAttributeMethodName = "AddHtmlAttribute"; ExecutionContextOutputPropertyName = "Output"; MarkAsHtmlEncodedMethodName = "Html.Raw"; @@ -57,6 +58,11 @@ namespace Microsoft.AspNet.Razor.Generator /// public string ExecutionContextAddTagHelperAttributeMethodName { get; set; } + /// + /// The name of the method used to add minimized HTML attributes. + /// + public string ExecutionContextAddMinimizedHtmlAttributeMethodName { get; set; } + /// /// The name of the method used to add HTML attributes. /// diff --git a/src/Microsoft.AspNet.Razor/Generator/TagHelperCodeGenerator.cs b/src/Microsoft.AspNet.Razor/Generator/TagHelperCodeGenerator.cs index bb924aa398..84818fc09a 100644 --- a/src/Microsoft.AspNet.Razor/Generator/TagHelperCodeGenerator.cs +++ b/src/Microsoft.AspNet.Razor/Generator/TagHelperCodeGenerator.cs @@ -57,19 +57,25 @@ namespace Microsoft.AspNet.Razor.Generator foreach (var attribute in tagHelperBlock.Attributes) { - // Populates the code tree with chunks associated with attributes - attribute.Value.Accept(codeGenerator); + ChunkBlock attributeChunkValue = null; - var chunks = codeGenerator.Context.CodeTreeBuilder.CodeTree.Chunks; - var first = chunks.FirstOrDefault(); + if (attribute.Value != null) + { + // Populates the code tree with chunks associated with attributes + attribute.Value.Accept(codeGenerator); - attributes.Add(new KeyValuePair(attribute.Key, - new ChunkBlock + var chunks = codeGenerator.Context.CodeTreeBuilder.CodeTree.Chunks; + var first = chunks.FirstOrDefault(); + + attributeChunkValue = new ChunkBlock { Association = first?.Association, Children = chunks, Start = first == null ? SourceLocation.Zero : first.Start - })); + }; + } + + attributes.Add(new KeyValuePair(attribute.Key, attributeChunkValue)); // Reset the code tree builder so we can build a new one for the next attribute codeGenerator.Context.CodeTreeBuilder = new CodeTreeBuilder(); diff --git a/src/Microsoft.AspNet.Razor/Parser/HtmlMarkupParser.Block.cs b/src/Microsoft.AspNet.Razor/Parser/HtmlMarkupParser.Block.cs index 351db46641..ef2a330946 100644 --- a/src/Microsoft.AspNet.Razor/Parser/HtmlMarkupParser.Block.cs +++ b/src/Microsoft.AspNet.Razor/Parser/HtmlMarkupParser.Block.cs @@ -473,12 +473,25 @@ namespace Microsoft.AspNet.Razor.Parser if (!At(HtmlSymbolType.Equals)) { - // Saw a space or newline after the name, so just skip this attribute and continue around the loop - Accept(whitespace); - Accept(name); + // Minimized attribute + + // Output anything prior to the attribute, in most cases this will be the tag name: + // |. If in-between other attributes this will noop or output malformed attribute + // content (if the previous attribute was malformed). + Output(SpanKind.Markup); + + using (Context.StartBlock(BlockType.Markup)) + { + Accept(whitespace); + Accept(name); + Output(SpanKind.Markup); + } + return; } + // Not a minimized attribute, parse as if it were well-formed (if attribute turns out to be malformed we + // will go into recovery). Output(SpanKind.Markup); // Start a new markup block for the attribute diff --git a/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlock.cs b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlock.cs index 48fb4df169..6b3f5f3362 100644 --- a/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlock.cs +++ b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlock.cs @@ -38,7 +38,10 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers foreach (var attributeChildren in Attributes) { - attributeChildren.Value.Parent = this; + if (attributeChildren.Value != null) + { + attributeChildren.Value.Parent = this; + } } } diff --git a/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockRewriter.cs b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockRewriter.cs index b564d78156..001d3d3deb 100644 --- a/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockRewriter.cs +++ b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockRewriter.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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; @@ -71,11 +71,12 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal // Only want to track the attribute if we succeeded in parsing its corresponding Block/Span. if (succeeded) { - // Check if it's a bound attribute that is not of type string and happens to be null or whitespace. + // Check if it's a bound attribute that is minimized or not of type string and null or whitespace. string attributeValueType; if (attributeValueTypes.TryGetValue(attribute.Key, out attributeValueType) && + (attribute.Value == null || !IsStringAttribute(attributeValueType) && - IsNullOrWhitespaceAttributeValue(attribute.Value)) + IsNullOrWhitespaceAttributeValue(attribute.Value))) { var errorLocation = GetAttributeNameStartLocation(child); @@ -167,7 +168,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal { Debug.Assert( name != null, - "Name should never be null here. The parser should guaruntee an attribute has a name."); + "Name should never be null here. The parser should guarantee an attribute has a name."); // We've captured all leading whitespace and the attribute name. // We're now at: " asp-for|='...'" or " asp-for|=..." @@ -239,7 +240,9 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal return false; } - attribute = CreateMarkupAttribute(name, builder, attributeValueTypes); + // If we're not after an equal then we should treat the value as if it were a minimized attribute. + var attributeValueBuilder = afterEquals ? builder : null; + attribute = CreateMarkupAttribute(name, attributeValueBuilder, attributeValueTypes); return true; } @@ -432,17 +435,24 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal IReadOnlyDictionary attributeValueTypes) { string attributeTypeName; + Span value = null; - // If the attribute was requested by the tag helper and doesn't happen to be a string then we need to treat - // its value as code. Any non-string value can be any C# value so we need to ensure the SyntaxTreeNode - // reflects that. - if (attributeValueTypes.TryGetValue(name, out attributeTypeName) && - !IsStringAttribute(attributeTypeName)) + // Builder will be null in the case of minimized attributes + if (builder != null) { - builder.Kind = SpanKind.Code; + // If the attribute was requested by the tag helper and doesn't happen to be a string then we need to treat + // its value as code. Any non-string value can be any C# value so we need to ensure the SyntaxTreeNode + // reflects that. + if (attributeValueTypes.TryGetValue(name, out attributeTypeName) && + !IsStringAttribute(attributeTypeName)) + { + builder.Kind = SpanKind.Code; + } + + value = builder.Build(); } - return new KeyValuePair(name, builder.Build()); + return new KeyValuePair(name, value); } private static bool IsNullOrWhitespaceAttributeValue(SyntaxTreeNode attributeValue)