diff --git a/src/Microsoft.AspNetCore.Razor.Runtime/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Razor.Runtime/Properties/Resources.Designer.cs
index b9da9197b7..b2d9e8a71c 100644
--- a/src/Microsoft.AspNetCore.Razor.Runtime/Properties/Resources.Designer.cs
+++ b/src/Microsoft.AspNetCore.Razor.Runtime/Properties/Resources.Designer.cs
@@ -426,6 +426,86 @@ namespace Microsoft.AspNetCore.Razor.Runtime
return string.Format(CultureInfo.CurrentCulture, GetString("ArgumentMustBeAnInstanceOf"), p0);
}
+ ///
+ /// Could not find matching ']' for required attribute '{0}'.
+ ///
+ internal static string TagHelperDescriptorFactory_CouldNotFindMatchingEndBrace
+ {
+ get { return GetString("TagHelperDescriptorFactory_CouldNotFindMatchingEndBrace"); }
+ }
+
+ ///
+ /// Could not find matching ']' for required attribute '{0}'.
+ ///
+ internal static string FormatTagHelperDescriptorFactory_CouldNotFindMatchingEndBrace(object p0)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_CouldNotFindMatchingEndBrace"), p0);
+ }
+
+ ///
+ /// Invalid required attribute character '{0}' in required attribute '{1}'. Separate required attributes with commas.
+ ///
+ internal static string TagHelperDescriptorFactory_InvalidRequiredAttributeCharacter
+ {
+ get { return GetString("TagHelperDescriptorFactory_InvalidRequiredAttributeCharacter"); }
+ }
+
+ ///
+ /// Invalid required attribute character '{0}' in required attribute '{1}'. Separate required attributes with commas.
+ ///
+ internal static string FormatTagHelperDescriptorFactory_InvalidRequiredAttributeCharacter(object p0, object p1)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidRequiredAttributeCharacter"), p0, p1);
+ }
+
+ ///
+ /// Required attribute '{0}' has mismatched quotes '{1}' around value.
+ ///
+ internal static string TagHelperDescriptorFactory_InvalidRequiredAttributeMismatchedQuotes
+ {
+ get { return GetString("TagHelperDescriptorFactory_InvalidRequiredAttributeMismatchedQuotes"); }
+ }
+
+ ///
+ /// Required attribute '{0}' has mismatched quotes '{1}' around value.
+ ///
+ internal static string FormatTagHelperDescriptorFactory_InvalidRequiredAttributeMismatchedQuotes(object p0, object p1)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidRequiredAttributeMismatchedQuotes"), p0, p1);
+ }
+
+ ///
+ /// Required attribute '{0}' has a partial CSS operator. '{1}' must be followed by an equals.
+ ///
+ internal static string TagHelperDescriptorFactory_PartialRequiredAttributeOperator
+ {
+ get { return GetString("TagHelperDescriptorFactory_PartialRequiredAttributeOperator"); }
+ }
+
+ ///
+ /// Required attribute '{0}' has a partial CSS operator. '{1}' must be followed by an equals.
+ ///
+ internal static string FormatTagHelperDescriptorFactory_PartialRequiredAttributeOperator(object p0, object p1)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_PartialRequiredAttributeOperator"), p0, p1);
+ }
+
+ ///
+ /// Invalid character '{0}' in required attribute '{1}'. Expected supported CSS operator or ']'.
+ ///
+ internal static string TagHelperDescriptorFactory_InvalidRequiredAttributeOperator
+ {
+ get { return GetString("TagHelperDescriptorFactory_InvalidRequiredAttributeOperator"); }
+ }
+
+ ///
+ /// Invalid character '{0}' in required attribute '{1}'. Expected supported CSS operator or ']'.
+ ///
+ internal static string FormatTagHelperDescriptorFactory_InvalidRequiredAttributeOperator(object p0, object p1)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidRequiredAttributeOperator"), p0, p1);
+ }
+
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);
diff --git a/src/Microsoft.AspNetCore.Razor.Runtime/Resources.resx b/src/Microsoft.AspNetCore.Razor.Runtime/Resources.resx
index a3d55d9bcf..c175059d9a 100644
--- a/src/Microsoft.AspNetCore.Razor.Runtime/Resources.resx
+++ b/src/Microsoft.AspNetCore.Razor.Runtime/Resources.resx
@@ -195,4 +195,19 @@
Argument must be an instance of '{0}'.
+
+ Could not find matching ']' for required attribute '{0}'.
+
+
+ Invalid required attribute character '{0}' in required attribute '{1}'. Separate required attributes with commas.
+
+
+ Required attribute '{0}' has mismatched quotes '{1}' around value.
+
+
+ Required attribute '{0}' has a partial CSS operator. '{1}' must be followed by an equals.
+
+
+ Invalid character '{0}' in required attribute '{1}'. Expected supported CSS operator or ']'.
+
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Razor.Runtime/Runtime/TagHelpers/TagHelperDescriptorFactory.cs b/src/Microsoft.AspNetCore.Razor.Runtime/Runtime/TagHelpers/TagHelperDescriptorFactory.cs
index 92da7f95e1..ad77c86369 100644
--- a/src/Microsoft.AspNetCore.Razor.Runtime/Runtime/TagHelpers/TagHelperDescriptorFactory.cs
+++ b/src/Microsoft.AspNetCore.Razor.Runtime/Runtime/TagHelpers/TagHelperDescriptorFactory.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
+using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
@@ -21,6 +22,7 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
private const string DataDashPrefix = "data-";
private const string TagHelperNameEnding = "TagHelper";
private const string HtmlCaseRegexReplacement = "-$1$2";
+ private const char RequiredAttributeWildcardSuffix = '*';
// This matches the following AFTER the start of the input string (MATCH).
// Any letter/number followed by an uppercase letter then lowercase letter: 1(Aa), a(Aa), A(Aa)
@@ -153,7 +155,7 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
typeName,
assemblyName,
attributeDescriptors,
- requiredAttributes: Enumerable.Empty(),
+ requiredAttributeDescriptors: Enumerable.Empty(),
allowedChildren: allowedChildren,
tagStructure: default(TagStructure),
parentTag: null,
@@ -235,14 +237,15 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
IEnumerable allowedChildren,
TagHelperDesignTimeDescriptor designTimeDescriptor)
{
- var requiredAttributes = GetCommaSeparatedValues(targetElementAttribute.Attributes);
+ IEnumerable requiredAttributeDescriptors;
+ TryGetRequiredAttributeDescriptors(targetElementAttribute.Attributes, errorSink: null, descriptors: out requiredAttributeDescriptors);
return BuildTagHelperDescriptor(
targetElementAttribute.Tag,
typeName,
assemblyName,
attributeDescriptors,
- requiredAttributes,
+ requiredAttributeDescriptors,
allowedChildren,
targetElementAttribute.ParentTag,
targetElementAttribute.TagStructure,
@@ -254,7 +257,7 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
string typeName,
string assemblyName,
IEnumerable attributeDescriptors,
- IEnumerable requiredAttributes,
+ IEnumerable requiredAttributeDescriptors,
IEnumerable allowedChildren,
string parentTag,
TagStructure tagStructure,
@@ -266,7 +269,7 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
TypeName = typeName,
AssemblyName = assemblyName,
Attributes = attributeDescriptors,
- RequiredAttributes = requiredAttributes,
+ RequiredAttributes = requiredAttributeDescriptors,
AllowedChildren = allowedChildren,
RequiredParent = parentTag,
TagStructure = tagStructure,
@@ -274,15 +277,6 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
};
}
- ///
- /// Internal for testing.
- ///
- internal static IEnumerable GetCommaSeparatedValues(string text)
- {
- // We don't want to remove empty entries, need to notify users of invalid values.
- return text?.Split(',').Select(tagName => tagName.Trim()) ?? Enumerable.Empty();
- }
-
///
/// Internal for testing.
///
@@ -291,20 +285,11 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
ErrorSink errorSink)
{
var validTagName = ValidateName(attribute.Tag, targetingAttributes: false, errorSink: errorSink);
- var validAttributeNames = true;
- var attributeNames = GetCommaSeparatedValues(attribute.Attributes);
-
- foreach (var attributeName in attributeNames)
- {
- if (!ValidateName(attributeName, targetingAttributes: true, errorSink: errorSink))
- {
- validAttributeNames = false;
- }
- }
-
+ IEnumerable requiredAttributeDescriptors;
+ var validRequiredAttributes = TryGetRequiredAttributeDescriptors(attribute.Attributes, errorSink, out requiredAttributeDescriptors);
var validParentTagName = ValidateParentTagName(attribute.ParentTag, errorSink);
- return validTagName && validAttributeNames && validParentTagName;
+ return validTagName && validRequiredAttributes && validParentTagName;
}
///
@@ -325,10 +310,17 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
errorSink: errorSink);
}
- private static bool ValidateName(
- string name,
- bool targetingAttributes,
- ErrorSink errorSink)
+ private static bool TryGetRequiredAttributeDescriptors(
+ string requiredAttributes,
+ ErrorSink errorSink,
+ out IEnumerable descriptors)
+ {
+ var parser = new RequiredAttributeParser(requiredAttributes);
+
+ return parser.TryParse(errorSink, out descriptors);
+ }
+
+ private static bool ValidateName(string name, bool targetingAttributes, ErrorSink errorSink)
{
if (!targetingAttributes &&
string.Equals(
@@ -339,15 +331,6 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
// '*' as the entire name is OK in the HtmlTargetElement catch-all case.
return true;
}
- else if (targetingAttributes &&
- name.EndsWith(
- TagHelperDescriptorProvider.RequiredAttributeWildcardSuffix,
- StringComparison.OrdinalIgnoreCase))
- {
- // A single '*' at the end of a required attribute is valid; everywhere else is invalid. Strip it from
- // the end so we can validate the rest of the name.
- name = name.Substring(0, name.Length - 1);
- }
var targetName = targetingAttributes ?
Resources.TagHelperDescriptorFactory_Attribute :
@@ -750,5 +733,329 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
{
return HtmlCaseRegex.Replace(name, HtmlCaseRegexReplacement).ToLowerInvariant();
}
+
+ // Internal for testing
+ internal class RequiredAttributeParser
+ {
+ private static readonly IReadOnlyDictionary CssValueComparisons =
+ new Dictionary
+ {
+ { '=', TagHelperRequiredAttributeValueComparison.FullMatch },
+ { '^', TagHelperRequiredAttributeValueComparison.PrefixMatch },
+ { '$', TagHelperRequiredAttributeValueComparison.SuffixMatch }
+ };
+ private static readonly char[] InvalidPlainAttributeNameCharacters = { ' ', '\t', ',', RequiredAttributeWildcardSuffix };
+ private static readonly char[] InvalidCssAttributeNameCharacters = (new[] { ' ', '\t', ',', ']' })
+ .Concat(CssValueComparisons.Keys)
+ .ToArray();
+ private static readonly char[] InvalidCssQuotelessValueCharacters = { ' ', '\t', ']' };
+
+ private int _index;
+ private string _requiredAttributes;
+
+ public RequiredAttributeParser(string requiredAttributes)
+ {
+ _requiredAttributes = requiredAttributes;
+ }
+
+ private char Current => _requiredAttributes[_index];
+
+ private bool AtEnd => _index >= _requiredAttributes.Length;
+
+ public bool TryParse(
+ ErrorSink errorSink,
+ out IEnumerable requiredAttributeDescriptors)
+ {
+ if (string.IsNullOrEmpty(_requiredAttributes))
+ {
+ requiredAttributeDescriptors = Enumerable.Empty();
+ return true;
+ }
+
+ requiredAttributeDescriptors = null;
+ var descriptors = new List();
+
+ PassOptionalWhitespace();
+
+ do
+ {
+ TagHelperRequiredAttributeDescriptor descriptor;
+ if (At('['))
+ {
+ descriptor = ParseCssSelector(errorSink);
+ }
+ else
+ {
+ descriptor = ParsePlainSelector(errorSink);
+ }
+
+ if (descriptor == null)
+ {
+ // Failed to create the descriptor due to an invalid required attribute.
+ return false;
+ }
+ else
+ {
+ descriptors.Add(descriptor);
+ }
+
+ PassOptionalWhitespace();
+
+ if (At(','))
+ {
+ _index++;
+
+ if (!EnsureNotAtEnd(errorSink))
+ {
+ return false;
+ }
+ }
+ else if (!AtEnd)
+ {
+ errorSink.OnError(
+ SourceLocation.Zero,
+ Resources.FormatTagHelperDescriptorFactory_InvalidRequiredAttributeCharacter(Current, _requiredAttributes),
+ length: 0);
+ return false;
+ }
+
+ PassOptionalWhitespace();
+ }
+ while (!AtEnd);
+
+ requiredAttributeDescriptors = descriptors;
+ return true;
+ }
+
+ private TagHelperRequiredAttributeDescriptor ParsePlainSelector(ErrorSink errorSink)
+ {
+ var nameEndIndex = _requiredAttributes.IndexOfAny(InvalidPlainAttributeNameCharacters, _index);
+ string attributeName;
+
+ var nameComparison = TagHelperRequiredAttributeNameComparison.FullMatch;
+ if (nameEndIndex == -1)
+ {
+ attributeName = _requiredAttributes.Substring(_index);
+ _index = _requiredAttributes.Length;
+ }
+ else
+ {
+ attributeName = _requiredAttributes.Substring(_index, nameEndIndex - _index);
+ _index = nameEndIndex;
+
+ if (_requiredAttributes[nameEndIndex] == RequiredAttributeWildcardSuffix)
+ {
+ nameComparison = TagHelperRequiredAttributeNameComparison.PrefixMatch;
+
+ // Move past wild card
+ _index++;
+ }
+ }
+
+ TagHelperRequiredAttributeDescriptor descriptor = null;
+ if (ValidateName(attributeName, targetingAttributes: true, errorSink: errorSink))
+ {
+ descriptor = new TagHelperRequiredAttributeDescriptor
+ {
+ Name = attributeName,
+ NameComparison = nameComparison
+ };
+ }
+
+ return descriptor;
+ }
+
+ private string ParseCssAttributeName(ErrorSink errorSink)
+ {
+ var nameStartIndex = _index;
+ var nameEndIndex = _requiredAttributes.IndexOfAny(InvalidCssAttributeNameCharacters, _index);
+ nameEndIndex = nameEndIndex == -1 ? _requiredAttributes.Length : nameEndIndex;
+ _index = nameEndIndex;
+
+ var attributeName = _requiredAttributes.Substring(nameStartIndex, nameEndIndex - nameStartIndex);
+
+ return attributeName;
+ }
+
+ private TagHelperRequiredAttributeValueComparison? ParseCssValueComparison(ErrorSink errorSink)
+ {
+ Debug.Assert(!AtEnd);
+ TagHelperRequiredAttributeValueComparison valueComparison;
+
+ if (CssValueComparisons.TryGetValue(Current, out valueComparison))
+ {
+ var op = Current;
+ _index++;
+
+ if (op != '=' && At('='))
+ {
+ // Two length operator (ex: ^=). Move past the second piece
+ _index++;
+ }
+ else if (op != '=') // We're at an incomplete operator (ex: [foo^]
+ {
+ errorSink.OnError(
+ SourceLocation.Zero,
+ Resources.FormatTagHelperDescriptorFactory_PartialRequiredAttributeOperator(_requiredAttributes, op),
+ length: 0);
+ return null;
+ }
+ }
+ else if (!At(']'))
+ {
+ errorSink.OnError(
+ SourceLocation.Zero,
+ Resources.FormatTagHelperDescriptorFactory_InvalidRequiredAttributeOperator(Current, _requiredAttributes),
+ length: 0);
+ return null;
+ }
+
+ return valueComparison;
+ }
+
+ private string ParseCssValue(ErrorSink errorSink)
+ {
+ int valueStart;
+ int valueEnd;
+ if (At('\'') || At('"'))
+ {
+ var quote = Current;
+
+ // Move past the quote
+ _index++;
+
+ valueStart = _index;
+ valueEnd = _requiredAttributes.IndexOf(quote, _index);
+ if (valueEnd == -1)
+ {
+ errorSink.OnError(
+ SourceLocation.Zero,
+ Resources.FormatTagHelperDescriptorFactory_InvalidRequiredAttributeMismatchedQuotes(
+ _requiredAttributes,
+ quote),
+ length: 0);
+ return null;
+ }
+ _index = valueEnd + 1;
+ }
+ else
+ {
+ valueStart = _index;
+ var valueEndIndex = _requiredAttributes.IndexOfAny(InvalidCssQuotelessValueCharacters, _index);
+ valueEnd = valueEndIndex == -1 ? _requiredAttributes.Length : valueEndIndex;
+ _index = valueEnd;
+ }
+
+ var value = _requiredAttributes.Substring(valueStart, valueEnd - valueStart);
+
+ return value;
+ }
+
+ private TagHelperRequiredAttributeDescriptor ParseCssSelector(ErrorSink errorSink)
+ {
+ Debug.Assert(At('['));
+
+ // Move past '['.
+ _index++;
+ PassOptionalWhitespace();
+
+ var attributeName = ParseCssAttributeName(errorSink);
+
+ PassOptionalWhitespace();
+
+ if (!EnsureNotAtEnd(errorSink))
+ {
+ return null;
+ }
+
+ if (!ValidateName(attributeName, targetingAttributes: true, errorSink: errorSink))
+ {
+ // Couldn't parse a valid attribute name.
+ return null;
+ }
+
+ var valueComparison = ParseCssValueComparison(errorSink);
+
+ if (!valueComparison.HasValue)
+ {
+ return null;
+ }
+
+ PassOptionalWhitespace();
+
+ if (!EnsureNotAtEnd(errorSink))
+ {
+ return null;
+ }
+
+ var value = ParseCssValue(errorSink);
+
+ if (value == null)
+ {
+ // Couldn't parse value
+ return null;
+ }
+
+ PassOptionalWhitespace();
+
+ if (At(']'))
+ {
+ // Move past the ending bracket.
+ _index++;
+ }
+ else if (AtEnd)
+ {
+ errorSink.OnError(
+ SourceLocation.Zero,
+ Resources.FormatTagHelperDescriptorFactory_CouldNotFindMatchingEndBrace(_requiredAttributes),
+ length: 0);
+ return null;
+ }
+ else
+ {
+ errorSink.OnError(
+ SourceLocation.Zero,
+ Resources.FormatTagHelperDescriptorFactory_InvalidRequiredAttributeCharacter(Current, _requiredAttributes),
+ length: 0);
+ return null;
+ }
+
+ return new TagHelperRequiredAttributeDescriptor
+ {
+ Name = attributeName,
+ NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch,
+ Value = value,
+ ValueComparison = valueComparison.Value,
+ };
+ }
+
+ private bool EnsureNotAtEnd(ErrorSink errorSink)
+ {
+ if (AtEnd)
+ {
+ errorSink.OnError(
+ SourceLocation.Zero,
+ Resources.FormatTagHelperDescriptorFactory_CouldNotFindMatchingEndBrace(_requiredAttributes),
+ length: 0);
+
+ return false;
+ }
+
+ return true;
+ }
+
+ private bool At(char c)
+ {
+ return !AtEnd && Current == c;
+ }
+
+ private void PassOptionalWhitespace()
+ {
+ while (!AtEnd && (Current == ' ' || Current == '\t'))
+ {
+ _index++;
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Razor.Runtime/TagHelpers/HtmlTargetElementAttribute.cs b/src/Microsoft.AspNetCore.Razor.Runtime/TagHelpers/HtmlTargetElementAttribute.cs
index d9657e0f56..fe8d3c221a 100644
--- a/src/Microsoft.AspNetCore.Razor.Runtime/TagHelpers/HtmlTargetElementAttribute.cs
+++ b/src/Microsoft.AspNetCore.Razor.Runtime/TagHelpers/HtmlTargetElementAttribute.cs
@@ -45,8 +45,10 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
public string Tag { get; }
///
- /// A comma-separated of attribute names the HTML element must contain for the
- /// to run. * at the end of an attribute name acts as a prefix match.
+ /// A comma-separated of attribute selectors the HTML element must match for the
+ /// to run. * at the end of an attribute name acts as a prefix match. A value
+ /// surrounded by square brackets is handled as a CSS attribute value selector. Operators ^=, $= and
+ /// = are supported e.g. "name", "[name]", "[name=value]", "[ name ^= 'value' ]".
///
public string Attributes { get; set; }
diff --git a/src/Microsoft.AspNetCore.Razor.Test.Sources/CaseSensitiveTagHelperDescriptorComparer.cs b/src/Microsoft.AspNetCore.Razor.Test.Sources/CaseSensitiveTagHelperDescriptorComparer.cs
index a412f7197c..c1ab1e3cc2 100644
--- a/src/Microsoft.AspNetCore.Razor.Test.Sources/CaseSensitiveTagHelperDescriptorComparer.cs
+++ b/src/Microsoft.AspNetCore.Razor.Test.Sources/CaseSensitiveTagHelperDescriptorComparer.cs
@@ -33,7 +33,10 @@ namespace Microsoft.AspNetCore.Razor.Test.Internal
// attributes or prefixes. In tests we do.
Assert.Equal(descriptorX.TagName, descriptorY.TagName, StringComparer.Ordinal);
Assert.Equal(descriptorX.Prefix, descriptorY.Prefix, StringComparer.Ordinal);
- Assert.Equal(descriptorX.RequiredAttributes, descriptorY.RequiredAttributes, StringComparer.Ordinal);
+ Assert.Equal(
+ descriptorX.RequiredAttributes,
+ descriptorY.RequiredAttributes,
+ CaseSensitiveTagHelperRequiredAttributeDescriptorComparer.Default);
Assert.Equal(descriptorX.RequiredParent, descriptorY.RequiredParent, StringComparer.Ordinal);
if (descriptorX.AllowedChildren != descriptorY.AllowedChildren)
@@ -66,9 +69,10 @@ namespace Microsoft.AspNetCore.Razor.Test.Internal
TagHelperDesignTimeDescriptorComparer.Default.GetHashCode(descriptor.DesignTimeDescriptor));
}
- foreach (var requiredAttribute in descriptor.RequiredAttributes.OrderBy(attribute => attribute))
+ foreach (var requiredAttribute in descriptor.RequiredAttributes.OrderBy(attribute => attribute.Name))
{
- hashCodeCombiner.Add(requiredAttribute, StringComparer.Ordinal);
+ hashCodeCombiner.Add(
+ CaseSensitiveTagHelperRequiredAttributeDescriptorComparer.Default.GetHashCode(requiredAttribute));
}
if (descriptor.AllowedChildren != null)
diff --git a/src/Microsoft.AspNetCore.Razor.Test.Sources/CaseSensitiveTagHelperRequiredAttributeDescriptorComparer.cs b/src/Microsoft.AspNetCore.Razor.Test.Sources/CaseSensitiveTagHelperRequiredAttributeDescriptorComparer.cs
new file mode 100644
index 0000000000..9c441f0561
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Razor.Test.Sources/CaseSensitiveTagHelperRequiredAttributeDescriptorComparer.cs
@@ -0,0 +1,43 @@
+// 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 Microsoft.AspNetCore.Razor.Compilation.TagHelpers;
+using Microsoft.Extensions.Internal;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Razor.Test.Internal
+{
+ internal class CaseSensitiveTagHelperRequiredAttributeDescriptorComparer : TagHelperRequiredAttributeDescriptorComparer
+ {
+ public new static readonly CaseSensitiveTagHelperRequiredAttributeDescriptorComparer Default =
+ new CaseSensitiveTagHelperRequiredAttributeDescriptorComparer();
+
+ private CaseSensitiveTagHelperRequiredAttributeDescriptorComparer()
+ : base()
+ {
+ }
+
+ public override bool Equals(TagHelperRequiredAttributeDescriptor descriptorX, TagHelperRequiredAttributeDescriptor descriptorY)
+ {
+ if (descriptorX == descriptorY)
+ {
+ return true;
+ }
+
+ Assert.True(base.Equals(descriptorX, descriptorY));
+ Assert.Equal(descriptorX.Name, descriptorY.Name, StringComparer.Ordinal);
+
+ return true;
+ }
+
+ public override int GetHashCode(TagHelperRequiredAttributeDescriptor descriptor)
+ {
+ var hashCodeCombiner = HashCodeCombiner.Start();
+ hashCodeCombiner.Add(base.GetHashCode(descriptor));
+ hashCodeCombiner.Add(descriptor.Name, StringComparer.Ordinal);
+
+ return hashCodeCombiner.CombinedHash;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Razor/Compilation/TagHelpers/TagHelperDescriptor.cs b/src/Microsoft.AspNetCore.Razor/Compilation/TagHelpers/TagHelperDescriptor.cs
index ef7bf64b83..77f5fa8488 100644
--- a/src/Microsoft.AspNetCore.Razor/Compilation/TagHelpers/TagHelperDescriptor.cs
+++ b/src/Microsoft.AspNetCore.Razor/Compilation/TagHelpers/TagHelperDescriptor.cs
@@ -19,7 +19,7 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
private string _assemblyName;
private IEnumerable _attributes =
Enumerable.Empty();
- private IEnumerable _requiredAttributes = Enumerable.Empty();
+ private IEnumerable _requiredAttributes = Enumerable.Empty();
///
/// Text used as a required prefix when matching HTML start and end tags in the Razor source to available
@@ -140,7 +140,7 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
///
/// * at the end of an attribute name acts as a prefix match.
///
- public IEnumerable RequiredAttributes
+ public IEnumerable RequiredAttributes
{
get
{
diff --git a/src/Microsoft.AspNetCore.Razor/Compilation/TagHelpers/TagHelperDescriptorComparer.cs b/src/Microsoft.AspNetCore.Razor/Compilation/TagHelpers/TagHelperDescriptorComparer.cs
index 1497e440e0..560ebf32b6 100644
--- a/src/Microsoft.AspNetCore.Razor/Compilation/TagHelpers/TagHelperDescriptorComparer.cs
+++ b/src/Microsoft.AspNetCore.Razor/Compilation/TagHelpers/TagHelperDescriptorComparer.cs
@@ -51,9 +51,9 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
descriptorY.RequiredParent,
StringComparison.OrdinalIgnoreCase) &&
Enumerable.SequenceEqual(
- descriptorX.RequiredAttributes.OrderBy(attribute => attribute, StringComparer.OrdinalIgnoreCase),
- descriptorY.RequiredAttributes.OrderBy(attribute => attribute, StringComparer.OrdinalIgnoreCase),
- StringComparer.OrdinalIgnoreCase) &&
+ descriptorX.RequiredAttributes.OrderBy(attribute => attribute.Name, StringComparer.OrdinalIgnoreCase),
+ descriptorY.RequiredAttributes.OrderBy(attribute => attribute.Name, StringComparer.OrdinalIgnoreCase),
+ TagHelperRequiredAttributeDescriptorComparer.Default) &&
(descriptorX.AllowedChildren == descriptorY.AllowedChildren ||
(descriptorX.AllowedChildren != null &&
descriptorY.AllowedChildren != null &&
@@ -80,11 +80,11 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
hashCodeCombiner.Add(descriptor.TagStructure);
var attributes = descriptor.RequiredAttributes.OrderBy(
- attribute => attribute,
+ attribute => attribute.Name,
StringComparer.OrdinalIgnoreCase);
foreach (var attribute in attributes)
{
- hashCodeCombiner.Add(attribute, StringComparer.OrdinalIgnoreCase);
+ hashCodeCombiner.Add(TagHelperRequiredAttributeDescriptorComparer.Default.GetHashCode(attribute));
}
if (descriptor.AllowedChildren != null)
diff --git a/src/Microsoft.AspNetCore.Razor/Compilation/TagHelpers/TagHelperDescriptorProvider.cs b/src/Microsoft.AspNetCore.Razor/Compilation/TagHelpers/TagHelperDescriptorProvider.cs
index d034da740a..47ef9da3b5 100644
--- a/src/Microsoft.AspNetCore.Razor/Compilation/TagHelpers/TagHelperDescriptorProvider.cs
+++ b/src/Microsoft.AspNetCore.Razor/Compilation/TagHelpers/TagHelperDescriptorProvider.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using Microsoft.AspNetCore.Razor.Parser.TagHelpers;
namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
{
@@ -14,8 +15,6 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
{
public const string ElementCatchAllTarget = "*";
- public static readonly string RequiredAttributeWildcardSuffix = "*";
-
private IDictionary> _registrations;
private string _tagHelperPrefix;
@@ -39,14 +38,14 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
///
/// The name of the HTML tag to match. Providing a '*' tag name
/// retrieves catch-all s (descriptors that target every tag).
- /// Attributes the HTML element must contain to match.
+ /// Attributes the HTML element must contain to match.
/// The parent tag name of the given tag.
/// s that apply to the given .
/// Will return an empty if no s are
/// found.
public IEnumerable GetDescriptors(
string tagName,
- IEnumerable attributeNames,
+ IEnumerable> attributes,
string parentTagName)
{
if (!string.IsNullOrEmpty(_tagHelperPrefix) &&
@@ -78,10 +77,10 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
descriptors = matchingDescriptors.Concat(descriptors);
}
- var applicableDescriptors = ApplyRequiredAttributes(descriptors, attributeNames);
+ var applicableDescriptors = ApplyRequiredAttributes(descriptors, attributes);
applicableDescriptors = ApplyParentTagFilter(applicableDescriptors, parentTagName);
- return applicableDescriptors;
+ return applicableDescriptors.ToArray();
}
private IEnumerable ApplyParentTagFilter(
@@ -95,37 +94,12 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
private IEnumerable ApplyRequiredAttributes(
IEnumerable descriptors,
- IEnumerable attributeNames)
+ IEnumerable> attributes)
{
return descriptors.Where(
- descriptor =>
- {
- foreach (var requiredAttribute in descriptor.RequiredAttributes)
- {
- // '*' at the end of a required attribute indicates: apply to attributes prefixed with the
- // required attribute value.
- if (requiredAttribute.EndsWith(
- RequiredAttributeWildcardSuffix,
- StringComparison.OrdinalIgnoreCase))
- {
- var prefix = requiredAttribute.Substring(0, requiredAttribute.Length - 1);
-
- if (!attributeNames.Any(
- attributeName =>
- attributeName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) &&
- !string.Equals(attributeName, prefix, StringComparison.OrdinalIgnoreCase)))
- {
- return false;
- }
- }
- else if (!attributeNames.Contains(requiredAttribute, StringComparer.OrdinalIgnoreCase))
- {
- return false;
- }
- }
-
- return true;
- });
+ descriptor => descriptor.RequiredAttributes.All(
+ requiredAttribute => attributes.Any(
+ attribute => requiredAttribute.IsMatch(attribute.Key, attribute.Value))));
}
private void Register(TagHelperDescriptor descriptor)
diff --git a/src/Microsoft.AspNetCore.Razor/Compilation/TagHelpers/TagHelperRequiredAttributeDescriptor.cs b/src/Microsoft.AspNetCore.Razor/Compilation/TagHelpers/TagHelperRequiredAttributeDescriptor.cs
new file mode 100644
index 0000000000..b813cbf5ec
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Razor/Compilation/TagHelpers/TagHelperRequiredAttributeDescriptor.cs
@@ -0,0 +1,82 @@
+// 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.Diagnostics;
+using System.Linq;
+
+namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
+{
+ ///
+ /// A metadata class describing a required tag helper attribute.
+ ///
+ public class TagHelperRequiredAttributeDescriptor
+ {
+ ///
+ /// The HTML attribute name.
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// The comparison method to use for when determining if an HTML attribute name matches.
+ ///
+ public TagHelperRequiredAttributeNameComparison NameComparison { get; set; }
+
+ ///
+ /// The HTML attribute value.
+ ///
+ public string Value { get; set; }
+
+ ///
+ /// The comparison method to use for when determining if an HTML attribute value matches.
+ ///
+ public TagHelperRequiredAttributeValueComparison ValueComparison { get; set; }
+
+ ///
+ /// Determines if the current matches the given
+ /// and .
+ ///
+ /// An HTML attribute name.
+ /// An HTML attribute value.
+ /// true if the current matches
+ /// and ; false otherwise.
+ public bool IsMatch(string attributeName, string attributeValue)
+ {
+ var nameMatches = false;
+ if (NameComparison == TagHelperRequiredAttributeNameComparison.FullMatch)
+ {
+ nameMatches = string.Equals(Name, attributeName, StringComparison.OrdinalIgnoreCase);
+ }
+ else if (NameComparison == TagHelperRequiredAttributeNameComparison.PrefixMatch)
+ {
+ // attributeName cannot equal the Name if comparing as a PrefixMatch.
+ nameMatches = attributeName.Length != Name.Length &&
+ attributeName.StartsWith(Name, StringComparison.OrdinalIgnoreCase);
+ }
+ else
+ {
+ Debug.Assert(false, "Unknown name comparison.");
+ }
+
+ if (!nameMatches)
+ {
+ return false;
+ }
+
+ switch (ValueComparison)
+ {
+ case TagHelperRequiredAttributeValueComparison.None:
+ return true;
+ case TagHelperRequiredAttributeValueComparison.PrefixMatch: // Value starts with
+ return attributeValue.StartsWith(Value, StringComparison.Ordinal);
+ case TagHelperRequiredAttributeValueComparison.SuffixMatch: // Value ends with
+ return attributeValue.EndsWith(Value, StringComparison.Ordinal);
+ case TagHelperRequiredAttributeValueComparison.FullMatch: // Value equals
+ return string.Equals(attributeValue, Value, StringComparison.Ordinal);
+ default:
+ Debug.Assert(false, "Unknown value comparison.");
+ return false;
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Razor/Compilation/TagHelpers/TagHelperRequiredAttributeDescriptorComparer.cs b/src/Microsoft.AspNetCore.Razor/Compilation/TagHelpers/TagHelperRequiredAttributeDescriptorComparer.cs
new file mode 100644
index 0000000000..da600b517c
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Razor/Compilation/TagHelpers/TagHelperRequiredAttributeDescriptorComparer.cs
@@ -0,0 +1,58 @@
+// 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.Collections.Generic;
+using Microsoft.Extensions.Internal;
+
+namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
+{
+ ///
+ /// An used to check equality between
+ /// two s.
+ ///
+ public class TagHelperRequiredAttributeDescriptorComparer : IEqualityComparer
+ {
+ ///
+ /// A default instance of the .
+ ///
+ public static readonly TagHelperRequiredAttributeDescriptorComparer Default =
+ new TagHelperRequiredAttributeDescriptorComparer();
+
+ ///
+ /// Initializes a new instance.
+ ///
+ protected TagHelperRequiredAttributeDescriptorComparer()
+ {
+ }
+
+ ///
+ public virtual bool Equals(
+ TagHelperRequiredAttributeDescriptor descriptorX,
+ TagHelperRequiredAttributeDescriptor descriptorY)
+ {
+ if (descriptorX == descriptorY)
+ {
+ return true;
+ }
+
+ return descriptorX != null &&
+ descriptorX.NameComparison == descriptorY.NameComparison &&
+ descriptorX.ValueComparison == descriptorY.ValueComparison &&
+ string.Equals(descriptorX.Name, descriptorY.Name, StringComparison.OrdinalIgnoreCase) &&
+ string.Equals(descriptorX.Value, descriptorY.Value, StringComparison.Ordinal);
+ }
+
+ ///
+ public virtual int GetHashCode(TagHelperRequiredAttributeDescriptor descriptor)
+ {
+ var hashCodeCombiner = HashCodeCombiner.Start();
+ hashCodeCombiner.Add(descriptor.NameComparison);
+ hashCodeCombiner.Add(descriptor.ValueComparison);
+ hashCodeCombiner.Add(descriptor.Name, StringComparer.OrdinalIgnoreCase);
+ hashCodeCombiner.Add(descriptor.Value, StringComparer.Ordinal);
+
+ return hashCodeCombiner.CombinedHash;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Razor/Compilation/TagHelpers/TagHelperRequiredAttributeNameComparison.cs b/src/Microsoft.AspNetCore.Razor/Compilation/TagHelpers/TagHelperRequiredAttributeNameComparison.cs
new file mode 100644
index 0000000000..46c76ea1e5
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Razor/Compilation/TagHelpers/TagHelperRequiredAttributeNameComparison.cs
@@ -0,0 +1,21 @@
+// 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.
+
+namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
+{
+ ///
+ /// Acceptable comparison modes.
+ ///
+ public enum TagHelperRequiredAttributeNameComparison
+ {
+ ///
+ /// HTML attribute name case insensitively matches .
+ ///
+ FullMatch,
+
+ ///
+ /// HTML attribute name case insensitively starts with .
+ ///
+ PrefixMatch,
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Razor/Compilation/TagHelpers/TagHelperRequiredAttributeValueComparison.cs b/src/Microsoft.AspNetCore.Razor/Compilation/TagHelpers/TagHelperRequiredAttributeValueComparison.cs
new file mode 100644
index 0000000000..f84ab8c0ff
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Razor/Compilation/TagHelpers/TagHelperRequiredAttributeValueComparison.cs
@@ -0,0 +1,31 @@
+// 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.
+
+namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
+{
+ ///
+ /// Acceptable comparison modes.
+ ///
+ public enum TagHelperRequiredAttributeValueComparison
+ {
+ ///
+ /// HTML attribute value always matches .
+ ///
+ None,
+
+ ///
+ /// HTML attribute value case sensitively matches .
+ ///
+ FullMatch,
+
+ ///
+ /// HTML attribute value case sensitively starts with .
+ ///
+ PrefixMatch,
+
+ ///
+ /// HTML attribute value case sensitively ends with .
+ ///
+ SuffixMatch,
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Razor/Parser/RazorParser.cs b/src/Microsoft.AspNetCore.Razor/Parser/RazorParser.cs
index dbb48d726c..8b762a02d3 100644
--- a/src/Microsoft.AspNetCore.Razor/Parser/RazorParser.cs
+++ b/src/Microsoft.AspNetCore.Razor/Parser/RazorParser.cs
@@ -239,7 +239,8 @@ namespace Microsoft.AspNetCore.Razor.Parser
return addOrRemoveTagHelperSpanVisitor.GetDescriptors(documentRoot);
}
- private static IEnumerable GetDefaultRewriters(ParserBase markupParser)
+ // Internal for testing
+ internal static IEnumerable GetDefaultRewriters(ParserBase markupParser)
{
return new ISyntaxTreeRewriter[]
{
diff --git a/src/Microsoft.AspNetCore.Razor/Parser/TagHelpers/TagHelperParseTreeRewriter.cs b/src/Microsoft.AspNetCore.Razor/Parser/TagHelpers/TagHelperParseTreeRewriter.cs
index eaed0a61f8..6168999c87 100644
--- a/src/Microsoft.AspNetCore.Razor/Parser/TagHelpers/TagHelperParseTreeRewriter.cs
+++ b/src/Microsoft.AspNetCore.Razor/Parser/TagHelpers/TagHelperParseTreeRewriter.cs
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
+using System.Text;
using Microsoft.AspNetCore.Razor.Compilation.TagHelpers;
using Microsoft.AspNetCore.Razor.Parser.SyntaxTree;
using Microsoft.AspNetCore.Razor.TagHelpers;
@@ -14,6 +15,10 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal
{
public class TagHelperParseTreeRewriter : ISyntaxTreeRewriter
{
+ // Internal for testing.
+ // Null characters are invalid markup for HTML attribute values.
+ internal static readonly string InvalidAttributeValueMarker = "\0";
+
// From http://dev.w3.org/html5/spec/Overview.html#elements-0
private static readonly HashSet VoidElements = new HashSet(StringComparer.OrdinalIgnoreCase)
{
@@ -35,10 +40,12 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal
"wbr"
};
- private TagHelperDescriptorProvider _provider;
- private Stack _trackerStack;
+ private readonly List> _htmlAttributeTracker;
+ private readonly StringBuilder _attributeValueBuilder;
+ private readonly TagHelperDescriptorProvider _provider;
+ private readonly Stack _trackerStack;
+ private readonly Stack _blockStack;
private TagHelperBlockTracker _currentTagHelperTracker;
- private Stack _blockStack;
private BlockBuilder _currentBlock;
private string _currentParentTagName;
@@ -47,6 +54,8 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal
_provider = provider;
_trackerStack = new Stack();
_blockStack = new Stack();
+ _attributeValueBuilder = new StringBuilder();
+ _htmlAttributeTracker = new List>();
}
public void Rewrite(RewritingContext context)
@@ -177,7 +186,7 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal
if (!IsEndTag(tagBlock))
{
// We're now in a start tag block, we first need to see if the tag block is a tag helper.
- var providedAttributes = GetAttributeNames(tagBlock);
+ var providedAttributes = GetAttributeNameValuePairs(tagBlock);
descriptors = _provider.GetDescriptors(tagName, providedAttributes, _currentParentTagName);
@@ -246,7 +255,7 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal
{
descriptors = _provider.GetDescriptors(
tagName,
- attributeNames: Enumerable.Empty(),
+ attributes: Enumerable.Empty>(),
parentTagName: _currentParentTagName);
// If there are not TagHelperDescriptors associated with the end tag block that also have no
@@ -299,7 +308,8 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal
return true;
}
- private IEnumerable GetAttributeNames(Block tagBlock)
+ // Internal for testing
+ internal IEnumerable> GetAttributeNameValuePairs(Block tagBlock)
{
// Need to calculate how many children we should take that represent the attributes.
var childrenOffset = IsPartialTag(tagBlock) ? 0 : 1;
@@ -307,32 +317,112 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal
if (childCount <= 1)
{
- return Enumerable.Empty();
+ return Enumerable.Empty>();
}
- var attributeChildren = new List(childCount - 1);
+ _htmlAttributeTracker.Clear();
+
+ var attributes = _htmlAttributeTracker;
+
for (var i = 1; i < childCount; i++)
{
- attributeChildren.Add(tagBlock.Children[i]);
- }
- var attributeNames = new List();
-
- foreach (var child in attributeChildren)
- {
+ var child = tagBlock.Children[i];
Span childSpan;
if (child.IsBlock)
{
- childSpan = ((Block)child).FindFirstDescendentSpan();
+ var childBlock = (Block)child;
+
+ if (childBlock.Type != BlockType.Markup)
+ {
+ // Anything other than markup blocks in the attribute area of tags mangles following attributes.
+ // It's also not supported by TagHelpers, bail early to avoid creating bad attribute value pairs.
+ break;
+ }
+
+ childSpan = childBlock.FindFirstDescendentSpan();
if (childSpan == null)
{
+ _attributeValueBuilder.Append(InvalidAttributeValueMarker);
continue;
}
+
+ // We can assume the first span will always contain attributename=" and the last span will always
+ // contain the final quote. Therefore, if the values not quoted there's no ending quote to skip.
+ var childOffset = 0;
+ if (childSpan.Symbols.Count > 0)
+ {
+ var potentialQuote = childSpan.Symbols[childSpan.Symbols.Count - 1] as HtmlSymbol;
+ if (potentialQuote != null &&
+ (potentialQuote.Type == HtmlSymbolType.DoubleQuote ||
+ potentialQuote.Type == HtmlSymbolType.SingleQuote))
+ {
+ childOffset = 1;
+ }
+ }
+
+ for (var j = 1; j < childBlock.Children.Count - childOffset; j++)
+ {
+ var valueChild = childBlock.Children[j];
+ if (valueChild.IsBlock)
+ {
+ _attributeValueBuilder.Append(InvalidAttributeValueMarker);
+ }
+ else
+ {
+ var valueChildSpan = (Span)valueChild;
+ for (var k = 0; k < valueChildSpan.Symbols.Count; k++)
+ {
+ _attributeValueBuilder.Append(valueChildSpan.Symbols[k].Content);
+ }
+ }
+ }
}
else
{
- childSpan = child as Span;
+ childSpan = (Span)child;
+
+ var afterEquals = false;
+ var atValue = false;
+ var endValueMarker = childSpan.Symbols.Count;
+
+ // Entire attribute is a string
+ for (var j = 0; j < endValueMarker; j++)
+ {
+ var htmlSymbol = (HtmlSymbol)childSpan.Symbols[j];
+
+ if (!afterEquals)
+ {
+ afterEquals = htmlSymbol.Type == HtmlSymbolType.Equals;
+ continue;
+ }
+
+ if (!atValue)
+ {
+ atValue = htmlSymbol.Type != HtmlSymbolType.WhiteSpace &&
+ htmlSymbol.Type != HtmlSymbolType.NewLine;
+
+ if (atValue)
+ {
+ if (htmlSymbol.Type == HtmlSymbolType.DoubleQuote ||
+ htmlSymbol.Type == HtmlSymbolType.SingleQuote)
+ {
+ endValueMarker--;
+ }
+ else
+ {
+ // Current symbol is considered the value (unquoted). Add its content to the
+ // attribute value builder before we move past it.
+ _attributeValueBuilder.Append(htmlSymbol.Content);
+ }
+ }
+
+ continue;
+ }
+
+ _attributeValueBuilder.Append(htmlSymbol.Content);
+ }
}
var start = 0;
@@ -344,8 +434,8 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal
}
}
- var end = 0;
- for (end = start; end < childSpan.Content.Length; end++)
+ var end = start;
+ for (; end < childSpan.Content.Length; end++)
{
if (childSpan.Content[end] == '=')
{
@@ -353,10 +443,15 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal
}
}
- attributeNames.Add(childSpan.Content.Substring(start, end - start));
+ var attributeName = childSpan.Content.Substring(start, end - start);
+ var attributeValue = _attributeValueBuilder.ToString();
+ var attribute = new KeyValuePair(attributeName, attributeValue);
+ attributes.Add(attribute);
+
+ _attributeValueBuilder.Clear();
}
- return attributeNames;
+ return attributes;
}
private bool HasAllowedChildren()
@@ -650,7 +745,7 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal
{
var child = tagBlock.Children[0];
- if (tagBlock.Type != BlockType.Tag || tagBlock.Children.Count == 0|| !(child is Span))
+ if (tagBlock.Type != BlockType.Tag || tagBlock.Children.Count == 0 || !(child is Span))
{
return null;
}
diff --git a/test/Microsoft.AspNetCore.Razor.Runtime.Test/Runtime/TagHelpers/TagHelperDescriptorFactoryTest.cs b/test/Microsoft.AspNetCore.Razor.Runtime.Test/Runtime/TagHelpers/TagHelperDescriptorFactoryTest.cs
index 3ad660a865..b3418e4eee 100644
--- a/test/Microsoft.AspNetCore.Razor.Runtime.Test/Runtime/TagHelpers/TagHelperDescriptorFactoryTest.cs
+++ b/test/Microsoft.AspNetCore.Razor.Runtime.Test/Runtime/TagHelpers/TagHelperDescriptorFactoryTest.cs
@@ -19,6 +19,155 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
protected static readonly string AssemblyName = TagHelperDescriptorFactoryTestAssembly.Name;
+ public static TheoryData RequiredAttributeParserErrorData
+ {
+ get
+ {
+ Func error = (message) => new RazorError(message, SourceLocation.Zero, 0);
+
+ return new TheoryData
+ {
+ { "name,", error(Resources.FormatTagHelperDescriptorFactory_CouldNotFindMatchingEndBrace("name,")) },
+ { " ", error(Resources.FormatHtmlTargetElementAttribute_NameCannotBeNullOrWhitespace("Attribute")) },
+ { "n@me", error(Resources.FormatHtmlTargetElementAttribute_InvalidName("attribute", "n@me", '@')) },
+ { "name extra", error(Resources.FormatTagHelperDescriptorFactory_InvalidRequiredAttributeCharacter('e', "name extra")) },
+ { "[[ ", error(Resources.FormatTagHelperDescriptorFactory_CouldNotFindMatchingEndBrace("[[ ")) },
+ { "[ ", error(Resources.FormatTagHelperDescriptorFactory_CouldNotFindMatchingEndBrace("[ ")) },
+ {
+ "[name='unended]",
+ error(Resources.FormatTagHelperDescriptorFactory_InvalidRequiredAttributeMismatchedQuotes("[name='unended]", '\''))
+ },
+ {
+ "[name='unended",
+ error(Resources.FormatTagHelperDescriptorFactory_InvalidRequiredAttributeMismatchedQuotes("[name='unended", '\''))
+ },
+ { "[name", error(Resources.FormatTagHelperDescriptorFactory_CouldNotFindMatchingEndBrace("[name")) },
+ { "[ ]", error(Resources.FormatHtmlTargetElementAttribute_NameCannotBeNullOrWhitespace("Attribute")) },
+ { "[n@me]", error(Resources.FormatHtmlTargetElementAttribute_InvalidName("attribute", "n@me", '@')) },
+ { "[name@]", error(Resources.FormatHtmlTargetElementAttribute_InvalidName("attribute", "name@", '@')) },
+ { "[name^]", error(Resources.FormatTagHelperDescriptorFactory_PartialRequiredAttributeOperator("[name^]", '^')) },
+ { "[name='value'", error(Resources.FormatTagHelperDescriptorFactory_CouldNotFindMatchingEndBrace("[name='value'")) },
+ { "[name ", error(Resources.FormatTagHelperDescriptorFactory_CouldNotFindMatchingEndBrace("[name ")) },
+ { "[name extra]", error(Resources.FormatTagHelperDescriptorFactory_InvalidRequiredAttributeOperator('e', "[name extra]")) },
+ { "[name=value ", error(Resources.FormatTagHelperDescriptorFactory_CouldNotFindMatchingEndBrace("[name=value ")) },
+ };
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(RequiredAttributeParserErrorData))]
+ public void RequiredAttributeParser_ParsesRequiredAttributesAndLogsErrorCorrectly(
+ string requiredAttributes,
+ RazorError expectedError)
+ {
+ // Arrange
+ var parser = new TagHelperDescriptorFactory.RequiredAttributeParser(requiredAttributes);
+ var errorSink = new ErrorSink();
+ IEnumerable descriptors;
+
+ // Act
+ var parsedCorrectly = parser.TryParse(errorSink, out descriptors);
+
+ // Assert
+ Assert.False(parsedCorrectly);
+ Assert.Null(descriptors);
+ var error = Assert.Single(errorSink.Errors);
+ Assert.Equal(expectedError, error);
+ }
+
+ public static TheoryData RequiredAttributeParserData
+ {
+ get
+ {
+ Func plain =
+ (name, nameComparison) => new TagHelperRequiredAttributeDescriptor
+ {
+ Name = name,
+ NameComparison = nameComparison
+ };
+ Func css =
+ (name, value, valueComparison) => new TagHelperRequiredAttributeDescriptor
+ {
+ Name = name,
+ NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch,
+ Value = value,
+ ValueComparison = valueComparison,
+ };
+
+ return new TheoryData>
+ {
+ { null, Enumerable.Empty() },
+ { string.Empty, Enumerable.Empty() },
+ { "name", new[] { plain("name", TagHelperRequiredAttributeNameComparison.FullMatch) } },
+ { "name-*", new[] { plain("name-", TagHelperRequiredAttributeNameComparison.PrefixMatch) } },
+ { " name-* ", new[] { plain("name-", TagHelperRequiredAttributeNameComparison.PrefixMatch) } },
+ {
+ "asp-route-*,valid , name-* ,extra",
+ new[]
+ {
+ plain("asp-route-", TagHelperRequiredAttributeNameComparison.PrefixMatch),
+ plain("valid", TagHelperRequiredAttributeNameComparison.FullMatch),
+ plain("name-", TagHelperRequiredAttributeNameComparison.PrefixMatch),
+ plain("extra", TagHelperRequiredAttributeNameComparison.FullMatch),
+ }
+ },
+ { "[name]", new[] { css("name", "", TagHelperRequiredAttributeValueComparison.None) } },
+ { "[ name ]", new[] { css("name", "", TagHelperRequiredAttributeValueComparison.None) } },
+ { " [ name ] ", new[] { css("name", "", TagHelperRequiredAttributeValueComparison.None) } },
+ { "[name=]", new[] { css("name", "", TagHelperRequiredAttributeValueComparison.FullMatch) } },
+ { "[name='']", new[] { css("name", "", TagHelperRequiredAttributeValueComparison.FullMatch) } },
+ { "[name ^=]", new[] { css("name", "", TagHelperRequiredAttributeValueComparison.PrefixMatch) } },
+ { "[name=hello]", new[] { css("name", "hello", TagHelperRequiredAttributeValueComparison.FullMatch) } },
+ { "[name= hello]", new[] { css("name", "hello", TagHelperRequiredAttributeValueComparison.FullMatch) } },
+ { "[name='hello']", new[] { css("name", "hello", TagHelperRequiredAttributeValueComparison.FullMatch) } },
+ { "[name=\"hello\"]", new[] { css("name", "hello", TagHelperRequiredAttributeValueComparison.FullMatch) } },
+ { " [ name $= \" hello\" ] ", new[] { css("name", " hello", TagHelperRequiredAttributeValueComparison.SuffixMatch) } },
+ {
+ "[name=\"hello\"],[other^=something ], [val = 'cool']",
+ new[]
+ {
+ css("name", "hello", TagHelperRequiredAttributeValueComparison.FullMatch),
+ css("other", "something", TagHelperRequiredAttributeValueComparison.PrefixMatch),
+ css("val", "cool", TagHelperRequiredAttributeValueComparison.FullMatch) }
+ },
+ {
+ "asp-route-*,[name=\"hello\"],valid ,[other^=something ], name-* ,[val = 'cool'],extra",
+ new[]
+ {
+ plain("asp-route-", TagHelperRequiredAttributeNameComparison.PrefixMatch),
+ css("name", "hello", TagHelperRequiredAttributeValueComparison.FullMatch),
+ plain("valid", TagHelperRequiredAttributeNameComparison.FullMatch),
+ css("other", "something", TagHelperRequiredAttributeValueComparison.PrefixMatch),
+ plain("name-", TagHelperRequiredAttributeNameComparison.PrefixMatch),
+ css("val", "cool", TagHelperRequiredAttributeValueComparison.FullMatch),
+ plain("extra", TagHelperRequiredAttributeNameComparison.FullMatch),
+ }
+ },
+ };
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(RequiredAttributeParserData))]
+ public void RequiredAttributeParser_ParsesRequiredAttributesCorrectly(
+ string requiredAttributes,
+ IEnumerable expectedDescriptors)
+ {
+ // Arrange
+ var parser = new TagHelperDescriptorFactory.RequiredAttributeParser(requiredAttributes);
+ var errorSink = new ErrorSink();
+ IEnumerable descriptors;
+
+ // Act
+ //System.Diagnostics.Debugger.Launch();
+ var parsedCorrectly = parser.TryParse(errorSink, out descriptors);
+
+ // Assert
+ Assert.True(parsedCorrectly);
+ Assert.Empty(errorSink.Errors);
+ Assert.Equal(expectedDescriptors, descriptors, CaseSensitiveTagHelperRequiredAttributeDescriptorComparer.Default);
+ }
+
public static TheoryData IsEnumData
{
get
@@ -617,7 +766,10 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
typeof(AttributeTargetingTagHelper).FullName,
AssemblyName,
attributes,
- requiredAttributes: new[] { "class" })
+ requiredAttributes: new[]
+ {
+ new TagHelperRequiredAttributeDescriptor { Name = "class" }
+ })
}
},
{
@@ -629,7 +781,11 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
typeof(MultiAttributeTargetingTagHelper).FullName,
AssemblyName,
attributes,
- requiredAttributes: new[] { "class", "style" })
+ requiredAttributes: new[]
+ {
+ new TagHelperRequiredAttributeDescriptor { Name = "class" },
+ new TagHelperRequiredAttributeDescriptor { Name = "style" }
+ })
}
},
{
@@ -641,13 +797,20 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
typeof(MultiAttributeAttributeTargetingTagHelper).FullName,
AssemblyName,
attributes,
- requiredAttributes: new[] { "custom" }),
+ requiredAttributes: new[]
+ {
+ new TagHelperRequiredAttributeDescriptor { Name = "custom" }
+ }),
CreateTagHelperDescriptor(
TagHelperDescriptorProvider.ElementCatchAllTarget,
typeof(MultiAttributeAttributeTargetingTagHelper).FullName,
AssemblyName,
attributes,
- requiredAttributes: new[] { "class", "style" })
+ requiredAttributes: new[]
+ {
+ new TagHelperRequiredAttributeDescriptor { Name = "class" },
+ new TagHelperRequiredAttributeDescriptor { Name = "style" }
+ })
}
},
{
@@ -659,7 +822,10 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
typeof(InheritedAttributeTargetingTagHelper).FullName,
AssemblyName,
attributes,
- requiredAttributes: new[] { "style" })
+ requiredAttributes: new[]
+ {
+ new TagHelperRequiredAttributeDescriptor { Name = "style" }
+ })
}
},
{
@@ -671,7 +837,10 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
typeof(RequiredAttributeTagHelper).FullName,
AssemblyName,
attributes,
- requiredAttributes: new[] { "class" })
+ requiredAttributes: new[]
+ {
+ new TagHelperRequiredAttributeDescriptor { Name = "class" }
+ })
}
},
{
@@ -683,7 +852,10 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
typeof(InheritedRequiredAttributeTagHelper).FullName,
AssemblyName,
attributes,
- requiredAttributes: new[] { "class" })
+ requiredAttributes: new[]
+ {
+ new TagHelperRequiredAttributeDescriptor { Name = "class" }
+ })
}
},
{
@@ -695,13 +867,19 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
typeof(MultiAttributeRequiredAttributeTagHelper).FullName,
AssemblyName,
attributes,
- requiredAttributes: new[] { "class" }),
+ requiredAttributes: new[]
+ {
+ new TagHelperRequiredAttributeDescriptor { Name = "class" }
+ }),
CreateTagHelperDescriptor(
"input",
typeof(MultiAttributeRequiredAttributeTagHelper).FullName,
AssemblyName,
attributes,
- requiredAttributes: new[] { "class" })
+ requiredAttributes: new[]
+ {
+ new TagHelperRequiredAttributeDescriptor { Name = "class" }
+ })
}
},
{
@@ -713,13 +891,19 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
typeof(MultiAttributeSameTagRequiredAttributeTagHelper).FullName,
AssemblyName,
attributes,
- requiredAttributes: new[] { "style" }),
+ requiredAttributes: new[]
+ {
+ new TagHelperRequiredAttributeDescriptor { Name = "style" }
+ }),
CreateTagHelperDescriptor(
"input",
typeof(MultiAttributeSameTagRequiredAttributeTagHelper).FullName,
AssemblyName,
attributes,
- requiredAttributes: new[] { "class" })
+ requiredAttributes: new[]
+ {
+ new TagHelperRequiredAttributeDescriptor { Name = "class" }
+ })
}
},
{
@@ -731,7 +915,11 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
typeof(MultiRequiredAttributeTagHelper).FullName,
AssemblyName,
attributes,
- requiredAttributes: new[] { "class", "style" })
+ requiredAttributes: new[]
+ {
+ new TagHelperRequiredAttributeDescriptor { Name = "class" },
+ new TagHelperRequiredAttributeDescriptor { Name = "style" }
+ })
}
},
{
@@ -743,13 +931,20 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
typeof(MultiTagMultiRequiredAttributeTagHelper).FullName,
AssemblyName,
attributes,
- requiredAttributes: new[] { "class", "style" }),
+ requiredAttributes: new[]
+ {
+ new TagHelperRequiredAttributeDescriptor { Name = "class" },
+ new TagHelperRequiredAttributeDescriptor { Name = "style" }
+ }),
CreateTagHelperDescriptor(
"input",
typeof(MultiTagMultiRequiredAttributeTagHelper).FullName,
AssemblyName,
attributes,
- requiredAttributes: new[] { "class", "style" }),
+ requiredAttributes: new[] {
+ new TagHelperRequiredAttributeDescriptor { Name = "class" },
+ new TagHelperRequiredAttributeDescriptor { Name = "style" }
+ }),
}
},
{
@@ -761,7 +956,14 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
typeof(AttributeWildcardTargetingTagHelper).FullName,
AssemblyName,
attributes,
- requiredAttributes: new[] { "class*" })
+ requiredAttributes: new[]
+ {
+ new TagHelperRequiredAttributeDescriptor
+ {
+ Name = "class",
+ NameComparison = TagHelperRequiredAttributeNameComparison.PrefixMatch,
+ }
+ })
}
},
{
@@ -773,7 +975,19 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
typeof(MultiAttributeWildcardTargetingTagHelper).FullName,
AssemblyName,
attributes,
- requiredAttributes: new[] { "class*", "style*" })
+ requiredAttributes: new[]
+ {
+ new TagHelperRequiredAttributeDescriptor
+ {
+ Name = "class",
+ NameComparison = TagHelperRequiredAttributeNameComparison.PrefixMatch,
+ },
+ new TagHelperRequiredAttributeDescriptor
+ {
+ Name = "style",
+ NameComparison = TagHelperRequiredAttributeNameComparison.PrefixMatch,
+ }
+ })
}
},
};
@@ -1327,29 +1541,6 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
}
}
- [Theory]
- [MemberData(nameof(ValidNameData))]
- public void GetCommaSeparatedValues_OutputsCommaSeparatedListOfNames(
- string name,
- IEnumerable expectedNames)
- {
- // Act
- var result = TagHelperDescriptorFactory.GetCommaSeparatedValues(name);
-
- // Assert
- Assert.Equal(expectedNames, result);
- }
-
- [Fact]
- public void GetCommaSeparatedValues_OutputsEmptyArrayForNullValue()
- {
- // Act
- var result = TagHelperDescriptorFactory.GetCommaSeparatedValues(text: null);
-
- // Assert
- Assert.Empty(result);
- }
-
public static TheoryData InvalidTagHelperAttributeDescriptorData
{
get
@@ -2293,7 +2484,7 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
string typeName,
string assemblyName,
IEnumerable attributes = null,
- IEnumerable requiredAttributes = null)
+ IEnumerable requiredAttributes = null)
{
return new TagHelperDescriptor
{
@@ -2301,7 +2492,7 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
TypeName = typeName,
AssemblyName = assemblyName,
Attributes = attributes ?? Enumerable.Empty(),
- RequiredAttributes = requiredAttributes ?? Enumerable.Empty()
+ RequiredAttributes = requiredAttributes ?? Enumerable.Empty()
};
}
diff --git a/test/Microsoft.AspNetCore.Razor.Test/CodeGenerators/CSharpTagHelperRenderingTest.cs b/test/Microsoft.AspNetCore.Razor.Test/CodeGenerators/CSharpTagHelperRenderingTest.cs
index 11adbd38bd..1c5bca449a 100644
--- a/test/Microsoft.AspNetCore.Razor.Test/CodeGenerators/CSharpTagHelperRenderingTest.cs
+++ b/test/Microsoft.AspNetCore.Razor.Test/CodeGenerators/CSharpTagHelperRenderingTest.cs
@@ -21,6 +21,126 @@ namespace Microsoft.AspNetCore.Razor.Test.Generator
private static IEnumerable PrefixedPAndInputTagHelperDescriptors { get; }
= BuildPAndInputTagHelperDescriptors(prefix: "THS");
+ private static IEnumerable CssSelectorTagHelperDescriptors
+ {
+ get
+ {
+ var inputTypePropertyInfo = typeof(TestType).GetProperty("Type");
+ var inputCheckedPropertyInfo = typeof(TestType).GetProperty("Checked");
+
+ return new[]
+ {
+ new TagHelperDescriptor
+ {
+ TagName = "a",
+ TypeName = "TestNamespace.ATagHelper",
+ AssemblyName = "SomeAssembly",
+ RequiredAttributes = new[]
+ {
+ new TagHelperRequiredAttributeDescriptor
+ {
+ Name = "href",
+ NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch,
+ Value = "~/",
+ ValueComparison = TagHelperRequiredAttributeValueComparison.FullMatch,
+ }
+ },
+ },
+ new TagHelperDescriptor
+ {
+ TagName = "a",
+ TypeName = "TestNamespace.ATagHelperMultipleSelectors",
+ AssemblyName = "SomeAssembly",
+ RequiredAttributes = new[]
+ {
+ new TagHelperRequiredAttributeDescriptor
+ {
+ Name = "href",
+ NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch,
+ Value = "~/",
+ ValueComparison = TagHelperRequiredAttributeValueComparison.PrefixMatch,
+ },
+ new TagHelperRequiredAttributeDescriptor
+ {
+ Name = "href",
+ NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch,
+ Value = "?hello=world",
+ ValueComparison = TagHelperRequiredAttributeValueComparison.SuffixMatch,
+ }
+ },
+ },
+ new TagHelperDescriptor
+ {
+ TagName = "input",
+ TypeName = "TestNamespace.InputTagHelper",
+ AssemblyName = "SomeAssembly",
+ Attributes = new TagHelperAttributeDescriptor[]
+ {
+ new TagHelperAttributeDescriptor("type", inputTypePropertyInfo),
+ },
+ RequiredAttributes = new[]
+ {
+ new TagHelperRequiredAttributeDescriptor
+ {
+ Name = "type",
+ NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch,
+ Value = "text",
+ ValueComparison = TagHelperRequiredAttributeValueComparison.FullMatch,
+ }
+ },
+ },
+ new TagHelperDescriptor
+ {
+ TagName = "input",
+ TypeName = "TestNamespace.InputTagHelper2",
+ AssemblyName = "SomeAssembly",
+ Attributes = new TagHelperAttributeDescriptor[]
+ {
+ new TagHelperAttributeDescriptor("type", inputTypePropertyInfo),
+ },
+ RequiredAttributes = new[]
+ {
+ new TagHelperRequiredAttributeDescriptor
+ {
+ Name = "ty",
+ NameComparison = TagHelperRequiredAttributeNameComparison.PrefixMatch,
+ }
+ },
+ },
+ new TagHelperDescriptor
+ {
+ TagName = "*",
+ TypeName = "TestNamespace.CatchAllTagHelper",
+ AssemblyName = "SomeAssembly",
+ RequiredAttributes = new[]
+ {
+ new TagHelperRequiredAttributeDescriptor
+ {
+ Name = "href",
+ NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch,
+ Value = "~/",
+ ValueComparison = TagHelperRequiredAttributeValueComparison.PrefixMatch,
+ }
+ },
+ },
+ new TagHelperDescriptor
+ {
+ TagName = "*",
+ TypeName = "TestNamespace.CatchAllTagHelper2",
+ AssemblyName = "SomeAssembly",
+ RequiredAttributes = new[]
+ {
+ new TagHelperRequiredAttributeDescriptor
+ {
+ Name = "type",
+ NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch,
+ }
+ },
+ }
+ };
+ }
+ }
+
private static IEnumerable EnumTagHelperDescriptors
{
get
@@ -113,7 +233,7 @@ namespace Microsoft.AspNetCore.Razor.Test.Generator
TypeName = typeof(string).FullName
},
},
- RequiredAttributes = new[] { "bound" },
+ RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "bound" } },
},
};
}
@@ -140,7 +260,10 @@ namespace Microsoft.AspNetCore.Razor.Test.Generator
IsStringProperty = true
}
},
- RequiredAttributes = new[] { "catchall-unbound-required" },
+ RequiredAttributes = new[]
+ {
+ new TagHelperRequiredAttributeDescriptor { Name = "catchall-unbound-required" }
+ },
},
new TagHelperDescriptor
{
@@ -164,7 +287,11 @@ namespace Microsoft.AspNetCore.Razor.Test.Generator
IsStringProperty = true
}
},
- RequiredAttributes = new[] { "input-bound-required-string", "input-unbound-required" },
+ RequiredAttributes = new[]
+ {
+ new TagHelperRequiredAttributeDescriptor { Name = "input-bound-required-string" },
+ new TagHelperRequiredAttributeDescriptor { Name = "input-unbound-required" }
+ },
}
};
}
@@ -214,7 +341,7 @@ namespace Microsoft.AspNetCore.Razor.Test.Generator
new TagHelperAttributeDescriptor("type", inputTypePropertyInfo),
new TagHelperAttributeDescriptor("checked", inputCheckedPropertyInfo)
},
- RequiredAttributes = new[] { "type" },
+ RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "type" } },
},
new TagHelperDescriptor
{
@@ -226,7 +353,7 @@ namespace Microsoft.AspNetCore.Razor.Test.Generator
new TagHelperAttributeDescriptor("type", inputTypePropertyInfo),
new TagHelperAttributeDescriptor("checked", inputCheckedPropertyInfo)
},
- RequiredAttributes = new[] { "checked" },
+ RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "checked" } },
},
new TagHelperDescriptor
{
@@ -238,7 +365,7 @@ namespace Microsoft.AspNetCore.Razor.Test.Generator
new TagHelperAttributeDescriptor("type", inputTypePropertyInfo),
new TagHelperAttributeDescriptor("checked", inputCheckedPropertyInfo)
},
- RequiredAttributes = new[] { "type" },
+ RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "type" } },
},
new TagHelperDescriptor
{
@@ -250,7 +377,7 @@ namespace Microsoft.AspNetCore.Razor.Test.Generator
new TagHelperAttributeDescriptor("type", inputTypePropertyInfo),
new TagHelperAttributeDescriptor("checked", inputCheckedPropertyInfo)
},
- RequiredAttributes = new[] { "checked" },
+ RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "checked" } },
}
};
}
@@ -269,7 +396,7 @@ namespace Microsoft.AspNetCore.Razor.Test.Generator
TagName = "p",
TypeName = "TestNamespace.PTagHelper",
AssemblyName = "SomeAssembly",
- RequiredAttributes = new[] { "class" },
+ RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "class" } },
},
new TagHelperDescriptor
{
@@ -280,7 +407,7 @@ namespace Microsoft.AspNetCore.Razor.Test.Generator
{
new TagHelperAttributeDescriptor("type", inputTypePropertyInfo)
},
- RequiredAttributes = new[] { "type" },
+ RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "type" } },
},
new TagHelperDescriptor
{
@@ -292,14 +419,18 @@ namespace Microsoft.AspNetCore.Razor.Test.Generator
new TagHelperAttributeDescriptor("type", inputTypePropertyInfo),
new TagHelperAttributeDescriptor("checked", inputCheckedPropertyInfo)
},
- RequiredAttributes = new[] { "type", "checked" },
+ RequiredAttributes = new[]
+ {
+ new TagHelperRequiredAttributeDescriptor { Name = "type" },
+ new TagHelperRequiredAttributeDescriptor { Name = "checked" }
+ },
},
new TagHelperDescriptor
{
TagName = "*",
TypeName = "TestNamespace.CatchAllTagHelper",
AssemblyName = "SomeAssembly",
- RequiredAttributes = new[] { "catchAll" },
+ RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "catchAll" } },
}
};
}
@@ -1774,6 +1905,7 @@ namespace Microsoft.AspNetCore.Razor.Test.Generator
// Note: The baseline resource name is equivalent to the test resource name.
return new TheoryData>
{
+ { "CssSelectorTagHelperAttributes", null, CssSelectorTagHelperDescriptors },
{ "IncompleteTagHelper", null, DefaultPAndInputTagHelperDescriptors },
{ "SingleTagHelper", null, DefaultPAndInputTagHelperDescriptors },
{ "SingleTagHelperWithNewlineBeforeAttributes", null, DefaultPAndInputTagHelperDescriptors },
diff --git a/test/Microsoft.AspNetCore.Razor.Test/TagHelpers/TagHelperBlockRewriterTest.cs b/test/Microsoft.AspNetCore.Razor.Test/TagHelpers/TagHelperBlockRewriterTest.cs
index e3651ed668..974a36417f 100644
--- a/test/Microsoft.AspNetCore.Razor.Test/TagHelpers/TagHelperBlockRewriterTest.cs
+++ b/test/Microsoft.AspNetCore.Razor.Test/TagHelpers/TagHelperBlockRewriterTest.cs
@@ -157,7 +157,7 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
TypeName = typeof(string).FullName
},
},
- RequiredAttributes = new[] { "bound" },
+ RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "bound" } },
},
};
var descriptorProvider = new TagHelperDescriptorProvider(descriptors);
@@ -3940,7 +3940,10 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
IsStringProperty = true
}
},
- RequiredAttributes = new[] { "unbound-required" }
+ RequiredAttributes = new[]
+ {
+ new TagHelperRequiredAttributeDescriptor { Name = "unbound-required" }
+ }
},
new TagHelperDescriptor
{
@@ -3957,7 +3960,10 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
IsStringProperty = true
}
},
- RequiredAttributes = new[] { "bound-required-string" }
+ RequiredAttributes = new[]
+ {
+ new TagHelperRequiredAttributeDescriptor { Name = "bound-required-string" }
+ }
},
new TagHelperDescriptor
{
@@ -3973,7 +3979,10 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
TypeName = typeof(int).FullName
}
},
- RequiredAttributes = new[] { "bound-required-int" }
+ RequiredAttributes = new[]
+ {
+ new TagHelperRequiredAttributeDescriptor { Name = "bound-required-int" }
+ }
},
new TagHelperDescriptor
{
diff --git a/test/Microsoft.AspNetCore.Razor.Test/TagHelpers/TagHelperDescriptorProviderTest.cs b/test/Microsoft.AspNetCore.Razor.Test/TagHelpers/TagHelperDescriptorProviderTest.cs
index a5faa405eb..523d6c1772 100644
--- a/test/Microsoft.AspNetCore.Razor.Test/TagHelpers/TagHelperDescriptorProviderTest.cs
+++ b/test/Microsoft.AspNetCore.Razor.Test/TagHelpers/TagHelperDescriptorProviderTest.cs
@@ -1,9 +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.Collections.Generic;
using System.Linq;
-using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.AspNetCore.Razor.Test.Internal;
using Xunit;
@@ -73,7 +73,7 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
[Theory]
[MemberData(nameof(RequiredParentData))]
- public void GetDescriptors_ReturnsDescriptorsWithRequiredAttributes(
+ public void GetDescriptors_ReturnsDescriptorsParentTags(
string tagName,
string parentTagName,
IEnumerable availableDescriptors,
@@ -85,7 +85,7 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
// Act
var resolvedDescriptors = provider.GetDescriptors(
tagName,
- attributeNames: Enumerable.Empty(),
+ attributes: Enumerable.Empty>(),
parentTagName: parentTagName);
// Assert
@@ -101,131 +101,155 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
TagName = "div",
TypeName = "DivTagHelper",
AssemblyName = "SomeAssembly",
- RequiredAttributes = new[] { "style" }
+ RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "style" } }
};
var inputDescriptor = new TagHelperDescriptor
{
TagName = "input",
TypeName = "InputTagHelper",
AssemblyName = "SomeAssembly",
- RequiredAttributes = new[] { "class", "style" }
+ RequiredAttributes = new[]
+ {
+ new TagHelperRequiredAttributeDescriptor { Name = "class" },
+ new TagHelperRequiredAttributeDescriptor { Name = "style" }
+ }
};
var inputWildcardPrefixDescriptor = new TagHelperDescriptor
{
TagName = "input",
TypeName = "InputWildCardAttribute",
AssemblyName = "SomeAssembly",
- RequiredAttributes = new[] { "nodashprefix*" }
+ RequiredAttributes = new[]
+ {
+ new TagHelperRequiredAttributeDescriptor
+ {
+ Name = "nodashprefix",
+ NameComparison = TagHelperRequiredAttributeNameComparison.PrefixMatch,
+ }
+ }
};
var catchAllDescriptor = new TagHelperDescriptor
{
TagName = TagHelperDescriptorProvider.ElementCatchAllTarget,
TypeName = "CatchAllTagHelper",
AssemblyName = "SomeAssembly",
- RequiredAttributes = new[] { "class" }
+ RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "class" } }
};
var catchAllDescriptor2 = new TagHelperDescriptor
{
TagName = TagHelperDescriptorProvider.ElementCatchAllTarget,
TypeName = "CatchAllTagHelper",
AssemblyName = "SomeAssembly",
- RequiredAttributes = new[] { "custom", "class" }
+ RequiredAttributes = new[]
+ {
+ new TagHelperRequiredAttributeDescriptor { Name = "custom" },
+ new TagHelperRequiredAttributeDescriptor { Name = "class" }
+ }
};
var catchAllWildcardPrefixDescriptor = new TagHelperDescriptor
{
TagName = TagHelperDescriptorProvider.ElementCatchAllTarget,
TypeName = "CatchAllWildCardAttribute",
AssemblyName = "SomeAssembly",
- RequiredAttributes = new[] { "prefix-*" }
+ RequiredAttributes = new[]
+ {
+ new TagHelperRequiredAttributeDescriptor
+ {
+ Name = "prefix-",
+ NameComparison = TagHelperRequiredAttributeNameComparison.PrefixMatch,
+ }
+ }
};
var defaultAvailableDescriptors =
new[] { divDescriptor, inputDescriptor, catchAllDescriptor, catchAllDescriptor2 };
var defaultWildcardDescriptors =
new[] { inputWildcardPrefixDescriptor, catchAllWildcardPrefixDescriptor };
+ Func> kvp =
+ (name) => new KeyValuePair(name, "test value");
return new TheoryData<
string, // tagName
- IEnumerable, // providedAttributes
+ IEnumerable>, // providedAttributes
IEnumerable, // availableDescriptors
IEnumerable> // expectedDescriptors
{
{
"div",
- new[] { "custom" },
+ new[] { kvp("custom") },
defaultAvailableDescriptors,
Enumerable.Empty()
},
- { "div", new[] { "style" }, defaultAvailableDescriptors, new[] { divDescriptor } },
- { "div", new[] { "class" }, defaultAvailableDescriptors, new[] { catchAllDescriptor } },
+ { "div", new[] { kvp("style") }, defaultAvailableDescriptors, new[] { divDescriptor } },
+ { "div", new[] { kvp("class") }, defaultAvailableDescriptors, new[] { catchAllDescriptor } },
{
"div",
- new[] { "class", "style" },
+ new[] { kvp("class"), kvp("style") },
defaultAvailableDescriptors,
new[] { divDescriptor, catchAllDescriptor }
},
{
"div",
- new[] { "class", "style", "custom" },
+ new[] { kvp("class"), kvp("style"), kvp("custom") },
defaultAvailableDescriptors,
new[] { divDescriptor, catchAllDescriptor, catchAllDescriptor2 }
},
{
"input",
- new[] { "class", "style" },
+ new[] { kvp("class"), kvp("style") },
defaultAvailableDescriptors,
new[] { inputDescriptor, catchAllDescriptor }
},
{
"input",
- new[] { "nodashprefixA" },
+ new[] { kvp("nodashprefixA") },
defaultWildcardDescriptors,
new[] { inputWildcardPrefixDescriptor }
},
{
"input",
- new[] { "nodashprefix-ABC-DEF", "random" },
+ new[] { kvp("nodashprefix-ABC-DEF"), kvp("random") },
defaultWildcardDescriptors,
new[] { inputWildcardPrefixDescriptor }
},
{
"input",
- new[] { "prefixABCnodashprefix" },
+ new[] { kvp("prefixABCnodashprefix") },
defaultWildcardDescriptors,
Enumerable.Empty()
},
{
"input",
- new[] { "prefix-" },
+ new[] { kvp("prefix-") },
defaultWildcardDescriptors,
Enumerable.Empty()
},
{
"input",
- new[] { "nodashprefix" },
+ new[] { kvp("nodashprefix") },
defaultWildcardDescriptors,
Enumerable.Empty()
},
{
"input",
- new[] { "prefix-A" },
+ new[] { kvp("prefix-A") },
defaultWildcardDescriptors,
new[] { catchAllWildcardPrefixDescriptor }
},
{
"input",
- new[] { "prefix-ABC-DEF", "random" },
+ new[] { kvp("prefix-ABC-DEF"), kvp("random") },
defaultWildcardDescriptors,
new[] { catchAllWildcardPrefixDescriptor }
},
{
"input",
- new[] { "prefix-abc", "nodashprefix-def" },
+ new[] { kvp("prefix-abc"), kvp("nodashprefix-def") },
defaultWildcardDescriptors,
new[] { inputWildcardPrefixDescriptor, catchAllWildcardPrefixDescriptor }
},
{
"input",
- new[] { "class", "prefix-abc", "onclick", "nodashprefix-def", "style" },
+ new[] { kvp("class"), kvp("prefix-abc"), kvp("onclick"), kvp("nodashprefix-def"), kvp("style") },
defaultWildcardDescriptors,
new[] { inputWildcardPrefixDescriptor, catchAllWildcardPrefixDescriptor }
},
@@ -237,7 +261,7 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
[MemberData(nameof(RequiredAttributeData))]
public void GetDescriptors_ReturnsDescriptorsWithRequiredAttributes(
string tagName,
- IEnumerable providedAttributes,
+ IEnumerable> providedAttributes,
IEnumerable availableDescriptors,
IEnumerable expectedDescriptors)
{
@@ -265,7 +289,7 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
// Act
var resolvedDescriptors = provider.GetDescriptors(
tagName: "th",
- attributeNames: Enumerable.Empty(),
+ attributes: Enumerable.Empty>(),
parentTagName: "p");
// Assert
@@ -284,11 +308,11 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
// Act
var retrievedDescriptorsDiv = provider.GetDescriptors(
tagName: "th:div",
- attributeNames: Enumerable.Empty(),
+ attributes: Enumerable.Empty>(),
parentTagName: "p");
var retrievedDescriptorsSpan = provider.GetDescriptors(
tagName: "th2:span",
- attributeNames: Enumerable.Empty(),
+ attributes: Enumerable.Empty>(),
parentTagName: "p");
// Assert
@@ -308,11 +332,11 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
// Act
var retrievedDescriptorsDiv = provider.GetDescriptors(
tagName: "th:div",
- attributeNames: Enumerable.Empty(),
+ attributes: Enumerable.Empty>(),
parentTagName: "p");
var retrievedDescriptorsSpan = provider.GetDescriptors(
tagName: "th:span",
- attributeNames: Enumerable.Empty(),
+ attributes: Enumerable.Empty>(),
parentTagName: "p");
// Assert
@@ -333,7 +357,7 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
// Act
var retrievedDescriptors = provider.GetDescriptors(
tagName: "th:div",
- attributeNames: Enumerable.Empty(),
+ attributes: Enumerable.Empty>(),
parentTagName: "p");
// Assert
@@ -354,7 +378,7 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
// Act
var retrievedDescriptorsDiv = provider.GetDescriptors(
tagName: "div",
- attributeNames: Enumerable.Empty(),
+ attributes: Enumerable.Empty>(),
parentTagName: "p");
// Assert
@@ -383,7 +407,7 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
// Act
var retrievedDescriptors = provider.GetDescriptors(
tagName: "foo",
- attributeNames: Enumerable.Empty(),
+ attributes: Enumerable.Empty>(),
parentTagName: "p");
// Assert
@@ -418,11 +442,11 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
// Act
var divDescriptors = provider.GetDescriptors(
tagName: "div",
- attributeNames: Enumerable.Empty(),
+ attributes: Enumerable.Empty>(),
parentTagName: "p");
var spanDescriptors = provider.GetDescriptors(
tagName: "span",
- attributeNames: Enumerable.Empty(),
+ attributes: Enumerable.Empty>(),
parentTagName: "p");
// Assert
@@ -453,7 +477,7 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
// Act
var retrievedDescriptors = provider.GetDescriptors(
tagName: "div",
- attributeNames: Enumerable.Empty(),
+ attributes: Enumerable.Empty>(),
parentTagName: "p");
// Assert
diff --git a/test/Microsoft.AspNetCore.Razor.Test/TagHelpers/TagHelperDescriptorTest.cs b/test/Microsoft.AspNetCore.Razor.Test/TagHelpers/TagHelperDescriptorTest.cs
index 408090eab8..12670dbf57 100644
--- a/test/Microsoft.AspNetCore.Razor.Test/TagHelpers/TagHelperDescriptorTest.cs
+++ b/test/Microsoft.AspNetCore.Razor.Test/TagHelpers/TagHelperDescriptorTest.cs
@@ -21,8 +21,22 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
Prefix = "prefix:",
TagName = "tag name",
TypeName = "type name",
- AssemblyName = "assembly name",
- RequiredAttributes = new[] { "required attribute one", "required attribute two" },
+ AssemblyName = "assembly name",
+ RequiredAttributes = new[]
+ {
+ new TagHelperRequiredAttributeDescriptor
+ {
+ Name = "required attribute one",
+ NameComparison = TagHelperRequiredAttributeNameComparison.PrefixMatch
+ },
+ new TagHelperRequiredAttributeDescriptor
+ {
+ Name = "required attribute two",
+ NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch,
+ Value = "something",
+ ValueComparison = TagHelperRequiredAttributeValueComparison.PrefixMatch,
+ }
+ },
AllowedChildren = new[] { "allowed child one" },
RequiredParent = "parent name",
DesignTimeDescriptor = new TagHelperDesignTimeDescriptor
@@ -41,7 +55,14 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
$"\"{ nameof(TagHelperDescriptor.AssemblyName) }\":\"assembly name\"," +
$"\"{ nameof(TagHelperDescriptor.Attributes) }\":[]," +
$"\"{ nameof(TagHelperDescriptor.RequiredAttributes) }\":" +
- "[\"required attribute one\",\"required attribute two\"]," +
+ $"[{{\"{ nameof(TagHelperRequiredAttributeDescriptor.Name)}\":\"required attribute one\"," +
+ $"\"{ nameof(TagHelperRequiredAttributeDescriptor.NameComparison) }\":1," +
+ $"\"{ nameof(TagHelperRequiredAttributeDescriptor.Value) }\":null," +
+ $"\"{ nameof(TagHelperRequiredAttributeDescriptor.ValueComparison) }\":0}}," +
+ $"{{\"{ nameof(TagHelperRequiredAttributeDescriptor.Name)}\":\"required attribute two\"," +
+ $"\"{ nameof(TagHelperRequiredAttributeDescriptor.NameComparison) }\":0," +
+ $"\"{ nameof(TagHelperRequiredAttributeDescriptor.Value) }\":\"something\"," +
+ $"\"{ nameof(TagHelperRequiredAttributeDescriptor.ValueComparison) }\":2}}]," +
$"\"{ nameof(TagHelperDescriptor.AllowedChildren) }\":[\"allowed child one\"]," +
$"\"{ nameof(TagHelperDescriptor.RequiredParent) }\":\"parent name\"," +
$"\"{ nameof(TagHelperDescriptor.TagStructure) }\":0," +
@@ -200,8 +221,15 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
$"\"{nameof(TagHelperDescriptor.TypeName)}\":\"type name\"," +
$"\"{nameof(TagHelperDescriptor.AssemblyName)}\":\"assembly name\"," +
$"\"{nameof(TagHelperDescriptor.Attributes)}\":[]," +
- $"\"{nameof(TagHelperDescriptor.RequiredAttributes)}\":" +
- "[\"required attribute one\",\"required attribute two\"]," +
+ $"\"{ nameof(TagHelperDescriptor.RequiredAttributes) }\":" +
+ $"[{{\"{ nameof(TagHelperRequiredAttributeDescriptor.Name)}\":\"required attribute one\"," +
+ $"\"{ nameof(TagHelperRequiredAttributeDescriptor.NameComparison) }\":1," +
+ $"\"{ nameof(TagHelperRequiredAttributeDescriptor.Value) }\":null," +
+ $"\"{ nameof(TagHelperRequiredAttributeDescriptor.ValueComparison) }\":0}}," +
+ $"{{\"{ nameof(TagHelperRequiredAttributeDescriptor.Name)}\":\"required attribute two\"," +
+ $"\"{ nameof(TagHelperRequiredAttributeDescriptor.NameComparison) }\":0," +
+ $"\"{ nameof(TagHelperRequiredAttributeDescriptor.Value) }\":\"something\"," +
+ $"\"{ nameof(TagHelperRequiredAttributeDescriptor.ValueComparison) }\":2}}]," +
$"\"{ nameof(TagHelperDescriptor.AllowedChildren) }\":[\"allowed child one\",\"allowed child two\"]," +
$"\"{ nameof(TagHelperDescriptor.RequiredParent) }\":\"parent name\"," +
$"\"{nameof(TagHelperDescriptor.TagStructure)}\":2," +
@@ -215,7 +243,21 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
TagName = "tag name",
TypeName = "type name",
AssemblyName = "assembly name",
- RequiredAttributes = new[] { "required attribute one", "required attribute two" },
+ RequiredAttributes = new[]
+ {
+ new TagHelperRequiredAttributeDescriptor
+ {
+ Name = "required attribute one",
+ NameComparison = TagHelperRequiredAttributeNameComparison.PrefixMatch
+ },
+ new TagHelperRequiredAttributeDescriptor
+ {
+ Name = "required attribute two",
+ NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch,
+ Value = "something",
+ ValueComparison = TagHelperRequiredAttributeValueComparison.PrefixMatch,
+ }
+ },
AllowedChildren = new[] { "allowed child one", "allowed child two" },
RequiredParent = "parent name",
DesignTimeDescriptor = new TagHelperDesignTimeDescriptor
@@ -237,7 +279,7 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
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);
+ Assert.Equal(expectedDescriptor.RequiredAttributes, descriptor.RequiredAttributes, TagHelperRequiredAttributeDescriptorComparer.Default);
Assert.Equal(
expectedDescriptor.DesignTimeDescriptor,
descriptor.DesignTimeDescriptor,
diff --git a/test/Microsoft.AspNetCore.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs b/test/Microsoft.AspNetCore.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs
index eea4e14d5f..f9586621c7 100644
--- a/test/Microsoft.AspNetCore.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs
+++ b/test/Microsoft.AspNetCore.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs
@@ -13,11 +13,74 @@ using Microsoft.AspNetCore.Razor.Compilation.TagHelpers;
using Microsoft.AspNetCore.Razor.Test.Framework;
using Microsoft.AspNetCore.Razor.Text;
using Xunit;
+using Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal;
namespace Microsoft.AspNetCore.Razor.Test.TagHelpers
{
public class TagHelperParseTreeRewriterTest : TagHelperRewritingTestBase
{
+ public static TheoryData GetAttributeNameValuePairsData
+ {
+ get
+ {
+ var factory = CreateDefaultSpanFactory();
+ var blockFactory = new BlockFactory(factory);
+ Func> kvp =
+ (key, value) => new KeyValuePair(key, value);
+ var empty = Enumerable.Empty>();
+ var csharp = TagHelperParseTreeRewriter.InvalidAttributeValueMarker;
+
+ // documentContent, expectedPairs
+ return new TheoryData>>
+ {
+ { "", empty },
+ { "", empty },
+ { "", new[] { kvp("href", csharp) } },
+ { "", new[] { kvp("href", $"prefix{csharp} suffix") } },
+ { "", new[] { kvp("href", "~/home") } },
+ { "", new[] { kvp("href", "~/home"), kvp("", "") } },
+ {
+ "",
+ new[] { kvp("href", $"{csharp}::0"), kvp("class", "btn btn-success"), kvp("random", "") }
+ },
+ { "", new[] { kvp("href", "") } },
+ { "> expectedPairs)
+ {
+ // Arrange
+ var errorSink = new ErrorSink();
+ var parseResult = ParseDocument(documentContent, errorSink);
+ var document = parseResult.Document;
+ var rewriters = RazorParser.GetDefaultRewriters(new HtmlMarkupParser());
+ var rewritingContext = new RewritingContext(document, errorSink);
+ foreach (var rewriter in rewriters)
+ {
+ rewriter.Rewrite(rewritingContext);
+ }
+ var block = rewritingContext.SyntaxTree.Children.First();
+ var parseTreeRewriter = new TagHelperParseTreeRewriter(provider: null);
+
+ // Assert - Guard
+ var tagBlock = Assert.IsType(block);
+ Assert.Equal(BlockType.Tag, tagBlock.Type);
+ Assert.Empty(errorSink.Errors);
+
+ // Act
+ var pairs = parseTreeRewriter.GetAttributeNameValuePairs(tagBlock);
+
+ // Assert
+ Assert.Equal(expectedPairs, pairs);
+ }
+
public static TheoryData PartialRequiredParentData
{
get
@@ -716,7 +779,7 @@ namespace Microsoft.AspNetCore.Razor.Test.TagHelpers
TagName = "strong",
TypeName = "StrongTagHelper",
AssemblyName = "SomeAssembly",
- RequiredAttributes = new[] { "required" },
+ RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "required" } },
AllowedChildren = new[] { "br" }
}
};
@@ -1648,21 +1711,25 @@ namespace Microsoft.AspNetCore.Razor.Test.TagHelpers
TagName = "p",
TypeName = "pTagHelper",
AssemblyName = "SomeAssembly",
- RequiredAttributes = new[] { "class" }
+ RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "class" } }
},
new TagHelperDescriptor
{
TagName = "div",
TypeName = "divTagHelper",
AssemblyName = "SomeAssembly",
- RequiredAttributes = new[] { "class", "style" }
+ RequiredAttributes = new[]
+ {
+ new TagHelperRequiredAttributeDescriptor { Name = "class" },
+ new TagHelperRequiredAttributeDescriptor { Name = "style" }
+ }
},
new TagHelperDescriptor
{
TagName = "*",
TypeName = "catchAllTagHelper",
AssemblyName = "SomeAssembly",
- RequiredAttributes = new[] { "catchAll" }
+ RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "catchAll" } }
}
};
var descriptorProvider = new TagHelperDescriptorProvider(descriptors);
@@ -1911,14 +1978,14 @@ namespace Microsoft.AspNetCore.Razor.Test.TagHelpers
TagName = "p",
TypeName = "pTagHelper",
AssemblyName = "SomeAssembly",
- RequiredAttributes = new[] { "class" }
+ RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "class" } }
},
new TagHelperDescriptor
{
TagName = "*",
TypeName = "catchAllTagHelper",
AssemblyName = "SomeAssembly",
- RequiredAttributes = new[] { "catchAll" }
+ RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "catchAll" } }
}
};
var descriptorProvider = new TagHelperDescriptorProvider(descriptors);
@@ -2135,7 +2202,7 @@ namespace Microsoft.AspNetCore.Razor.Test.TagHelpers
TagName = "p",
TypeName = "pTagHelper",
AssemblyName = "SomeAssembly",
- RequiredAttributes = new[] { "class" }
+ RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "class" } }
}
};
var descriptorProvider = new TagHelperDescriptorProvider(descriptors);
diff --git a/test/Microsoft.AspNetCore.Razor.Test/TagHelpers/TagHelperRequiredAttributeDescriptorTest.cs b/test/Microsoft.AspNetCore.Razor.Test/TagHelpers/TagHelperRequiredAttributeDescriptorTest.cs
new file mode 100644
index 0000000000..eca127af6a
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Razor.Test/TagHelpers/TagHelperRequiredAttributeDescriptorTest.cs
@@ -0,0 +1,173 @@
+using Xunit;
+
+namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
+{
+ public class TagHelperRequiredAttributeDescriptorTest
+ {
+ public static TheoryData RequiredAttributeDescriptorData
+ {
+ get
+ {
+ // requiredAttributeDescriptor, attributeName, attributeValue, expectedResult
+ return new TheoryData
+ {
+ {
+ new TagHelperRequiredAttributeDescriptor
+ {
+ Name = "key"
+ },
+ "KeY",
+ "value",
+ true
+ },
+ {
+ new TagHelperRequiredAttributeDescriptor
+ {
+ Name = "key"
+ },
+ "keys",
+ "value",
+ false
+ },
+ {
+ new TagHelperRequiredAttributeDescriptor
+ {
+ Name = "route-",
+ NameComparison = TagHelperRequiredAttributeNameComparison.PrefixMatch,
+ },
+ "ROUTE-area",
+ "manage",
+ true
+ },
+ {
+ new TagHelperRequiredAttributeDescriptor
+ {
+ Name = "route-",
+ NameComparison = TagHelperRequiredAttributeNameComparison.PrefixMatch,
+ },
+ "routearea",
+ "manage",
+ false
+ },
+ {
+ new TagHelperRequiredAttributeDescriptor
+ {
+ Name = "route-",
+ NameComparison = TagHelperRequiredAttributeNameComparison.PrefixMatch,
+ },
+ "route-",
+ "manage",
+ false
+ },
+ {
+ new TagHelperRequiredAttributeDescriptor
+ {
+ Name = "key",
+ NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch,
+ },
+ "KeY",
+ "value",
+ true
+ },
+ {
+ new TagHelperRequiredAttributeDescriptor
+ {
+ Name = "key",
+ NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch,
+ },
+ "keys",
+ "value",
+ false
+ },
+ {
+ new TagHelperRequiredAttributeDescriptor
+ {
+ Name = "key",
+ NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch,
+ Value = "value",
+ ValueComparison = TagHelperRequiredAttributeValueComparison.FullMatch,
+ },
+ "key",
+ "value",
+ true
+ },
+ {
+ new TagHelperRequiredAttributeDescriptor
+ {
+ Name = "key",
+ NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch,
+ Value = "value",
+ ValueComparison = TagHelperRequiredAttributeValueComparison.FullMatch,
+ },
+ "key",
+ "Value",
+ false
+ },
+ {
+ new TagHelperRequiredAttributeDescriptor
+ {
+ Name = "class",
+ NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch,
+ Value = "btn",
+ ValueComparison = TagHelperRequiredAttributeValueComparison.PrefixMatch,
+ },
+ "class",
+ "btn btn-success",
+ true
+ },
+ {
+ new TagHelperRequiredAttributeDescriptor
+ {
+ Name = "class",
+ NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch,
+ Value = "btn",
+ ValueComparison = TagHelperRequiredAttributeValueComparison.PrefixMatch,
+ },
+ "class",
+ "BTN btn-success",
+ false
+ },
+ {
+ new TagHelperRequiredAttributeDescriptor
+ {
+ Name = "href",
+ NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch,
+ Value = "#navigate",
+ ValueComparison = TagHelperRequiredAttributeValueComparison.SuffixMatch,
+ },
+ "href",
+ "/home/index#navigate",
+ true
+ },
+ {
+ new TagHelperRequiredAttributeDescriptor
+ {
+ Name = "href",
+ NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch,
+ Value = "#navigate",
+ ValueComparison = TagHelperRequiredAttributeValueComparison.SuffixMatch,
+ },
+ "href",
+ "/home/index#NAVigate",
+ false
+ },
+ };
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(RequiredAttributeDescriptorData))]
+ public void Matches_ReturnsExpectedResult(
+ TagHelperRequiredAttributeDescriptor requiredAttributeDescriptor,
+ string attributeName,
+ string attributeValue,
+ bool expectedResult)
+ {
+ // Act
+ var result = requiredAttributeDescriptor.IsMatch(attributeName, attributeValue);
+
+ // Assert
+ Assert.Equal(expectedResult, result);
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Razor.Test/TestFiles/CodeGenerator/Output/CssSelectorTagHelperAttributes.cs b/test/Microsoft.AspNetCore.Razor.Test/TestFiles/CodeGenerator/Output/CssSelectorTagHelperAttributes.cs
new file mode 100644
index 0000000000..9370be99be
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Razor.Test/TestFiles/CodeGenerator/Output/CssSelectorTagHelperAttributes.cs
@@ -0,0 +1,272 @@
+#pragma checksum "CssSelectorTagHelperAttributes.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "7c9072aeb0075e207732cb34646d54529eade9f0"
+namespace TestOutput
+{
+ using System;
+ using System.Threading.Tasks;
+
+ public class CssSelectorTagHelperAttributes
+ {
+ #line hidden
+ #pragma warning disable 0414
+ private global::Microsoft.AspNetCore.Razor.TagHelperContent __tagHelperStringValueBuffer = null;
+ #pragma warning restore 0414
+ private global::Microsoft.AspNetCore.Razor.Runtime.TagHelperExecutionContext __tagHelperExecutionContext = null;
+ private global::Microsoft.AspNetCore.Razor.Runtime.TagHelperRunner __tagHelperRunner = null;
+ private global::Microsoft.AspNetCore.Razor.Runtime.TagHelperScopeManager __tagHelperScopeManager = new global::Microsoft.AspNetCore.Razor.Runtime.TagHelperScopeManager();
+ private global::TestNamespace.ATagHelper __TestNamespace_ATagHelper = null;
+ private global::TestNamespace.CatchAllTagHelper __TestNamespace_CatchAllTagHelper = null;
+ private static readonly global::Microsoft.AspNetCore.Razor.TagHelpers.TagHelperAttribute __tagHelperAttribute_0 = new global::Microsoft.AspNetCore.Razor.TagHelpers.TagHelperAttribute("href", new global::Microsoft.AspNetCore.Html.HtmlEncodedString("~/"));
+ private static readonly global::Microsoft.AspNetCore.Razor.TagHelpers.TagHelperAttribute __tagHelperAttribute_1 = new global::Microsoft.AspNetCore.Razor.TagHelpers.TagHelperAttribute("href", new global::Microsoft.AspNetCore.Html.HtmlEncodedString("~/hello"));
+ private global::TestNamespace.ATagHelperMultipleSelectors __TestNamespace_ATagHelperMultipleSelectors = null;
+ private static readonly global::Microsoft.AspNetCore.Razor.TagHelpers.TagHelperAttribute __tagHelperAttribute_2 = new global::Microsoft.AspNetCore.Razor.TagHelpers.TagHelperAttribute("href", new global::Microsoft.AspNetCore.Html.HtmlEncodedString("~/?hello=world"));
+ private static readonly global::Microsoft.AspNetCore.Razor.TagHelpers.TagHelperAttribute __tagHelperAttribute_3 = new global::Microsoft.AspNetCore.Razor.TagHelpers.TagHelperAttribute("href", new global::Microsoft.AspNetCore.Html.HtmlEncodedString("~/?hello=world@false"));
+ private global::TestNamespace.InputTagHelper __TestNamespace_InputTagHelper = null;
+ private global::TestNamespace.InputTagHelper2 __TestNamespace_InputTagHelper2 = null;
+ private global::TestNamespace.CatchAllTagHelper2 __TestNamespace_CatchAllTagHelper2 = null;
+ private static readonly global::Microsoft.AspNetCore.Razor.TagHelpers.TagHelperAttribute __tagHelperAttribute_4 = new global::Microsoft.AspNetCore.Razor.TagHelpers.TagHelperAttribute("value", new global::Microsoft.AspNetCore.Html.HtmlEncodedString("3 TagHelpers"));
+ private static readonly global::Microsoft.AspNetCore.Razor.TagHelpers.TagHelperAttribute __tagHelperAttribute_5 = new global::Microsoft.AspNetCore.Razor.TagHelpers.TagHelperAttribute("value", new global::Microsoft.AspNetCore.Html.HtmlEncodedString("2 TagHelper"));
+ #line hidden
+ public CssSelectorTagHelperAttributes()
+ {
+ }
+
+ #pragma warning disable 1998
+ public override async Task ExecuteAsync()
+ {
+ __tagHelperRunner = __tagHelperRunner ?? new global::Microsoft.AspNetCore.Razor.Runtime.TagHelperRunner();
+ Instrumentation.BeginContext(30, 2, true);
+ WriteLiteral("\r\n");
+ Instrumentation.EndContext();
+ __tagHelperExecutionContext = __tagHelperScopeManager.Begin("a", global::Microsoft.AspNetCore.Razor.TagHelpers.TagMode.StartTagAndEndTag, "test", async() => {
+ Instrumentation.BeginContext(45, 13, true);
+ WriteLiteral("2 TagHelpers.");
+ Instrumentation.EndContext();
+ }
+ , StartTagHelperWritingScope, EndTagHelperWritingScope);
+ __TestNamespace_ATagHelper = CreateTagHelper();
+ __tagHelperExecutionContext.Add(__TestNamespace_ATagHelper);
+ __TestNamespace_CatchAllTagHelper = CreateTagHelper();
+ __tagHelperExecutionContext.Add(__TestNamespace_CatchAllTagHelper);
+ __tagHelperExecutionContext.AddHtmlAttribute(__tagHelperAttribute_0);
+ __tagHelperExecutionContext.Output = await __tagHelperRunner.RunAsync(__tagHelperExecutionContext);
+ if (!__tagHelperExecutionContext.Output.IsContentModified)
+ {
+ __tagHelperExecutionContext.Output.Content = await __tagHelperExecutionContext.Output.GetChildContentAsync();
+ }
+ Instrumentation.BeginContext(32, 30, false);
+ Write(__tagHelperExecutionContext.Output);
+ Instrumentation.EndContext();
+ __tagHelperExecutionContext = __tagHelperScopeManager.End();
+ Instrumentation.BeginContext(62, 2, true);
+ WriteLiteral("\r\n");
+ Instrumentation.EndContext();
+ __tagHelperExecutionContext = __tagHelperScopeManager.Begin("a", global::Microsoft.AspNetCore.Razor.TagHelpers.TagMode.StartTagAndEndTag, "test", async() => {
+ Instrumentation.BeginContext(80, 12, true);
+ WriteLiteral("1 TagHelper.");
+ Instrumentation.EndContext();
+ }
+ , StartTagHelperWritingScope, EndTagHelperWritingScope);
+ __TestNamespace_CatchAllTagHelper = CreateTagHelper();
+ __tagHelperExecutionContext.Add(__TestNamespace_CatchAllTagHelper);
+ __tagHelperExecutionContext.AddHtmlAttribute(__tagHelperAttribute_1);
+ __tagHelperExecutionContext.Output = await __tagHelperRunner.RunAsync(__tagHelperExecutionContext);
+ if (!__tagHelperExecutionContext.Output.IsContentModified)
+ {
+ __tagHelperExecutionContext.Output.Content = await __tagHelperExecutionContext.Output.GetChildContentAsync();
+ }
+ Instrumentation.BeginContext(64, 32, false);
+ Write(__tagHelperExecutionContext.Output);
+ Instrumentation.EndContext();
+ __tagHelperExecutionContext = __tagHelperScopeManager.End();
+ Instrumentation.BeginContext(96, 2, true);
+ WriteLiteral("\r\n");
+ Instrumentation.EndContext();
+ __tagHelperExecutionContext = __tagHelperScopeManager.Begin("a", global::Microsoft.AspNetCore.Razor.TagHelpers.TagMode.StartTagAndEndTag, "test", async() => {
+ Instrumentation.BeginContext(123, 12, true);
+ WriteLiteral("2 TagHelpers");
+ Instrumentation.EndContext();
+ }
+ , StartTagHelperWritingScope, EndTagHelperWritingScope);
+ __TestNamespace_ATagHelperMultipleSelectors = CreateTagHelper();
+ __tagHelperExecutionContext.Add(__TestNamespace_ATagHelperMultipleSelectors);
+ __TestNamespace_CatchAllTagHelper = CreateTagHelper();
+ __tagHelperExecutionContext.Add(__TestNamespace_CatchAllTagHelper);
+ __tagHelperExecutionContext.AddHtmlAttribute(__tagHelperAttribute_2);
+ __tagHelperExecutionContext.Output = await __tagHelperRunner.RunAsync(__tagHelperExecutionContext);
+ if (!__tagHelperExecutionContext.Output.IsContentModified)
+ {
+ __tagHelperExecutionContext.Output.Content = await __tagHelperExecutionContext.Output.GetChildContentAsync();
+ }
+ Instrumentation.BeginContext(98, 41, false);
+ Write(__tagHelperExecutionContext.Output);
+ Instrumentation.EndContext();
+ __tagHelperExecutionContext = __tagHelperScopeManager.End();
+ Instrumentation.BeginContext(139, 2, true);
+ WriteLiteral("\r\n");
+ Instrumentation.EndContext();
+ __tagHelperExecutionContext = __tagHelperScopeManager.Begin("a", global::Microsoft.AspNetCore.Razor.TagHelpers.TagMode.StartTagAndEndTag, "test", async() => {
+ Instrumentation.BeginContext(172, 12, true);
+ WriteLiteral("2 TagHelpers");
+ Instrumentation.EndContext();
+ }
+ , StartTagHelperWritingScope, EndTagHelperWritingScope);
+ __TestNamespace_ATagHelperMultipleSelectors = CreateTagHelper();
+ __tagHelperExecutionContext.Add(__TestNamespace_ATagHelperMultipleSelectors);
+ __TestNamespace_CatchAllTagHelper = CreateTagHelper();
+ __tagHelperExecutionContext.Add(__TestNamespace_CatchAllTagHelper);
+ BeginAddHtmlAttributeValues(__tagHelperExecutionContext, "href", 3);
+ AddHtmlAttributeValue("", 150, "~/", 150, 2, true);
+#line 6 "CssSelectorTagHelperAttributes.cshtml"
+AddHtmlAttributeValue("", 152, false, 152, 6, false);
+
+#line default
+#line hidden
+ AddHtmlAttributeValue("", 158, "?hello=world", 158, 12, true);
+ EndAddHtmlAttributeValues(__tagHelperExecutionContext);
+ __tagHelperExecutionContext.Output = await __tagHelperRunner.RunAsync(__tagHelperExecutionContext);
+ if (!__tagHelperExecutionContext.Output.IsContentModified)
+ {
+ __tagHelperExecutionContext.Output.Content = await __tagHelperExecutionContext.Output.GetChildContentAsync();
+ }
+ Instrumentation.BeginContext(141, 47, false);
+ Write(__tagHelperExecutionContext.Output);
+ Instrumentation.EndContext();
+ __tagHelperExecutionContext = __tagHelperScopeManager.End();
+ Instrumentation.BeginContext(188, 35, true);
+ WriteLiteral("\r\n0 TagHelpers.\r\n");
+ Instrumentation.EndContext();
+ __tagHelperExecutionContext = __tagHelperScopeManager.Begin("a", global::Microsoft.AspNetCore.Razor.TagHelpers.TagMode.StartTagAndEndTag, "test", async() => {
+ Instrumentation.BeginContext(240, 11, true);
+ WriteLiteral("1 TagHelper");
+ Instrumentation.EndContext();
+ }
+ , StartTagHelperWritingScope, EndTagHelperWritingScope);
+ __TestNamespace_CatchAllTagHelper = CreateTagHelper();
+ __tagHelperExecutionContext.Add(__TestNamespace_CatchAllTagHelper);
+ BeginAddHtmlAttributeValues(__tagHelperExecutionContext, "href", 2);
+ AddHtmlAttributeValue("", 231, "~/", 231, 2, true);
+#line 8 "CssSelectorTagHelperAttributes.cshtml"
+AddHtmlAttributeValue("", 233, false, 233, 6, false);
+
+#line default
+#line hidden
+ EndAddHtmlAttributeValues(__tagHelperExecutionContext);
+ __tagHelperExecutionContext.Output = await __tagHelperRunner.RunAsync(__tagHelperExecutionContext);
+ if (!__tagHelperExecutionContext.Output.IsContentModified)
+ {
+ __tagHelperExecutionContext.Output.Content = await __tagHelperExecutionContext.Output.GetChildContentAsync();
+ }
+ Instrumentation.BeginContext(223, 32, false);
+ Write(__tagHelperExecutionContext.Output);
+ Instrumentation.EndContext();
+ __tagHelperExecutionContext = __tagHelperScopeManager.End();
+ Instrumentation.BeginContext(255, 2, true);
+ WriteLiteral("\r\n");
+ Instrumentation.EndContext();
+ __tagHelperExecutionContext = __tagHelperScopeManager.Begin("a", global::Microsoft.AspNetCore.Razor.TagHelpers.TagMode.StartTagAndEndTag, "test", async() => {
+ Instrumentation.BeginContext(288, 11, true);
+ WriteLiteral("1 TagHelper");
+ Instrumentation.EndContext();
+ }
+ , StartTagHelperWritingScope, EndTagHelperWritingScope);
+ __TestNamespace_CatchAllTagHelper = CreateTagHelper();
+ __tagHelperExecutionContext.Add(__TestNamespace_CatchAllTagHelper);
+ __tagHelperExecutionContext.AddHtmlAttribute(__tagHelperAttribute_3);
+ __tagHelperExecutionContext.Output = await __tagHelperRunner.RunAsync(__tagHelperExecutionContext);
+ if (!__tagHelperExecutionContext.Output.IsContentModified)
+ {
+ __tagHelperExecutionContext.Output.Content = await __tagHelperExecutionContext.Output.GetChildContentAsync();
+ }
+ Instrumentation.BeginContext(257, 46, false);
+ Write(__tagHelperExecutionContext.Output);
+ Instrumentation.EndContext();
+ __tagHelperExecutionContext = __tagHelperScopeManager.End();
+ Instrumentation.BeginContext(303, 2, true);
+ WriteLiteral("\r\n");
+ Instrumentation.EndContext();
+ __tagHelperExecutionContext = __tagHelperScopeManager.Begin("a", global::Microsoft.AspNetCore.Razor.TagHelpers.TagMode.StartTagAndEndTag, "test", async() => {
+ Instrumentation.BeginContext(337, 11, true);
+ WriteLiteral("1 TagHelper");
+ Instrumentation.EndContext();
+ }
+ , StartTagHelperWritingScope, EndTagHelperWritingScope);
+ __TestNamespace_CatchAllTagHelper = CreateTagHelper();
+ __tagHelperExecutionContext.Add(__TestNamespace_CatchAllTagHelper);
+ BeginAddHtmlAttributeValues(__tagHelperExecutionContext, "href", 2);
+ AddHtmlAttributeValue("", 314, "~/?hello=world", 314, 14, true);
+#line 10 "CssSelectorTagHelperAttributes.cshtml"
+AddHtmlAttributeValue(" ", 328, false, 329, 7, false);
+
+#line default
+#line hidden
+ EndAddHtmlAttributeValues(__tagHelperExecutionContext);
+ __tagHelperExecutionContext.Output = await __tagHelperRunner.RunAsync(__tagHelperExecutionContext);
+ if (!__tagHelperExecutionContext.Output.IsContentModified)
+ {
+ __tagHelperExecutionContext.Output.Content = await __tagHelperExecutionContext.Output.GetChildContentAsync();
+ }
+ Instrumentation.BeginContext(305, 47, false);
+ Write(__tagHelperExecutionContext.Output);
+ Instrumentation.EndContext();
+ __tagHelperExecutionContext = __tagHelperScopeManager.End();
+ Instrumentation.BeginContext(352, 2, true);
+ WriteLiteral("\r\n");
+ Instrumentation.EndContext();
+ __tagHelperExecutionContext = __tagHelperScopeManager.Begin("input", global::Microsoft.AspNetCore.Razor.TagHelpers.TagMode.SelfClosing, "test", async() => {
+ }
+ , StartTagHelperWritingScope, EndTagHelperWritingScope);
+ __TestNamespace_InputTagHelper = CreateTagHelper();
+ __tagHelperExecutionContext.Add(__TestNamespace_InputTagHelper);
+ __TestNamespace_InputTagHelper2 = CreateTagHelper();
+ __tagHelperExecutionContext.Add(__TestNamespace_InputTagHelper2);
+ __TestNamespace_CatchAllTagHelper2 = CreateTagHelper();
+ __tagHelperExecutionContext.Add(__TestNamespace_CatchAllTagHelper2);
+ __TestNamespace_InputTagHelper.Type = "text";
+ __tagHelperExecutionContext.AddTagHelperAttribute("type", __TestNamespace_InputTagHelper.Type);
+ __TestNamespace_InputTagHelper2.Type = __TestNamespace_InputTagHelper.Type;
+ __tagHelperExecutionContext.AddHtmlAttribute(__tagHelperAttribute_4);
+ __tagHelperExecutionContext.Output = await __tagHelperRunner.RunAsync(__tagHelperExecutionContext);
+ Instrumentation.BeginContext(354, 42, false);
+ Write(__tagHelperExecutionContext.Output);
+ Instrumentation.EndContext();
+ __tagHelperExecutionContext = __tagHelperScopeManager.End();
+ Instrumentation.BeginContext(396, 2, true);
+ WriteLiteral("\r\n");
+ Instrumentation.EndContext();
+ __tagHelperExecutionContext = __tagHelperScopeManager.Begin("input", global::Microsoft.AspNetCore.Razor.TagHelpers.TagMode.SelfClosing, "test", async() => {
+ }
+ , StartTagHelperWritingScope, EndTagHelperWritingScope);
+ __TestNamespace_InputTagHelper2 = CreateTagHelper();
+ __tagHelperExecutionContext.Add(__TestNamespace_InputTagHelper2);
+ __TestNamespace_CatchAllTagHelper2 = CreateTagHelper();
+ __tagHelperExecutionContext.Add(__TestNamespace_CatchAllTagHelper2);
+ __TestNamespace_InputTagHelper2.Type = "texty";
+ __tagHelperExecutionContext.AddTagHelperAttribute("type", __TestNamespace_InputTagHelper2.Type);
+ __tagHelperExecutionContext.AddHtmlAttribute(__tagHelperAttribute_4);
+ __tagHelperExecutionContext.Output = await __tagHelperRunner.RunAsync(__tagHelperExecutionContext);
+ Instrumentation.BeginContext(398, 43, false);
+ Write(__tagHelperExecutionContext.Output);
+ Instrumentation.EndContext();
+ __tagHelperExecutionContext = __tagHelperScopeManager.End();
+ Instrumentation.BeginContext(441, 2, true);
+ WriteLiteral("\r\n");
+ Instrumentation.EndContext();
+ __tagHelperExecutionContext = __tagHelperScopeManager.Begin("input", global::Microsoft.AspNetCore.Razor.TagHelpers.TagMode.SelfClosing, "test", async() => {
+ }
+ , StartTagHelperWritingScope, EndTagHelperWritingScope);
+ __TestNamespace_InputTagHelper2 = CreateTagHelper();
+ __tagHelperExecutionContext.Add(__TestNamespace_InputTagHelper2);
+ __TestNamespace_CatchAllTagHelper2 = CreateTagHelper();
+ __tagHelperExecutionContext.Add(__TestNamespace_CatchAllTagHelper2);
+ __TestNamespace_InputTagHelper2.Type = "checkbox";
+ __tagHelperExecutionContext.AddTagHelperAttribute("type", __TestNamespace_InputTagHelper2.Type);
+ __tagHelperExecutionContext.AddHtmlAttribute(__tagHelperAttribute_5);
+ __tagHelperExecutionContext.Output = await __tagHelperRunner.RunAsync(__tagHelperExecutionContext);
+ Instrumentation.BeginContext(443, 45, false);
+ Write(__tagHelperExecutionContext.Output);
+ Instrumentation.EndContext();
+ __tagHelperExecutionContext = __tagHelperScopeManager.End();
+ }
+ #pragma warning restore 1998
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Razor.Test/TestFiles/CodeGenerator/Source/CssSelectorTagHelperAttributes.cshtml b/test/Microsoft.AspNetCore.Razor.Test/TestFiles/CodeGenerator/Source/CssSelectorTagHelperAttributes.cshtml
new file mode 100644
index 0000000000..9b83d5f7fb
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Razor.Test/TestFiles/CodeGenerator/Source/CssSelectorTagHelperAttributes.cshtml
@@ -0,0 +1,13 @@
+@addTagHelper "*, something"
+
+2 TagHelpers.
+1 TagHelper.
+2 TagHelpers
+2 TagHelpers
+0 TagHelpers.
+1 TagHelper
+1 TagHelper
+1 TagHelper
+
+
+
\ No newline at end of file