Add `[HtmlAttributeName(..., DictionaryAttributePrefix="prefix")]` part II
- relates to #89 because that changes `string` property checks and needs this refactor - determine `string`-ness when creating `TagHelperAttributeDescriptor`s - add `TagHelperAttributeDescriptor.IsStringProperty` (set in constructor) - avoid repeated `string` comparisons and be more explicit - change `TagHelperBlockRewriter` to centralize more of the `string`-ness determination - also add `TryParseResult` DTO, avoiding multiple `out` parameters - refactor `CSharpTagHelperCodeRenderer` to allow reuse of core attribute value rendering - test all of it - add `TagHelperDescriptorTest` to confirm serialization / deserialization minor: - fix `TagHelperBlockRewriter.TryParseBlock()` end quote removal when tag is malformed nits: - remove dangling mention of fixed bug #220 - make recently-added `TagHelperBlockRewriterTest` tests realistic - multiple `TagHelperDescriptor`s for same tag helper have identical `Attributes`
This commit is contained in:
parent
2fe78d70db
commit
94f2f904b3
|
|
@ -67,14 +67,14 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp
|
|||
|
||||
RenderTagHelpersCreation(chunk, tagHelperDescriptors);
|
||||
|
||||
var attributeDescriptors = tagHelperDescriptors.SelectMany(descriptor => descriptor.Attributes);
|
||||
var boundHTMLAttributes = attributeDescriptors.Select(descriptor => descriptor.Name);
|
||||
// Determine what attributes exist in the element and divide them up.
|
||||
var htmlAttributes = chunk.Attributes;
|
||||
var unboundHTMLAttributes =
|
||||
htmlAttributes.Where(htmlAttribute => !boundHTMLAttributes.Contains(htmlAttribute.Key,
|
||||
StringComparer.OrdinalIgnoreCase));
|
||||
var attributeDescriptors = tagHelperDescriptors.SelectMany(descriptor => descriptor.Attributes);
|
||||
var unboundHtmlAttributes = htmlAttributes.Where(
|
||||
attribute => !attributeDescriptors.Any(
|
||||
descriptor => string.Equals(attribute.Key, descriptor.Name, StringComparison.OrdinalIgnoreCase)));
|
||||
|
||||
RenderUnboundHTMLAttributes(unboundHTMLAttributes);
|
||||
RenderUnboundHTMLAttributes(unboundHtmlAttributes);
|
||||
|
||||
// No need to run anything in design time mode.
|
||||
if (!_designTimeMode)
|
||||
|
|
@ -180,22 +180,24 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp
|
|||
}
|
||||
|
||||
// Render all of the bound attribute values for the tag helper.
|
||||
RenderBoundHTMLAttributes(chunk.Attributes,
|
||||
tagHelperVariableName,
|
||||
tagHelperDescriptor.Attributes,
|
||||
htmlAttributeValues);
|
||||
RenderBoundHTMLAttributes(
|
||||
chunk.Attributes,
|
||||
tagHelperVariableName,
|
||||
tagHelperDescriptor.Attributes,
|
||||
htmlAttributeValues);
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderBoundHTMLAttributes(IList<KeyValuePair<string, Chunk>> chunkAttributes,
|
||||
string tagHelperVariableName,
|
||||
IEnumerable<TagHelperAttributeDescriptor> attributeDescriptors,
|
||||
Dictionary<string, string> htmlAttributeValues)
|
||||
private void RenderBoundHTMLAttributes(
|
||||
IList<KeyValuePair<string, Chunk>> chunkAttributes,
|
||||
string tagHelperVariableName,
|
||||
IEnumerable<TagHelperAttributeDescriptor> attributeDescriptors,
|
||||
Dictionary<string, string> htmlAttributeValues)
|
||||
{
|
||||
foreach (var attributeDescriptor in attributeDescriptors)
|
||||
{
|
||||
var matchingAttributes = chunkAttributes.Where(
|
||||
attr => string.Equals(attr.Key, attributeDescriptor.Name, StringComparison.OrdinalIgnoreCase));
|
||||
kvp => string.Equals(kvp.Key, attributeDescriptor.Name, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (matchingAttributes.Any())
|
||||
{
|
||||
|
|
@ -210,31 +212,15 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp
|
|||
continue;
|
||||
}
|
||||
|
||||
var attributeValueRecorded = htmlAttributeValues.ContainsKey(attributeDescriptor.Name);
|
||||
|
||||
// Bufferable attributes are attributes that can have Razor code inside of them.
|
||||
var bufferableAttribute = IsStringAttribute(attributeDescriptor);
|
||||
|
||||
// Plain text values are non Razor code (@DateTime.Now) values. If an attribute is bufferable it
|
||||
// may be more than just a plain text value, it may also contain Razor code which is why we attempt
|
||||
// to retrieve a plain text value here.
|
||||
string textValue;
|
||||
var isPlainTextValue = TryGetPlainTextValue(attributeValueChunk, out textValue);
|
||||
|
||||
// If we haven't recorded a value and we need to buffer an attribute value and the value is not
|
||||
// plain text then we need to prepare the value prior to setting it below.
|
||||
if (!attributeValueRecorded && bufferableAttribute && !isPlainTextValue)
|
||||
{
|
||||
BuildBufferedWritingScope(attributeValueChunk, htmlEncodeValues: false);
|
||||
}
|
||||
|
||||
// We capture the tag helpers property value accessor so we can retrieve it later (if we need to).
|
||||
var valueAccessor = string.Format(CultureInfo.InvariantCulture,
|
||||
"{0}.{1}",
|
||||
tagHelperVariableName,
|
||||
attributeDescriptor.PropertyName);
|
||||
var valueAccessor = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"{0}.{1}",
|
||||
tagHelperVariableName,
|
||||
attributeDescriptor.PropertyName);
|
||||
|
||||
// If we haven't recorded this attribute value before then we need to record its value.
|
||||
var attributeValueRecorded = htmlAttributeValues.ContainsKey(attributeDescriptor.Name);
|
||||
if (!attributeValueRecorded)
|
||||
{
|
||||
// We only need to create attribute values once per HTML element (not once per tag helper).
|
||||
|
|
@ -242,58 +228,16 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp
|
|||
// helpers that need the value.
|
||||
htmlAttributeValues.Add(attributeDescriptor.Name, valueAccessor);
|
||||
|
||||
if (bufferableAttribute)
|
||||
{
|
||||
_writer.WriteStartAssignment(valueAccessor);
|
||||
// Bufferable attributes are attributes that can have Razor code inside of them. Such
|
||||
// attributes have string values and may be calculated using a temporary TextWriter or other
|
||||
// buffer.
|
||||
var bufferableAttribute = attributeDescriptor.IsStringProperty;
|
||||
|
||||
if (isPlainTextValue)
|
||||
{
|
||||
// If the attribute is bufferable but has a plain text value that means the value
|
||||
// is a string which needs to be surrounded in quotes.
|
||||
RenderQuotedAttributeValue(textValue, attributeDescriptor);
|
||||
}
|
||||
else
|
||||
{
|
||||
// The value contains more than plain text e.g.
|
||||
// stringAttribute ="Time: @DateTime.Now"
|
||||
RenderBufferedAttributeValue(attributeDescriptor);
|
||||
}
|
||||
|
||||
_writer.WriteLine(";");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Write out simple assignment for non-string property value. Try to keep the whole
|
||||
// statement together and the #line pragma correct to make debugging possible.
|
||||
using (var lineMapper = new CSharpLineMappingWriter(
|
||||
_writer,
|
||||
attributeValueChunk.Association.Start,
|
||||
_context.SourceFile))
|
||||
{
|
||||
// Place the assignment LHS to align RHS with original attribute value's indentation.
|
||||
// Unfortunately originalIndent is incorrect if original line contains tabs. Unable to
|
||||
// use a CSharpPaddingBuilder because the Association has no Previous node; lost the
|
||||
// original Span sequence when the parse tree was rewritten.
|
||||
var originalIndent = attributeValueChunk.Start.CharacterIndex;
|
||||
var generatedLength = valueAccessor.Length + " = ".Length;
|
||||
var newIndent = originalIndent - generatedLength;
|
||||
if (newIndent > 0)
|
||||
{
|
||||
_writer.Indent(newIndent);
|
||||
}
|
||||
|
||||
_writer.WriteStartAssignment(valueAccessor);
|
||||
lineMapper.MarkLineMappingStart();
|
||||
|
||||
// Write out bare expression for this attribute value. Property is not a string.
|
||||
// So quoting or buffering are not helpful.
|
||||
RenderRawAttributeValue(attributeValueChunk, attributeDescriptor, isPlainTextValue);
|
||||
|
||||
// End the assignment to the attribute.
|
||||
lineMapper.MarkLineMappingEnd();
|
||||
_writer.WriteLine(";");
|
||||
}
|
||||
}
|
||||
RenderNewAttributeValueAssignment(
|
||||
attributeDescriptor,
|
||||
bufferableAttribute,
|
||||
attributeValueChunk,
|
||||
valueAccessor);
|
||||
|
||||
// Execution contexts are a runtime feature.
|
||||
if (_designTimeMode)
|
||||
|
|
@ -301,8 +245,8 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp
|
|||
continue;
|
||||
}
|
||||
|
||||
var attributeName = firstAttribute.Key;
|
||||
// We need to inform the context of the attribute value.
|
||||
var attributeName = firstAttribute.Key;
|
||||
_writer
|
||||
.WriteStartInstanceMethodInvocation(
|
||||
ExecutionContextVariableName,
|
||||
|
|
@ -325,6 +269,79 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp
|
|||
}
|
||||
}
|
||||
|
||||
// Render assignment of attribute value to the value accessor.
|
||||
private void RenderNewAttributeValueAssignment(
|
||||
TagHelperAttributeDescriptor attributeDescriptor,
|
||||
bool bufferableAttribute,
|
||||
Chunk attributeValueChunk,
|
||||
string valueAccessor)
|
||||
{
|
||||
// Plain text values are non Razor code (@DateTime.Now) values. If an attribute is bufferable it
|
||||
// may be more than just a plain text value, it may also contain Razor code which is why we attempt
|
||||
// to retrieve a plain text value here.
|
||||
string textValue;
|
||||
var isPlainTextValue = TryGetPlainTextValue(attributeValueChunk, out textValue);
|
||||
|
||||
if (bufferableAttribute)
|
||||
{
|
||||
if (!isPlainTextValue)
|
||||
{
|
||||
// If we haven't recorded a value and we need to buffer an attribute value and the value is not
|
||||
// plain text then we need to prepare the value prior to setting it below.
|
||||
BuildBufferedWritingScope(attributeValueChunk, htmlEncodeValues: false);
|
||||
}
|
||||
|
||||
_writer.WriteStartAssignment(valueAccessor);
|
||||
|
||||
if (isPlainTextValue)
|
||||
{
|
||||
// If the attribute is bufferable but has a plain text value that means the value
|
||||
// is a string which needs to be surrounded in quotes.
|
||||
RenderQuotedAttributeValue(textValue, attributeDescriptor);
|
||||
}
|
||||
else
|
||||
{
|
||||
// The value contains more than plain text e.g. stringAttribute ="Time: @DateTime.Now".
|
||||
RenderBufferedAttributeValue(attributeDescriptor);
|
||||
}
|
||||
|
||||
_writer.WriteLine(";");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Write out simple assignment for non-string property value. Try to keep the whole
|
||||
// statement together and the #line pragma correct to make debugging possible.
|
||||
using (var lineMapper = new CSharpLineMappingWriter(
|
||||
_writer,
|
||||
attributeValueChunk.Association.Start,
|
||||
_context.SourceFile))
|
||||
{
|
||||
// Place the assignment LHS to align RHS with original attribute value's indentation.
|
||||
// Unfortunately originalIndent is incorrect if original line contains tabs. Unable to
|
||||
// use a CSharpPaddingBuilder because the Association has no Previous node; lost the
|
||||
// original Span sequence when the parse tree was rewritten.
|
||||
var originalIndent = attributeValueChunk.Start.CharacterIndex;
|
||||
var generatedLength = valueAccessor.Length + " = ".Length;
|
||||
var newIndent = originalIndent - generatedLength;
|
||||
if (newIndent > 0)
|
||||
{
|
||||
_writer.Indent(newIndent);
|
||||
}
|
||||
|
||||
_writer.WriteStartAssignment(valueAccessor);
|
||||
lineMapper.MarkLineMappingStart();
|
||||
|
||||
// Write out bare expression for this attribute value. Property is not a string.
|
||||
// So quoting or buffering are not helpful.
|
||||
RenderRawAttributeValue(attributeValueChunk, attributeDescriptor, isPlainTextValue);
|
||||
|
||||
// End the assignment to the attribute.
|
||||
lineMapper.MarkLineMappingEnd();
|
||||
_writer.WriteLine(";");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderUnboundHTMLAttributes(IEnumerable<KeyValuePair<string, Chunk>> unboundHTMLAttributes)
|
||||
{
|
||||
// Build out the unbound HTML attributes for the tag builder
|
||||
|
|
@ -540,14 +557,6 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp
|
|||
}
|
||||
}
|
||||
|
||||
private static bool IsStringAttribute(TagHelperAttributeDescriptor attributeDescriptor)
|
||||
{
|
||||
return string.Equals(
|
||||
attributeDescriptor.TypeName,
|
||||
typeof(string).FullName,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static bool TryGetPlainTextValue(Chunk chunk, out string plainText)
|
||||
{
|
||||
var chunkBlock = chunk as ChunkBlock;
|
||||
|
|
|
|||
|
|
@ -38,15 +38,11 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
|
|||
IEnumerable<TagHelperDescriptor> descriptors,
|
||||
ErrorSink errorSink)
|
||||
{
|
||||
var attributes = new List<KeyValuePair<string, SyntaxTreeNode>>();
|
||||
// Ignore all but one descriptor per type since this method uses the TagHelperDescriptors only to get the
|
||||
// contained TagHelperAttributeDescriptor's.
|
||||
descriptors = descriptors.Distinct(TypeBasedTagHelperDescriptorComparer.Default);
|
||||
|
||||
// Build a dictionary so we can easily lookup expected attribute value lookups
|
||||
IReadOnlyDictionary<string, string> attributeValueTypes =
|
||||
descriptors.SelectMany(descriptor => descriptor.Attributes)
|
||||
.Distinct(TagHelperAttributeDescriptorComparer.Default)
|
||||
.ToDictionary(descriptor => descriptor.Name,
|
||||
descriptor => descriptor.TypeName,
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
var attributes = new List<KeyValuePair<string, SyntaxTreeNode>>();
|
||||
|
||||
// We skip the first child "<tagname" and take everything up to the ending portion of the tag ">" or "/>".
|
||||
// The -2 accounts for both the start and end tags. If the tag does not have a valid structure then there's
|
||||
|
|
@ -56,40 +52,38 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
|
|||
|
||||
foreach (var child in attributeChildren)
|
||||
{
|
||||
KeyValuePair<string, SyntaxTreeNode> attribute;
|
||||
bool succeeded = true;
|
||||
|
||||
TryParseResult result;
|
||||
if (child.IsBlock)
|
||||
{
|
||||
succeeded = TryParseBlock(tagName, (Block)child, attributeValueTypes, errorSink, out attribute);
|
||||
result = TryParseBlock(tagName, (Block)child, descriptors, errorSink);
|
||||
}
|
||||
else
|
||||
{
|
||||
succeeded = TryParseSpan((Span)child, attributeValueTypes, errorSink, out attribute);
|
||||
result = TryParseSpan((Span)child, descriptors, errorSink);
|
||||
}
|
||||
|
||||
// Only want to track the attribute if we succeeded in parsing its corresponding Block/Span.
|
||||
if (succeeded)
|
||||
if (result != null)
|
||||
{
|
||||
// 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)))
|
||||
// Check if it's a bound attribute that is minimized or if it's a bound non-string attribute that
|
||||
// is null or whitespace.
|
||||
if ((result.IsBoundAttribute && result.AttributeValueNode == null) ||
|
||||
(result.IsBoundNonStringAttribute &&
|
||||
IsNullOrWhitespaceAttributeValue(result.AttributeValueNode)))
|
||||
{
|
||||
var errorLocation = GetAttributeNameStartLocation(child);
|
||||
|
||||
errorSink.OnError(
|
||||
errorLocation,
|
||||
RazorResources.FormatRewriterError_EmptyTagHelperBoundAttribute(
|
||||
attribute.Key,
|
||||
result.AttributeName,
|
||||
tagName,
|
||||
attributeValueType),
|
||||
attribute.Key.Length);
|
||||
GetPropertyType(result.AttributeName, descriptors)),
|
||||
result.AttributeName.Length);
|
||||
}
|
||||
|
||||
attributes.Add(new KeyValuePair<string, SyntaxTreeNode>(attribute.Key, attribute.Value));
|
||||
attributes.Add(
|
||||
new KeyValuePair<string, SyntaxTreeNode>(result.AttributeName, result.AttributeValueNode));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -106,11 +100,10 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
|
|||
// This method handles cases when the attribute is a simple span attribute such as
|
||||
// class="something moresomething". This does not handle complex attributes such as
|
||||
// class="@myclass". Therefore the span.Content is equivalent to the entire attribute.
|
||||
private static bool TryParseSpan(
|
||||
private static TryParseResult TryParseSpan(
|
||||
Span span,
|
||||
IReadOnlyDictionary<string, string> attributeValueTypes,
|
||||
ErrorSink errorSink,
|
||||
out KeyValuePair<string, SyntaxTreeNode> attribute)
|
||||
IEnumerable<TagHelperDescriptor> descriptors,
|
||||
ErrorSink errorSink)
|
||||
{
|
||||
var afterEquals = false;
|
||||
var builder = new SpanBuilder
|
||||
|
|
@ -235,24 +228,29 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
|
|||
span.Content.Length);
|
||||
}
|
||||
|
||||
attribute = default(KeyValuePair<string, SyntaxTreeNode>);
|
||||
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
bool isBoundNonStringAttribute;
|
||||
var result = new TryParseResult
|
||||
{
|
||||
IsBoundAttribute = IsBoundAttribute(name, descriptors, out isBoundNonStringAttribute),
|
||||
AttributeName = name,
|
||||
};
|
||||
result.IsBoundNonStringAttribute = isBoundNonStringAttribute;
|
||||
|
||||
// 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);
|
||||
result.AttributeValueNode = CreateMarkupAttribute(name, attributeValueBuilder, isBoundNonStringAttribute);
|
||||
|
||||
return true;
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool TryParseBlock(
|
||||
private static TryParseResult TryParseBlock(
|
||||
string tagName,
|
||||
Block block,
|
||||
IReadOnlyDictionary<string, string> attributeValueTypes,
|
||||
ErrorSink errorSink,
|
||||
out KeyValuePair<string, SyntaxTreeNode> attribute)
|
||||
IEnumerable<TagHelperDescriptor> descriptors,
|
||||
ErrorSink errorSink)
|
||||
{
|
||||
// TODO: Accept more than just spans: https://github.com/aspnet/Razor/issues/96.
|
||||
// The first child will only ever NOT be a Span if a user is doing something like:
|
||||
|
|
@ -265,9 +263,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
|
|||
errorSink.OnError(block.Children.First().Start,
|
||||
RazorResources.FormatTagHelpers_CannotHaveCSharpInTagDeclaration(tagName));
|
||||
|
||||
attribute = default(KeyValuePair<string, SyntaxTreeNode>);
|
||||
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
var builder = new BlockBuilder(block);
|
||||
|
|
@ -276,7 +272,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
|
|||
// i.e. <div class="plain text in attribute">
|
||||
if (builder.Children.Count == 1)
|
||||
{
|
||||
return TryParseSpan(childSpan, attributeValueTypes, errorSink, out attribute);
|
||||
return TryParseSpan(childSpan, descriptors, errorSink);
|
||||
}
|
||||
|
||||
var textSymbol = childSpan.Symbols.FirstHtmlSymbolAs(HtmlSymbolType.Text);
|
||||
|
|
@ -286,12 +282,17 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
|
|||
{
|
||||
errorSink.OnError(childSpan.Start, RazorResources.FormatTagHelpers_AttributesMustHaveAName(tagName));
|
||||
|
||||
attribute = default(KeyValuePair<string, SyntaxTreeNode>);
|
||||
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO: Support no attribute values: https://github.com/aspnet/Razor/issues/220
|
||||
// Have a name now. Able to determine correct isBoundNonStringAttribute value.
|
||||
bool isBoundNonStringAttribute;
|
||||
var result = new TryParseResult
|
||||
{
|
||||
IsBoundAttribute = IsBoundAttribute(name, descriptors, out isBoundNonStringAttribute),
|
||||
AttributeName = name,
|
||||
};
|
||||
result.IsBoundNonStringAttribute = isBoundNonStringAttribute;
|
||||
|
||||
// Remove first child i.e. foo="
|
||||
builder.Children.RemoveAt(0);
|
||||
|
|
@ -301,10 +302,14 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
|
|||
if (!endNode.IsBlock)
|
||||
{
|
||||
var endSpan = (Span)endNode;
|
||||
var endSymbol = (HtmlSymbol)endSpan.Symbols.Last();
|
||||
|
||||
// In some malformed cases e.g. <p bar="false', the last Span (false' in the ex.) may contain more
|
||||
// than a single HTML symbol. Do not ignore those other symbols.
|
||||
var symbolCount = endSpan.Symbols.Count();
|
||||
var endSymbol = symbolCount == 1 ? (HtmlSymbol)endSpan.Symbols.First() : null;
|
||||
|
||||
// Checking to see if it's a quoted attribute, if so we should remove end quote
|
||||
if (IsQuote(endSymbol))
|
||||
if (endSymbol != null && IsQuote(endSymbol))
|
||||
{
|
||||
builder.Children.RemoveAt(builder.Children.Count - 1);
|
||||
}
|
||||
|
|
@ -323,18 +328,17 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
|
|||
if (child != null)
|
||||
{
|
||||
// After pulling apart the block we just have a value span.
|
||||
|
||||
var spanBuilder = new SpanBuilder(child);
|
||||
|
||||
attribute = CreateMarkupAttribute(name, spanBuilder, attributeValueTypes);
|
||||
result.AttributeValueNode = CreateMarkupAttribute(name, spanBuilder, isBoundNonStringAttribute);
|
||||
|
||||
return true;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
attribute = new KeyValuePair<string, SyntaxTreeNode>(name, block);
|
||||
result.AttributeValueNode = block;
|
||||
|
||||
return true;
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Block RebuildCodeGenerators(Block block)
|
||||
|
|
@ -429,22 +433,20 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
|
|||
return nodeStart + firstNonWhitespaceSymbol.Start;
|
||||
}
|
||||
|
||||
private static KeyValuePair<string, SyntaxTreeNode> CreateMarkupAttribute(
|
||||
private static SyntaxTreeNode CreateMarkupAttribute(
|
||||
string name,
|
||||
SpanBuilder builder,
|
||||
IReadOnlyDictionary<string, string> attributeValueTypes)
|
||||
bool isBoundNonStringAttribute)
|
||||
{
|
||||
string attributeTypeName;
|
||||
Span value = null;
|
||||
|
||||
// Builder will be null in the case of minimized attributes
|
||||
if (builder != 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))
|
||||
// If the attribute was requested by a tag helper but the corresponding property was not a string,
|
||||
// then treat its value as code. A non-string value can be any C# value so we need to ensure the
|
||||
// SyntaxTreeNode reflects that.
|
||||
if (isBoundNonStringAttribute)
|
||||
{
|
||||
builder.Kind = SpanKind.Code;
|
||||
}
|
||||
|
|
@ -452,7 +454,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
|
|||
value = builder.Build();
|
||||
}
|
||||
|
||||
return new KeyValuePair<string, SyntaxTreeNode>(name, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
private static bool IsNullOrWhitespaceAttributeValue(SyntaxTreeNode attributeValue)
|
||||
|
|
@ -475,9 +477,35 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
|
|||
}
|
||||
}
|
||||
|
||||
private static bool IsStringAttribute(string attributeTypeName)
|
||||
// Determines the full name of the Type of the property corresponding to an attribute with the given name.
|
||||
private static string GetPropertyType(string name, IEnumerable<TagHelperDescriptor> descriptors)
|
||||
{
|
||||
return string.Equals(attributeTypeName, StringTypeName, StringComparison.OrdinalIgnoreCase);
|
||||
var firstBoundAttribute = FindFirstBoundAttribute(name, descriptors);
|
||||
|
||||
return firstBoundAttribute?.TypeName;
|
||||
}
|
||||
|
||||
// Determines whether an attribute with the given name is bound to a non-string tag helper property.
|
||||
private static bool IsBoundAttribute(
|
||||
string name,
|
||||
IEnumerable<TagHelperDescriptor> descriptors,
|
||||
out bool isBoundNonStringAttribute)
|
||||
{
|
||||
var firstBoundAttribute = FindFirstBoundAttribute(name, descriptors);
|
||||
var isBoundAttribute = firstBoundAttribute != null;
|
||||
isBoundNonStringAttribute = isBoundAttribute && !firstBoundAttribute.IsStringProperty;
|
||||
|
||||
return isBoundAttribute;
|
||||
}
|
||||
|
||||
// Finds first TagHelperAttributeDescriptor matching given name.
|
||||
private static TagHelperAttributeDescriptor FindFirstBoundAttribute(
|
||||
string name,
|
||||
IEnumerable<TagHelperDescriptor> descriptors)
|
||||
{
|
||||
return descriptors
|
||||
.SelectMany(descriptor => descriptor.Attributes)
|
||||
.FirstOrDefault(attribute => string.Equals(attribute.Name, name, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private static bool IsQuote(HtmlSymbol htmlSymbol)
|
||||
|
|
@ -485,5 +513,16 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
|
|||
return htmlSymbol.Type == HtmlSymbolType.DoubleQuote ||
|
||||
htmlSymbol.Type == HtmlSymbolType.SingleQuote;
|
||||
}
|
||||
|
||||
private class TryParseResult
|
||||
{
|
||||
public string AttributeName { get; set; }
|
||||
|
||||
public SyntaxTreeNode AttributeValueNode { get; set; }
|
||||
|
||||
public bool IsBoundAttribute { get; set; }
|
||||
|
||||
public bool IsBoundNonStringAttribute { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
// 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;
|
||||
using System.Reflection;
|
||||
using Microsoft.Framework.Internal;
|
||||
|
||||
namespace Microsoft.AspNet.Razor.TagHelpers
|
||||
{
|
||||
|
|
@ -10,9 +12,13 @@ namespace Microsoft.AspNet.Razor.TagHelpers
|
|||
/// </summary>
|
||||
public class TagHelperAttributeDescriptor
|
||||
{
|
||||
// Internal for testing
|
||||
internal TagHelperAttributeDescriptor(string name, PropertyInfo propertyInfo)
|
||||
: this(name, propertyInfo.Name, propertyInfo.PropertyType.FullName)
|
||||
// Internal for testing i.e. for easy TagHelperAttributeDescriptor creation when PropertyInfo is available.
|
||||
internal TagHelperAttributeDescriptor([NotNull] string name, [NotNull] PropertyInfo propertyInfo)
|
||||
: this(
|
||||
name,
|
||||
propertyInfo.Name,
|
||||
propertyInfo.PropertyType.FullName,
|
||||
isStringProperty: propertyInfo.PropertyType == typeof(string))
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -20,35 +26,58 @@ namespace Microsoft.AspNet.Razor.TagHelpers
|
|||
/// Instantiates a new instance of the <see cref="TagHelperAttributeDescriptor"/> class.
|
||||
/// </summary>
|
||||
/// <param name="name">The HTML attribute name.</param>
|
||||
/// <param name="propertyName">The name of the CLR property name that corresponds to the HTML
|
||||
/// attribute.</param>
|
||||
/// <param name="propertyName">The name of the CLR property that corresponds to the HTML attribute.</param>
|
||||
/// <param name="typeName">
|
||||
/// The full name of the named (see <paramref name="propertyName"/>) property's
|
||||
/// <see cref="System.Type"/>.
|
||||
/// The full name of the named (see <paramref name="propertyName"/>) property's <see cref="System.Type"/>.
|
||||
/// </param>
|
||||
public TagHelperAttributeDescriptor(string name,
|
||||
string propertyName,
|
||||
string typeName)
|
||||
public TagHelperAttributeDescriptor(
|
||||
[NotNull] string name,
|
||||
[NotNull] string propertyName,
|
||||
[NotNull] string typeName)
|
||||
: this(
|
||||
name,
|
||||
propertyName,
|
||||
typeName,
|
||||
isStringProperty: string.Equals(typeName, typeof(string).FullName, StringComparison.Ordinal))
|
||||
{
|
||||
}
|
||||
|
||||
// Internal for testing i.e. for confirming above constructor sets `IsStringProperty` as expected.
|
||||
internal TagHelperAttributeDescriptor(
|
||||
[NotNull] string name,
|
||||
[NotNull] string propertyName,
|
||||
[NotNull] string typeName,
|
||||
bool isStringProperty)
|
||||
{
|
||||
Name = name;
|
||||
PropertyName = propertyName;
|
||||
TypeName = typeName;
|
||||
IsStringProperty = isStringProperty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an indication whether this property is of type <see cref="string"/>.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// If <c>true</c> the <see cref="TypeName"/> is for <see cref="string"/>. This causes the Razor parser
|
||||
/// to allow empty values for attributes that have names matching <see cref="Name"/>. If <c>false</c>
|
||||
/// empty values for such matching attributes lead to errors.
|
||||
/// </value>
|
||||
public bool IsStringProperty { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The HTML attribute name.
|
||||
/// </summary>
|
||||
public string Name { get; private set; }
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the CLR property name that corresponds to the HTML attribute name.
|
||||
/// The name of the CLR property that corresponds to the HTML attribute.
|
||||
/// </summary>
|
||||
public string PropertyName { get; private set; }
|
||||
public string PropertyName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The full name of the named (see <see name="PropertyName"/>) property's
|
||||
/// <see cref="System.Type"/>.
|
||||
/// The full name of the named (see <see name="PropertyName"/>) property's <see cref="System.Type"/>.
|
||||
/// </summary>
|
||||
public string TypeName { get; private set; }
|
||||
public string TypeName { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -24,13 +24,17 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
|
|||
return true;
|
||||
}
|
||||
|
||||
// Normal comparer doesn't care about case, in tests we do. Also double-check IsStringProperty though
|
||||
// it is inferred from TypeName.
|
||||
return base.Equals(descriptorX, descriptorY) &&
|
||||
// Normal comparer doesn't care about case, in tests we do.
|
||||
descriptorX.IsStringProperty == descriptorY.IsStringProperty &&
|
||||
string.Equals(descriptorX.Name, descriptorY.Name, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
public override int GetHashCode(TagHelperAttributeDescriptor descriptor)
|
||||
{
|
||||
// Rarely if ever hash TagHelperAttributeDescriptor. If we do, ignore IsStringProperty since it should
|
||||
// not vary for a given TypeName i.e. will not change the bucket.
|
||||
return HashCodeCombiner.Start()
|
||||
.Add(base.GetHashCode(descriptor))
|
||||
.Add(descriptor.Name, StringComparer.Ordinal)
|
||||
|
|
|
|||
|
|
@ -289,8 +289,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
|
|||
typeof(InheritedOverriddenAttributeTagHelper).FullName,
|
||||
AssemblyName,
|
||||
new[] {
|
||||
new TagHelperAttributeDescriptor("valid-attribute1",
|
||||
validProperty1),
|
||||
new TagHelperAttributeDescriptor("valid-attribute1", validProperty1),
|
||||
new TagHelperAttributeDescriptor("Something-Else", validProperty2)
|
||||
})
|
||||
};
|
||||
|
|
@ -363,14 +362,18 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
|
|||
{
|
||||
// Arrange
|
||||
var errorSink = new ErrorSink();
|
||||
var intProperty = typeof(InheritedSingleAttributeTagHelper).GetProperty(
|
||||
nameof(InheritedSingleAttributeTagHelper.IntAttribute));
|
||||
|
||||
// Also confirm isStringProperty is calculated correctly.
|
||||
var expectedDescriptor = new TagHelperDescriptor(
|
||||
"inherited-single-attribute",
|
||||
typeof(InheritedSingleAttributeTagHelper).FullName,
|
||||
AssemblyName,
|
||||
new[] {
|
||||
new TagHelperAttributeDescriptor("int-attribute", intProperty)
|
||||
new TagHelperAttributeDescriptor(
|
||||
"int-attribute",
|
||||
nameof(InheritedSingleAttributeTagHelper.IntAttribute),
|
||||
typeof(int).FullName,
|
||||
isStringProperty: false)
|
||||
});
|
||||
|
||||
// Act
|
||||
|
|
@ -450,8 +453,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
|
|||
typeof(NonPublicAccessorTagHelper).FullName,
|
||||
AssemblyName,
|
||||
new[] {
|
||||
new TagHelperAttributeDescriptor(
|
||||
"valid-attribute", validProperty)
|
||||
new TagHelperAttributeDescriptor("valid-attribute", validProperty)
|
||||
});
|
||||
|
||||
// Act
|
||||
|
|
@ -471,15 +473,19 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
|
|||
{
|
||||
// Arrange
|
||||
var errorSink = new ErrorSink();
|
||||
var boundProperty = typeof(NotBoundAttributeTagHelper).GetProperty(
|
||||
nameof(NotBoundAttributeTagHelper.BoundProperty));
|
||||
|
||||
// Also confirm isStringProperty is calculated correctly.
|
||||
var expectedDescriptor = new TagHelperDescriptor(
|
||||
"not-bound-attribute",
|
||||
typeof(NotBoundAttributeTagHelper).FullName,
|
||||
AssemblyName,
|
||||
new[]
|
||||
{
|
||||
new TagHelperAttributeDescriptor("bound-property", boundProperty)
|
||||
new TagHelperAttributeDescriptor(
|
||||
"bound-property",
|
||||
nameof(NotBoundAttributeTagHelper.BoundProperty),
|
||||
typeof(object).FullName,
|
||||
isStringProperty: false)
|
||||
});
|
||||
|
||||
// Act
|
||||
|
|
@ -516,21 +522,30 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
|
|||
{
|
||||
// Arrange
|
||||
var errorSink = new ErrorSink();
|
||||
var validProp = typeof(MultiTagTagHelper).GetProperty(nameof(MultiTagTagHelper.ValidAttribute));
|
||||
|
||||
// Also confirm isStringProperty is calculated correctly.
|
||||
var expectedDescriptors = new[] {
|
||||
new TagHelperDescriptor(
|
||||
"div",
|
||||
typeof(MultiTagTagHelper).FullName,
|
||||
AssemblyName,
|
||||
new[] {
|
||||
new TagHelperAttributeDescriptor("valid-attribute", validProp)
|
||||
new TagHelperAttributeDescriptor(
|
||||
"valid-attribute",
|
||||
nameof(MultiTagTagHelper.ValidAttribute),
|
||||
typeof(string).FullName,
|
||||
isStringProperty: true)
|
||||
}),
|
||||
new TagHelperDescriptor(
|
||||
"p",
|
||||
typeof(MultiTagTagHelper).FullName,
|
||||
AssemblyName,
|
||||
new[] {
|
||||
new TagHelperAttributeDescriptor("valid-attribute", validProp)
|
||||
new TagHelperAttributeDescriptor(
|
||||
"valid-attribute",
|
||||
nameof(MultiTagTagHelper.ValidAttribute),
|
||||
typeof(string).FullName,
|
||||
isStringProperty: true)
|
||||
})
|
||||
};
|
||||
|
||||
|
|
@ -1104,7 +1119,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
|
|||
|
||||
private class NotBoundAttributeTagHelper
|
||||
{
|
||||
public string BoundProperty { get; set; }
|
||||
public object BoundProperty { get; set; }
|
||||
|
||||
[HtmlAttributeNotBound]
|
||||
public string NotBoundProperty { get; set; }
|
||||
|
|
|
|||
|
|
@ -14,10 +14,10 @@ namespace Microsoft.AspNet.Razor.Test.Generator
|
|||
{
|
||||
public class CSharpTagHelperRenderingTest : TagHelperTestBase
|
||||
{
|
||||
private static IEnumerable<TagHelperDescriptor> DefaultPAndInputTagHelperDescriptors
|
||||
=> BuildPAndInputTagHelperDescriptors(prefix: string.Empty);
|
||||
private static IEnumerable<TagHelperDescriptor> PrefixedPAndInputTagHelperDescriptors
|
||||
=> BuildPAndInputTagHelperDescriptors("THS");
|
||||
private static IEnumerable<TagHelperDescriptor> DefaultPAndInputTagHelperDescriptors { get; }
|
||||
= BuildPAndInputTagHelperDescriptors(prefix: string.Empty);
|
||||
private static IEnumerable<TagHelperDescriptor> PrefixedPAndInputTagHelperDescriptors { get; }
|
||||
= BuildPAndInputTagHelperDescriptors(prefix: "THS");
|
||||
|
||||
private static IEnumerable<TagHelperDescriptor> MinimizedTagHelpers_Descriptors
|
||||
{
|
||||
|
|
@ -609,6 +609,7 @@ namespace Microsoft.AspNet.Razor.Test.Generator
|
|||
var pAgePropertyInfo = typeof(TestType).GetProperty("Age");
|
||||
var inputTypePropertyInfo = typeof(TestType).GetProperty("Type");
|
||||
var checkedPropertyInfo = typeof(TestType).GetProperty("Checked");
|
||||
|
||||
return new[]
|
||||
{
|
||||
new TagHelperDescriptor(
|
||||
|
|
|
|||
|
|
@ -712,13 +712,19 @@ namespace Microsoft.AspNet.Razor.TagHelpers
|
|||
{
|
||||
new TagHelperDescriptor(
|
||||
tagName: "input",
|
||||
typeName: "InputTagHelper",
|
||||
typeName: "InputTagHelper1",
|
||||
assemblyName: "SomeAssembly",
|
||||
attributes: new TagHelperAttributeDescriptor[0],
|
||||
attributes: new[]
|
||||
{
|
||||
new TagHelperAttributeDescriptor(
|
||||
"bound-required-string",
|
||||
"BoundRequiredString",
|
||||
typeof(string).FullName)
|
||||
},
|
||||
requiredAttributes: new[] { "unbound-required" }),
|
||||
new TagHelperDescriptor(
|
||||
tagName: "input",
|
||||
typeName: "InputTagHelper",
|
||||
typeName: "InputTagHelper1",
|
||||
assemblyName: "SomeAssembly",
|
||||
attributes: new[]
|
||||
{
|
||||
|
|
@ -730,7 +736,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers
|
|||
requiredAttributes: new[] { "bound-required-string" }),
|
||||
new TagHelperDescriptor(
|
||||
tagName: "input",
|
||||
typeName: "InputTagHelper",
|
||||
typeName: "InputTagHelper2",
|
||||
assemblyName: "SomeAssembly",
|
||||
attributes: new[]
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,193 @@
|
|||
// 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;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.Razor.TagHelpers
|
||||
{
|
||||
public class TagHelperDescriptorTest
|
||||
{
|
||||
[Fact]
|
||||
public void TagHelperDescriptor_CanBeSerialized()
|
||||
{
|
||||
// Arrange
|
||||
var descriptor = new TagHelperDescriptor(
|
||||
prefix: "prefix:",
|
||||
tagName: "tag name",
|
||||
typeName: "type name",
|
||||
assemblyName: "assembly name",
|
||||
attributes: Enumerable.Empty<TagHelperAttributeDescriptor>(),
|
||||
requiredAttributes: new[] { "required attribute one", "required attribute two" });
|
||||
var expectedSerializedDescriptor =
|
||||
$"{{\"{ nameof(TagHelperDescriptor.Prefix) }\":\"prefix:\"," +
|
||||
$"\"{ nameof(TagHelperDescriptor.TagName) }\":\"tag name\"," +
|
||||
$"\"{ nameof(TagHelperDescriptor.FullTagName) }\":\"prefix:tag name\"," +
|
||||
$"\"{ nameof(TagHelperDescriptor.TypeName) }\":\"type name\"," +
|
||||
$"\"{ nameof(TagHelperDescriptor.AssemblyName) }\":\"assembly name\"," +
|
||||
$"\"{ nameof(TagHelperDescriptor.Attributes) }\":[]," +
|
||||
$"\"{ nameof(TagHelperDescriptor.RequiredAttributes) }\":" +
|
||||
"[\"required attribute one\",\"required attribute two\"]}";
|
||||
|
||||
// Act
|
||||
var serializedDescriptor = JsonConvert.SerializeObject(descriptor);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedSerializedDescriptor, serializedDescriptor, StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TagHelperDescriptor_WithAttributes_CanBeSerialized()
|
||||
{
|
||||
// Arrange
|
||||
var descriptor = new TagHelperDescriptor(
|
||||
prefix: "prefix:",
|
||||
tagName: "tag name",
|
||||
typeName: "type name",
|
||||
assemblyName: "assembly name",
|
||||
attributes: new[]
|
||||
{
|
||||
new TagHelperAttributeDescriptor(
|
||||
name: "attribute one",
|
||||
propertyName: "property name",
|
||||
typeName: "property type name"),
|
||||
new TagHelperAttributeDescriptor(
|
||||
name: "attribute two",
|
||||
propertyName: "property name",
|
||||
typeName: typeof(string).FullName),
|
||||
},
|
||||
requiredAttributes: Enumerable.Empty<string>());
|
||||
var expectedSerializedDescriptor =
|
||||
$"{{\"{ nameof(TagHelperDescriptor.Prefix) }\":\"prefix:\"," +
|
||||
$"\"{ nameof(TagHelperDescriptor.TagName) }\":\"tag name\"," +
|
||||
$"\"{ nameof(TagHelperDescriptor.FullTagName) }\":\"prefix:tag name\"," +
|
||||
$"\"{ nameof(TagHelperDescriptor.TypeName) }\":\"type name\"," +
|
||||
$"\"{ nameof(TagHelperDescriptor.AssemblyName) }\":\"assembly name\"," +
|
||||
$"\"{ nameof(TagHelperDescriptor.Attributes) }\":[" +
|
||||
$"{{\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":false," +
|
||||
$"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute one\"," +
|
||||
$"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," +
|
||||
$"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"property type name\"}}," +
|
||||
$"{{\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":true," +
|
||||
$"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute two\"," +
|
||||
$"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," +
|
||||
$"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"{ typeof(string).FullName }\"}}]," +
|
||||
$"\"{ nameof(TagHelperDescriptor.RequiredAttributes) }\":[]}}";
|
||||
|
||||
// Act
|
||||
var serializedDescriptor = JsonConvert.SerializeObject(descriptor);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedSerializedDescriptor, serializedDescriptor, StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TagHelperDescriptor_CanBeDeserialized()
|
||||
{
|
||||
// Arrange
|
||||
var serializedDescriptor =
|
||||
$"{{\"{nameof(TagHelperDescriptor.Prefix)}\":\"prefix:\"," +
|
||||
$"\"{nameof(TagHelperDescriptor.TagName)}\":\"tag name\"," +
|
||||
$"\"{nameof(TagHelperDescriptor.FullTagName)}\":\"prefix:tag name\"," +
|
||||
$"\"{nameof(TagHelperDescriptor.TypeName)}\":\"type name\"," +
|
||||
$"\"{nameof(TagHelperDescriptor.AssemblyName)}\":\"assembly name\"," +
|
||||
$"\"{nameof(TagHelperDescriptor.Attributes)}\":[]," +
|
||||
$"\"{nameof(TagHelperDescriptor.RequiredAttributes)}\":" +
|
||||
"[\"required attribute one\",\"required attribute two\"]}";
|
||||
var expectedDescriptor = new TagHelperDescriptor(
|
||||
prefix: "prefix:",
|
||||
tagName: "tag name",
|
||||
typeName: "type name",
|
||||
assemblyName: "assembly name",
|
||||
attributes: Enumerable.Empty<TagHelperAttributeDescriptor>(),
|
||||
requiredAttributes: new[] { "required attribute one", "required attribute two" });
|
||||
|
||||
// Act
|
||||
var descriptor = JsonConvert.DeserializeObject<TagHelperDescriptor>(serializedDescriptor);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(descriptor);
|
||||
Assert.Equal(expectedDescriptor.Prefix, descriptor.Prefix, StringComparer.Ordinal);
|
||||
Assert.Equal(expectedDescriptor.TagName, descriptor.TagName, StringComparer.Ordinal);
|
||||
Assert.Equal(expectedDescriptor.FullTagName, descriptor.FullTagName, StringComparer.Ordinal);
|
||||
Assert.Equal(expectedDescriptor.TypeName, descriptor.TypeName, StringComparer.Ordinal);
|
||||
Assert.Equal(expectedDescriptor.AssemblyName, descriptor.AssemblyName, StringComparer.Ordinal);
|
||||
Assert.Empty(descriptor.Attributes);
|
||||
Assert.Equal(expectedDescriptor.RequiredAttributes, descriptor.RequiredAttributes, StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TagHelperDescriptor_WithAttributes_CanBeDeserialized()
|
||||
{
|
||||
// Arrange
|
||||
var serializedDescriptor =
|
||||
$"{{\"{ nameof(TagHelperDescriptor.Prefix) }\":\"prefix:\"," +
|
||||
$"\"{ nameof(TagHelperDescriptor.TagName) }\":\"tag name\"," +
|
||||
$"\"{ nameof(TagHelperDescriptor.FullTagName) }\":\"prefix:tag name\"," +
|
||||
$"\"{ nameof(TagHelperDescriptor.TypeName) }\":\"type name\"," +
|
||||
$"\"{ nameof(TagHelperDescriptor.AssemblyName) }\":\"assembly name\"," +
|
||||
$"\"{ nameof(TagHelperDescriptor.Attributes) }\":[" +
|
||||
$"{{\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":false," +
|
||||
$"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute one\"," +
|
||||
$"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," +
|
||||
$"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"property type name\"}}," +
|
||||
$"{{\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":true," +
|
||||
$"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute two\"," +
|
||||
$"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," +
|
||||
$"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"{ typeof(string).FullName }\"}}]," +
|
||||
$"\"{ nameof(TagHelperDescriptor.RequiredAttributes) }\":[]}}";
|
||||
var expectedDescriptor = new TagHelperDescriptor(
|
||||
prefix: "prefix:",
|
||||
tagName: "tag name",
|
||||
typeName: "type name",
|
||||
assemblyName: "assembly name",
|
||||
attributes: new[]
|
||||
{
|
||||
new TagHelperAttributeDescriptor(
|
||||
name: "attribute one",
|
||||
propertyName: "property name",
|
||||
typeName: "property type name"),
|
||||
new TagHelperAttributeDescriptor(
|
||||
name: "attribute two",
|
||||
propertyName: "property name",
|
||||
typeName: typeof(string).FullName),
|
||||
},
|
||||
requiredAttributes: Enumerable.Empty<string>());
|
||||
|
||||
// Act
|
||||
var descriptor = JsonConvert.DeserializeObject<TagHelperDescriptor>(serializedDescriptor);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(descriptor);
|
||||
Assert.Equal(expectedDescriptor.Prefix, descriptor.Prefix, StringComparer.Ordinal);
|
||||
Assert.Equal(expectedDescriptor.TagName, descriptor.TagName, StringComparer.Ordinal);
|
||||
Assert.Equal(expectedDescriptor.FullTagName, descriptor.FullTagName, StringComparer.Ordinal);
|
||||
Assert.Equal(expectedDescriptor.TypeName, descriptor.TypeName, StringComparer.Ordinal);
|
||||
Assert.Equal(expectedDescriptor.AssemblyName, descriptor.AssemblyName, StringComparer.Ordinal);
|
||||
Assert.Equal(2, descriptor.Attributes.Count);
|
||||
Assert.Equal(expectedDescriptor.Attributes[0].IsStringProperty, descriptor.Attributes[0].IsStringProperty);
|
||||
Assert.Equal(expectedDescriptor.Attributes[0].Name, descriptor.Attributes[0].Name, StringComparer.Ordinal);
|
||||
Assert.Equal(
|
||||
expectedDescriptor.Attributes[0].PropertyName,
|
||||
descriptor.Attributes[0].PropertyName,
|
||||
StringComparer.Ordinal);
|
||||
Assert.Equal(
|
||||
expectedDescriptor.Attributes[0].TypeName,
|
||||
descriptor.Attributes[0].TypeName,
|
||||
StringComparer.Ordinal);
|
||||
Assert.Equal(expectedDescriptor.Attributes[1].IsStringProperty, descriptor.Attributes[1].IsStringProperty);
|
||||
Assert.Equal(expectedDescriptor.Attributes[1].Name, descriptor.Attributes[1].Name, StringComparer.Ordinal);
|
||||
Assert.Equal(
|
||||
expectedDescriptor.Attributes[1].PropertyName,
|
||||
descriptor.Attributes[1].PropertyName,
|
||||
StringComparer.Ordinal);
|
||||
Assert.Equal(
|
||||
expectedDescriptor.Attributes[1].TypeName,
|
||||
descriptor.Attributes[1].TypeName,
|
||||
StringComparer.Ordinal);
|
||||
Assert.Empty(descriptor.RequiredAttributes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3355,6 +3355,95 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
|
|||
SourceLocation.Zero)
|
||||
}
|
||||
},
|
||||
{
|
||||
"<p bar='false <strong'",
|
||||
new MarkupBlock(
|
||||
new MarkupTagHelperBlock("p",
|
||||
new List<KeyValuePair<string, SyntaxTreeNode>>
|
||||
{
|
||||
new KeyValuePair<string, SyntaxTreeNode>(
|
||||
"bar",
|
||||
new MarkupBlock(
|
||||
factory.Markup("false"),
|
||||
factory.Markup(" <strong")))
|
||||
})),
|
||||
new []
|
||||
{
|
||||
new RazorError(
|
||||
string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "p"),
|
||||
SourceLocation.Zero),
|
||||
new RazorError(
|
||||
string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"),
|
||||
SourceLocation.Zero)
|
||||
}
|
||||
},
|
||||
{
|
||||
"<p bar=false'",
|
||||
new MarkupBlock(
|
||||
new MarkupTagHelperBlock("p",
|
||||
new List<KeyValuePair<string, SyntaxTreeNode>>
|
||||
{
|
||||
new KeyValuePair<string, SyntaxTreeNode>(
|
||||
"bar",
|
||||
factory.Markup("false"))
|
||||
})),
|
||||
new []
|
||||
{
|
||||
new RazorError(
|
||||
string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "p"),
|
||||
SourceLocation.Zero),
|
||||
new RazorError(
|
||||
string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"),
|
||||
SourceLocation.Zero),
|
||||
new RazorError(
|
||||
"TagHelper attributes must be welformed.",
|
||||
absoluteIndex: 12,
|
||||
lineIndex: 0,
|
||||
columnIndex: 12)
|
||||
}
|
||||
},
|
||||
{
|
||||
"<p bar=\"false'",
|
||||
new MarkupBlock(
|
||||
new MarkupTagHelperBlock("p",
|
||||
new List<KeyValuePair<string, SyntaxTreeNode>>
|
||||
{
|
||||
new KeyValuePair<string, SyntaxTreeNode>(
|
||||
"bar",
|
||||
factory.Markup("false'"))
|
||||
})),
|
||||
new []
|
||||
{
|
||||
new RazorError(
|
||||
string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "p"),
|
||||
SourceLocation.Zero),
|
||||
new RazorError(
|
||||
string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"),
|
||||
SourceLocation.Zero)
|
||||
}
|
||||
},
|
||||
{
|
||||
"<p bar=\"false' ></p>",
|
||||
new MarkupBlock(
|
||||
new MarkupTagHelperBlock("p",
|
||||
new List<KeyValuePair<string, SyntaxTreeNode>>
|
||||
{
|
||||
new KeyValuePair<string, SyntaxTreeNode>(
|
||||
"bar",
|
||||
new MarkupBlock(
|
||||
factory.Markup("false'"),
|
||||
factory.Markup(" ></p>")))
|
||||
})),
|
||||
new []
|
||||
{
|
||||
new RazorError(
|
||||
string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "p"),
|
||||
SourceLocation.Zero),
|
||||
new RazorError(
|
||||
string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"),
|
||||
SourceLocation.Zero)
|
||||
}
|
||||
},
|
||||
{
|
||||
"<p foo bar<strong>",
|
||||
new MarkupBlock(
|
||||
|
|
|
|||
Loading…
Reference in New Issue