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:
parent
7d8f5d7b84
commit
6fa3e405af
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue