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