Add support for minimized attributes in TagHelpers.

- Updated the Razor parser to understand minimized attributes instead of just treating them like plain text. This just involved encompassing minimized attributes in their own blocks just like the other attributes found on the HTML tag.
- Updated TagHelperParseTreeRewriter to only accept minimized attributes for unbound attributes.
- Updated IReadOnlyTagHelperAttribute/TagHelperAttribute to have a Minimized property to indicate that an attribute was minimized.
- Updated parser level block structures to represent minimized attributes as null syntax tree nodes.
- Updated chunk level structures to represent minimized attributes as null chunks.

#220
This commit is contained in:
N. Taylor Mullen 2015-05-01 16:43:53 -07:00
parent 7d8f5d7b84
commit 6fa3e405af
9 changed files with 144 additions and 48 deletions

View File

@ -19,5 +19,11 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
/// Gets the value of the attribute.
/// </summary>
object Value { get; }
/// <summary>
/// Gets an indication whether the attribute is minimized or not.
/// </summary>
/// <remarks>If <c>true</c>, <see cref="Value"/> will be ignored.</remarks>
bool Minimized { get; }
}
}

View File

@ -41,6 +41,12 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
/// </summary>
public object Value { get; set; }
/// <summary>
/// Gets or sets an indication whether the attribute is minimized or not.
/// </summary>
/// <remarks>If <c>true</c>, <see cref="Value"/> will be ignored.</remarks>
public bool Minimized { get; set; }
/// <summary>
/// Converts the specified <paramref name="value"/> into a <see cref="TagHelperAttribute"/>.
/// </summary>
@ -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));
}
/// <inheritdoc />

View File

@ -39,7 +39,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
/// <param name="tagName">The HTML tag name in the Razor source.</param>
/// <param name="selfClosing">
/// <see cref="bool"/> indicating whether or not the tag in the Razor source was self-closing.</param>
/// <param name="items">The collection of items used to communicate with other
/// <param name="items">The collection of items used to communicate with other
/// <see cref="ITagHelper"/>s</param>
/// <param name="uniqueId">An identifier unique to the HTML element this context is for.</param>
/// <param name="executeChildContentAsync">A delegate used to execute the child content asynchronously.</param>
@ -133,6 +133,26 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
_tagHelpers.Add(tagHelper);
}
/// <summary>
/// Tracks the minimized HTML attribute in <see cref="AllAttributes"/> and <see cref="HTMLAttributes"/>.
/// </summary>
/// <param name="name">The minimized HTML attribute name.</param>
public void AddMinimizedHtmlAttribute([NotNull] string name)
{
HTMLAttributes.Add(
new TagHelperAttribute
{
Name = name,
Minimized = true
});
AllAttributes.Add(
new TagHelperAttribute
{
Name = name,
Minimized = true
});
}
/// <summary>
/// Tracks the HTML attribute in <see cref="AllAttributes"/> and <see cref="HTMLAttributes"/>.
/// </summary>
@ -168,7 +188,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
/// </summary>
/// <returns>A <see cref="Task"/> that on completion returns the rendered child content.</returns>
/// <remarks>
/// 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 <see cref="Task{TagHelperContent}"/> return a cached result.
/// </remarks>
public async Task<TagHelperContent> GetChildContentAsync()

View File

@ -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();
}
}
}

View File

@ -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
/// </summary>
public string ExecutionContextAddTagHelperAttributeMethodName { get; set; }
/// <summary>
/// The name of the <see cref="ExecutionContextTypeName"/> method used to add minimized HTML attributes.
/// </summary>
public string ExecutionContextAddMinimizedHtmlAttributeMethodName { get; set; }
/// <summary>
/// The name of the <see cref="ExecutionContextTypeName"/> method used to add HTML attributes.
/// </summary>

View File

@ -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<string, Chunk>(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<string, Chunk>(attribute.Key, attributeChunkValue));
// Reset the code tree builder so we can build a new one for the next attribute
codeGenerator.Context.CodeTreeBuilder = new CodeTreeBuilder();

View File

@ -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:
// |<input| checked />. 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

View File

@ -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;
}
}
}

View File

@ -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<string, string> 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<string, SyntaxTreeNode>(name, builder.Build());
return new KeyValuePair<string, SyntaxTreeNode>(name, value);
}
private static bool IsNullOrWhitespaceAttributeValue(SyntaxTreeNode attributeValue)