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.
|
/// Gets the value of the attribute.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
object Value { get; }
|
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>
|
/// </summary>
|
||||||
public object Value { get; set; }
|
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>
|
/// <summary>
|
||||||
/// Converts the specified <paramref name="value"/> into a <see cref="TagHelperAttribute"/>.
|
/// Converts the specified <paramref name="value"/> into a <see cref="TagHelperAttribute"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -61,7 +67,8 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
|
||||||
return
|
return
|
||||||
other != null &&
|
other != null &&
|
||||||
string.Equals(Name, other.Name, StringComparison.OrdinalIgnoreCase) &&
|
string.Equals(Name, other.Name, StringComparison.OrdinalIgnoreCase) &&
|
||||||
Equals(Value, other.Value);
|
Minimized == other.Minimized &&
|
||||||
|
(Minimized || Equals(Value, other.Value));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,26 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
|
||||||
_tagHelpers.Add(tagHelper);
|
_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>
|
/// <summary>
|
||||||
/// Tracks the HTML attribute in <see cref="AllAttributes"/> and <see cref="HTMLAttributes"/>.
|
/// Tracks the HTML attribute in <see cref="AllAttributes"/> and <see cref="HTMLAttributes"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -203,6 +203,13 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp
|
||||||
var firstAttribute = matchingAttributes.First();
|
var firstAttribute = matchingAttributes.First();
|
||||||
var attributeValueChunk = firstAttribute.Value;
|
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);
|
var attributeValueRecorded = htmlAttributeValues.ContainsKey(attributeDescriptor.Name);
|
||||||
|
|
||||||
// Bufferable attributes are attributes that can have Razor code inside of them.
|
// 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
|
// Build out the unbound HTML attributes for the tag builder
|
||||||
foreach (var htmlAttribute in unboundHTMLAttributes)
|
foreach (var htmlAttribute in unboundHTMLAttributes)
|
||||||
{
|
{
|
||||||
string textValue;
|
string textValue = null;
|
||||||
|
var isPlainTextValue = false;
|
||||||
var attributeValue = htmlAttribute.Value;
|
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
|
// A null attribute value means the HTML attribute is minimized.
|
||||||
// C# code, then we need to buffer it.
|
if (attributeValue != null)
|
||||||
if (!isPlainTextValue)
|
|
||||||
{
|
{
|
||||||
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.
|
// 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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
_writer
|
// If we have a minimized attribute there is no value
|
||||||
.WriteStartInstanceMethodInvocation(
|
if (attributeValue == null)
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
_writer.WriteStringLiteral(textValue);
|
_writer
|
||||||
|
.WriteStartInstanceMethodInvocation(
|
||||||
|
ExecutionContextVariableName,
|
||||||
|
_tagHelperContext.ExecutionContextAddMinimizedHtmlAttributeMethodName)
|
||||||
|
.WriteStringLiteral(htmlAttribute.Key)
|
||||||
|
.WriteEndMethodInvocation();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
RenderBufferedAttributeValueAccessor(_writer);
|
_writer
|
||||||
}
|
.WriteStartInstanceMethodInvocation(
|
||||||
|
ExecutionContextVariableName,
|
||||||
|
_tagHelperContext.ExecutionContextAddHtmlAttributeMethodName)
|
||||||
|
.WriteStringLiteral(htmlAttribute.Key)
|
||||||
|
.WriteParameterSeparator()
|
||||||
|
.WriteStartMethodInvocation(_tagHelperContext.MarkAsHtmlEncodedMethodName);
|
||||||
|
|
||||||
_writer
|
// If it's a plain text value then we need to surround the value with quotes.
|
||||||
.WriteEndMethodInvocation(endLine: false)
|
if (isPlainTextValue)
|
||||||
.WriteEndMethodInvocation();
|
{
|
||||||
|
_writer.WriteStringLiteral(textValue);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
RenderBufferedAttributeValueAccessor(_writer);
|
||||||
|
}
|
||||||
|
|
||||||
|
_writer.WriteEndMethodInvocation(endLine: false)
|
||||||
|
.WriteEndMethodInvocation();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ namespace Microsoft.AspNet.Razor.Generator
|
||||||
ScopeManagerEndMethodName = "End";
|
ScopeManagerEndMethodName = "End";
|
||||||
ExecutionContextAddMethodName = "Add";
|
ExecutionContextAddMethodName = "Add";
|
||||||
ExecutionContextAddTagHelperAttributeMethodName = "AddTagHelperAttribute";
|
ExecutionContextAddTagHelperAttributeMethodName = "AddTagHelperAttribute";
|
||||||
|
ExecutionContextAddMinimizedHtmlAttributeMethodName = "AddMinimizedHtmlAttribute";
|
||||||
ExecutionContextAddHtmlAttributeMethodName = "AddHtmlAttribute";
|
ExecutionContextAddHtmlAttributeMethodName = "AddHtmlAttribute";
|
||||||
ExecutionContextOutputPropertyName = "Output";
|
ExecutionContextOutputPropertyName = "Output";
|
||||||
MarkAsHtmlEncodedMethodName = "Html.Raw";
|
MarkAsHtmlEncodedMethodName = "Html.Raw";
|
||||||
|
|
@ -57,6 +58,11 @@ namespace Microsoft.AspNet.Razor.Generator
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string ExecutionContextAddTagHelperAttributeMethodName { get; set; }
|
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>
|
/// <summary>
|
||||||
/// The name of the <see cref="ExecutionContextTypeName"/> method used to add HTML attributes.
|
/// The name of the <see cref="ExecutionContextTypeName"/> method used to add HTML attributes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -57,19 +57,25 @@ namespace Microsoft.AspNet.Razor.Generator
|
||||||
|
|
||||||
foreach (var attribute in tagHelperBlock.Attributes)
|
foreach (var attribute in tagHelperBlock.Attributes)
|
||||||
{
|
{
|
||||||
// Populates the code tree with chunks associated with attributes
|
ChunkBlock attributeChunkValue = null;
|
||||||
attribute.Value.Accept(codeGenerator);
|
|
||||||
|
|
||||||
var chunks = codeGenerator.Context.CodeTreeBuilder.CodeTree.Chunks;
|
if (attribute.Value != null)
|
||||||
var first = chunks.FirstOrDefault();
|
{
|
||||||
|
// Populates the code tree with chunks associated with attributes
|
||||||
|
attribute.Value.Accept(codeGenerator);
|
||||||
|
|
||||||
attributes.Add(new KeyValuePair<string, Chunk>(attribute.Key,
|
var chunks = codeGenerator.Context.CodeTreeBuilder.CodeTree.Chunks;
|
||||||
new ChunkBlock
|
var first = chunks.FirstOrDefault();
|
||||||
|
|
||||||
|
attributeChunkValue = new ChunkBlock
|
||||||
{
|
{
|
||||||
Association = first?.Association,
|
Association = first?.Association,
|
||||||
Children = chunks,
|
Children = chunks,
|
||||||
Start = first == null ? SourceLocation.Zero : first.Start
|
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
|
// Reset the code tree builder so we can build a new one for the next attribute
|
||||||
codeGenerator.Context.CodeTreeBuilder = new CodeTreeBuilder();
|
codeGenerator.Context.CodeTreeBuilder = new CodeTreeBuilder();
|
||||||
|
|
|
||||||
|
|
@ -473,12 +473,25 @@ namespace Microsoft.AspNet.Razor.Parser
|
||||||
|
|
||||||
if (!At(HtmlSymbolType.Equals))
|
if (!At(HtmlSymbolType.Equals))
|
||||||
{
|
{
|
||||||
// Saw a space or newline after the name, so just skip this attribute and continue around the loop
|
// Minimized attribute
|
||||||
Accept(whitespace);
|
|
||||||
Accept(name);
|
// 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;
|
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);
|
Output(SpanKind.Markup);
|
||||||
|
|
||||||
// Start a new markup block for the attribute
|
// Start a new markup block for the attribute
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,10 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers
|
||||||
|
|
||||||
foreach (var attributeChildren in Attributes)
|
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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
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.
|
// Only want to track the attribute if we succeeded in parsing its corresponding Block/Span.
|
||||||
if (succeeded)
|
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;
|
string attributeValueType;
|
||||||
if (attributeValueTypes.TryGetValue(attribute.Key, out attributeValueType) &&
|
if (attributeValueTypes.TryGetValue(attribute.Key, out attributeValueType) &&
|
||||||
|
(attribute.Value == null ||
|
||||||
!IsStringAttribute(attributeValueType) &&
|
!IsStringAttribute(attributeValueType) &&
|
||||||
IsNullOrWhitespaceAttributeValue(attribute.Value))
|
IsNullOrWhitespaceAttributeValue(attribute.Value)))
|
||||||
{
|
{
|
||||||
var errorLocation = GetAttributeNameStartLocation(child);
|
var errorLocation = GetAttributeNameStartLocation(child);
|
||||||
|
|
||||||
|
|
@ -167,7 +168,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
|
||||||
{
|
{
|
||||||
Debug.Assert(
|
Debug.Assert(
|
||||||
name != null,
|
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've captured all leading whitespace and the attribute name.
|
||||||
// We're now at: " asp-for|='...'" or " asp-for|=..."
|
// We're now at: " asp-for|='...'" or " asp-for|=..."
|
||||||
|
|
@ -239,7 +240,9 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
|
||||||
return false;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -432,17 +435,24 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
|
||||||
IReadOnlyDictionary<string, string> attributeValueTypes)
|
IReadOnlyDictionary<string, string> attributeValueTypes)
|
||||||
{
|
{
|
||||||
string attributeTypeName;
|
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
|
// Builder will be null in the case of minimized attributes
|
||||||
// its value as code. Any non-string value can be any C# value so we need to ensure the SyntaxTreeNode
|
if (builder != null)
|
||||||
// reflects that.
|
|
||||||
if (attributeValueTypes.TryGetValue(name, out attributeTypeName) &&
|
|
||||||
!IsStringAttribute(attributeTypeName))
|
|
||||||
{
|
{
|
||||||
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)
|
private static bool IsNullOrWhitespaceAttributeValue(SyntaxTreeNode attributeValue)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue