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)