From 5f404977fea4f443fd4cc286c0efe12d33c4d8b2 Mon Sep 17 00:00:00 2001 From: "N. Taylor Mullen" Date: Sat, 13 Feb 2016 13:59:51 -0800 Subject: [PATCH] Add CSS attribute selectors for `TagHelper` attributes. - Added the ability for users to opt into CSS `TagHelper` selectors in their required attributes by surrounding the value with `[` and `]`. Added operators `^`, `$` and `=`. - Added tests to cover code paths used when determining CSS selectors. #684 --- .../TagHelpers/TagHelperDescriptorFactory.cs | 384 ++++++++++++++++-- ...aseSensitiveTagHelperDescriptorComparer.cs | 10 +- ...lperRequiredAttributeDescriptorComparer.cs | 44 ++ .../TagHelpers/TagHelperDescriptor.cs | 4 +- .../TagHelpers/TagHelperDescriptorComparer.cs | 10 +- .../TagHelpers/TagHelperDescriptorProvider.cs | 44 +- .../TagHelperRequiredAttributeDescriptor.cs | 93 +++++ ...lperRequiredAttributeDescriptorComparer.cs | 55 +++ .../Parser/RazorParser.cs | 2 +- .../TagHelpers/TagHelperParseTreeRewriter.cs | 134 +++++- .../TagHelperDescriptorFactoryTest.cs | 207 ++++++++-- .../CSharpTagHelperRenderingTest.cs | 140 ++++++- .../TagHelpers/TagHelperBlockRewriterTest.cs | 17 +- .../TagHelperDescriptorProviderTest.cs | 98 +++-- .../TagHelpers/TagHelperDescriptorTest.cs | 46 ++- .../TagHelperParseTreeRewriterTest.cs | 81 +++- ...agHelperRequiredAttributeDescriptorTest.cs | 173 ++++++++ .../Output/CSSSelectorTagHelperAttributes.cs | 259 ++++++++++++ .../CSSSelectorTagHelperAttributes.cshtml | 13 + 19 files changed, 1603 insertions(+), 211 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Razor.Test.Sources/CaseSensitiveTagHelperRequiredAttributeDescriptorComparer.cs create mode 100644 src/Microsoft.AspNetCore.Razor/Compilation/TagHelpers/TagHelperRequiredAttributeDescriptor.cs create mode 100644 src/Microsoft.AspNetCore.Razor/Compilation/TagHelpers/TagHelperRequiredAttributeDescriptorComparer.cs create mode 100644 test/Microsoft.AspNetCore.Razor.Test/TagHelpers/TagHelperRequiredAttributeDescriptorTest.cs create mode 100644 test/Microsoft.AspNetCore.Razor.Test/TestFiles/CodeGenerator/Output/CSSSelectorTagHelperAttributes.cs create mode 100644 test/Microsoft.AspNetCore.Razor.Test/TestFiles/CodeGenerator/Source/CSSSelectorTagHelperAttributes.cshtml diff --git a/src/Microsoft.AspNetCore.Razor.Runtime/Runtime/TagHelpers/TagHelperDescriptorFactory.cs b/src/Microsoft.AspNetCore.Razor.Runtime/Runtime/TagHelpers/TagHelperDescriptorFactory.cs index 92da7f95e..d8a1994eb 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 @@ public class TagHelperDescriptorFactory 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 @@ private IEnumerable BuildTagHelperDescriptors( typeName, assemblyName, attributeDescriptors, - requiredAttributes: Enumerable.Empty(), + requiredAttributeDescriptors: Enumerable.Empty(), allowedChildren: allowedChildren, tagStructure: default(TagStructure), parentTag: null, @@ -235,14 +237,15 @@ private static TagHelperDescriptor BuildTagHelperDescriptor( 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 @@ private static TagHelperDescriptor BuildTagHelperDescriptor( string typeName, string assemblyName, IEnumerable attributeDescriptors, - IEnumerable requiredAttributes, + IEnumerable requiredAttributeDescriptors, IEnumerable allowedChildren, string parentTag, TagStructure tagStructure, @@ -266,7 +269,7 @@ private static TagHelperDescriptor BuildTagHelperDescriptor( TypeName = typeName, AssemblyName = assemblyName, Attributes = attributeDescriptors, - RequiredAttributes = requiredAttributes, + RequiredAttributes = requiredAttributeDescriptors, AllowedChildren = allowedChildren, RequiredParent = parentTag, TagStructure = tagStructure, @@ -274,15 +277,6 @@ private static TagHelperDescriptor BuildTagHelperDescriptor( }; } - /// - /// 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 @@ internal static bool ValidHtmlTargetElementAttributeNames( 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 @@ internal static bool ValidateParentTagName(string parentTag, ErrorSink errorSink 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 @@ private static bool ValidateName( // '*' 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,328 @@ private static string ToHtmlCase(string name) { return HtmlCaseRegex.Replace(name, HtmlCaseRegexReplacement).ToLowerInvariant(); } + + // Internal for testing + internal class RequiredAttributeParser + { + 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 (_requiredAttributes == null) + { + requiredAttributeDescriptors = Enumerable.Empty(); + return true; + } + + requiredAttributeDescriptors = null; + var descriptors = new List(); + + while (!AtEnd) + { + PassOptionalWhitespace(); + + 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(',')) + { + Next(); + PassOptionalWhitespace(); + } + else if (!AtEnd) + { + errorSink.OnError(SourceLocation.Zero, $"TODO: Unknown character '{Current}'. Separate required attributes with commas.", 0); + return false; + } + } + + requiredAttributeDescriptors = descriptors; + return true; + } + + private TagHelperRequiredAttributeDescriptor ParsePlainSelector(ErrorSink errorSink) + { + var nextSeparator = _requiredAttributes.IndexOf(',', _index); + string attributeName; + + if (nextSeparator == -1) + { + attributeName = _requiredAttributes.Substring(_index).Trim(); + _index = _requiredAttributes.Length; + } + else + { + attributeName = _requiredAttributes.Substring(_index, nextSeparator - _index).Trim(); + _index = nextSeparator; + } + + var hasWildcard = false; + if (attributeName[attributeName.Length - 1] == RequiredAttributeWildcardSuffix) + { + attributeName = attributeName.Substring(0, attributeName.Length - 1); + hasWildcard = true; + } + + TagHelperRequiredAttributeDescriptor descriptor = null; + if (ValidateName(attributeName, targetingAttributes: true, errorSink: errorSink)) + { + descriptor = new TagHelperRequiredAttributeDescriptor + { + Name = attributeName + }; + + if (hasWildcard) + { + descriptor.Operator = RequiredAttributeWildcardSuffix; + } + } + + return descriptor; + } + + private string ParseCSSAttributeName(ErrorSink errorSink) + { + var nameStartIndex = _index; + while (!AtEnd && !char.IsWhiteSpace(Current) && !At('=') && !At(']')) + { + Next(); + } + var nameEndIndex = _index; + + PassOptionalWhitespace(); + + var attributeNameLength = 0; + if (At('=')) + { + attributeNameLength = nameEndIndex - nameStartIndex; + + // If this proves true it means there was 0 whitespace between the attribute name and the '='. + // Therefore, the operator would be baked into the attribute name by default. + var potentialOperator = _requiredAttributes[_index - 1]; + if (TagHelperRequiredAttributeDescriptor.IsSupportedCSSValueOperator(potentialOperator)) + { + attributeNameLength--; + } + } + else if (At(']')) + { + attributeNameLength = nameEndIndex - nameStartIndex; + } + else if (NextIs('=') && TagHelperRequiredAttributeDescriptor.IsSupportedCSSValueOperator(Current)) + { + // Move past selector + Next(); + attributeNameLength = nameEndIndex - nameStartIndex; + } + else + { + errorSink.OnError(SourceLocation.Zero, "TODO: Create error for unknown character not matching a square braces.", 0); + return null; + } + + var attributeName = _requiredAttributes.Substring(nameStartIndex, attributeNameLength); + + return attributeName; + } + + private char ParseCSSValueOperator(ErrorSink errorSink) + { + Debug.Assert(Current == '='); + + // Move past the '=' + Next(); + var potentialOperator = _requiredAttributes[_index - 2]; + if (TagHelperRequiredAttributeDescriptor.IsSupportedCSSValueOperator(potentialOperator)) + { + return potentialOperator; + } + + return '='; + } + + private string ParseCSSValue(ErrorSink errorSink) + { + PassOptionalWhitespace(); + + int valueStart, valueEnd; + if (At('\'') || At('"')) + { + var quote = Current; + + // Move past the quote + Next(); + + valueStart = _index; + while (!At(quote)) + { + if (AtEnd) + { + errorSink.OnError(SourceLocation.Zero, "TODO: Mismatching quotes", 0); + return null; + } + + Next(); + } + valueEnd = _index; + + // Move past the end quote; + Next(); + } + else + { + valueStart = _index; + while (!AtEnd && !char.IsWhiteSpace(Current) && !At(']')) + { + Next(); + } + valueEnd = _index; + } + + PassOptionalWhitespace(); + + var value = _requiredAttributes.Substring(valueStart, valueEnd - valueStart); + + return value; + } + + private TagHelperRequiredAttributeDescriptor ParseCSSSelector(ErrorSink errorSink) + { + Debug.Assert(Current == '['); + + // Move past '['. + Next(); + PassOptionalWhitespace(); + + var attributeName = ParseCSSAttributeName(errorSink); + + if (attributeName == null) + { + // Couldn't parse attribute name. + return null; + } + + if (!ValidateName(attributeName, targetingAttributes: true, errorSink: errorSink)) + { + return null; + } + + if (!EnsureNotAtEnd(errorSink, ']')) + { + return null; + } + + var valueOperator = default(char); + if (At('=')) + { + valueOperator = ParseCSSValueOperator(errorSink); + } + + if (!EnsureNotAtEnd(errorSink, ']')) + { + return null; + } + + var value = ParseCSSValue(errorSink); + + if (value == null) + { + // Couldn't parse value + return null; + } + + if (At(']')) + { + // Move past the ending bracket. + Next(); + } + else + { + errorSink.OnError(SourceLocation.Zero, "TODO: Could not find matching ']' for required attribute.", 0); + return null; + } + + return new TagHelperRequiredAttributeDescriptor + { + Name = attributeName, + Value = value, + Operator = valueOperator, + IsCSSSelector = true, + }; + } + + private bool EnsureNotAtEnd(ErrorSink errorSink, char endCharacter) + { + if (AtEnd) + { + errorSink.OnError( + SourceLocation.Zero, + $"TODO: Invalid required attribute, never encountered an ending {endCharacter}.", + 0); + + return false; + } + + return true; + } + + private void Next() + { + _index = Math.Min(_index + 1, _requiredAttributes.Length); + } + + private bool NextIs(char c) + { + _index++; + var nextIs = At(c); + _index--; + + return nextIs; + } + + private bool At(char c) + { + return !AtEnd && Current == c; + } + + private void PassOptionalWhitespace() + { + while (!AtEnd && char.IsWhiteSpace(Current)) + { + Next(); + } + } + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Razor.Test.Sources/CaseSensitiveTagHelperDescriptorComparer.cs b/src/Microsoft.AspNetCore.Razor.Test.Sources/CaseSensitiveTagHelperDescriptorComparer.cs index a412f7197..c1ab1e3cc 100644 --- a/src/Microsoft.AspNetCore.Razor.Test.Sources/CaseSensitiveTagHelperDescriptorComparer.cs +++ b/src/Microsoft.AspNetCore.Razor.Test.Sources/CaseSensitiveTagHelperDescriptorComparer.cs @@ -33,7 +33,10 @@ public override bool Equals(TagHelperDescriptor descriptorX, TagHelperDescriptor // 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 @@ public override int GetHashCode(TagHelperDescriptor descriptor) 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 000000000..64459d9c0 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Test.Sources/CaseSensitiveTagHelperRequiredAttributeDescriptorComparer.cs @@ -0,0 +1,44 @@ +// 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 ef7bf64b8..77f5fa848 100644 --- a/src/Microsoft.AspNetCore.Razor/Compilation/TagHelpers/TagHelperDescriptor.cs +++ b/src/Microsoft.AspNetCore.Razor/Compilation/TagHelpers/TagHelperDescriptor.cs @@ -19,7 +19,7 @@ public class TagHelperDescriptor 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 @@ public IEnumerable Attributes /// /// * 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 1497e440e..560ebf32b 100644 --- a/src/Microsoft.AspNetCore.Razor/Compilation/TagHelpers/TagHelperDescriptorComparer.cs +++ b/src/Microsoft.AspNetCore.Razor/Compilation/TagHelpers/TagHelperDescriptorComparer.cs @@ -51,9 +51,9 @@ public virtual bool Equals(TagHelperDescriptor descriptorX, TagHelperDescriptor 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 @@ public virtual int GetHashCode(TagHelperDescriptor descriptor) 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 d034da740..c31f60479 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 @@ public class TagHelperDescriptorProvider { public const string ElementCatchAllTarget = "*"; - public static readonly string RequiredAttributeWildcardSuffix = "*"; - private IDictionary> _registrations; private string _tagHelperPrefix; @@ -39,14 +38,14 @@ public TagHelperDescriptorProvider(IEnumerable descriptors) /// /// 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 @@ public IEnumerable GetDescriptors( 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 @@ private IEnumerable ApplyParentTagFilter( 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.Matches(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 000000000..00e12ac8f --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor/Compilation/TagHelpers/TagHelperRequiredAttributeDescriptor.cs @@ -0,0 +1,93 @@ +// 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; + +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 HTML attribute selector value. If is not true, this field is + /// ignored. + /// + public string Value { get; set; } + + /// + /// An operator that modifies how a required attribute is applied to an HTML attribute value or name. + /// + public char Operator { get; set; } + + /// + /// Indicates if the represents a CSS selector. + /// + public bool IsCSSSelector { get; set; } + + /// + /// Determines if the current matches the given + /// and . + /// + /// An HTML attribute name. + /// An HTML attribute value. + /// + public bool Matches(string attributeName, string attributeValue) + { + if (IsCSSSelector) + { + var nameMatches = string.Equals(Name, attributeName, StringComparison.OrdinalIgnoreCase); + + if (!nameMatches) + { + return false; + } + + var valueMatches = false; + switch (Operator) + { + case '^': // Value starts with + valueMatches = attributeValue.StartsWith(Value, StringComparison.Ordinal); + break; + case '$': // Value ends with + valueMatches = attributeValue.EndsWith(Value, StringComparison.Ordinal); + break; + case '=': // Value equals + valueMatches = string.Equals(attributeValue, Value, StringComparison.Ordinal); + break; + default: // No value selector, force true because at least the attribute name matched. + valueMatches = true; + break; + } + + return valueMatches; + } + else if (Operator == '*') + { + return attributeName.Length != Name.Length & + attributeName.StartsWith(Name, StringComparison.OrdinalIgnoreCase); + } + else + { + return string.Equals(Name, attributeName, StringComparison.OrdinalIgnoreCase); + } + } + + /// + /// Determines whether the provided is a supported CSS value operator. + /// + /// The CSS value operator + /// true if is =, ^ or $; false otherwise. + /// + public static bool IsSupportedCSSValueOperator(char op) + { + return op == '=' || op == '^' || op == '$'; + } + } +} 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 000000000..82d2559a9 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor/Compilation/TagHelpers/TagHelperRequiredAttributeDescriptorComparer.cs @@ -0,0 +1,55 @@ +// 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.Operator == descriptorY.Operator && + descriptorX.IsCSSSelector == descriptorY.IsCSSSelector && + 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.Operator); + hashCodeCombiner.Add(descriptor.IsCSSSelector); + hashCodeCombiner.Add(descriptor.Name, StringComparer.OrdinalIgnoreCase); + hashCodeCombiner.Add(descriptor.Value, StringComparer.Ordinal); + + return hashCodeCombiner.CombinedHash; + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor/Parser/RazorParser.cs b/src/Microsoft.AspNetCore.Razor/Parser/RazorParser.cs index dbb48d726..4c0fb84cd 100644 --- a/src/Microsoft.AspNetCore.Razor/Parser/RazorParser.cs +++ b/src/Microsoft.AspNetCore.Razor/Parser/RazorParser.cs @@ -239,7 +239,7 @@ protected virtual IEnumerable GetTagHelperDescriptors(Block return addOrRemoveTagHelperSpanVisitor.GetDescriptors(documentRoot); } - private static IEnumerable GetDefaultRewriters(ParserBase markupParser) + 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 eaed0a61f..d1753f414 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.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 @@ public class TagHelperParseTreeRewriter : ISyntaxTreeRewriter "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 @@ public TagHelperParseTreeRewriter(TagHelperDescriptorProvider provider) _provider = provider; _trackerStack = new Stack(); _blockStack = new Stack(); + _attributeValueBuilder = new StringBuilder(); + _htmlAttributeTracker = new List>(); } public void Rewrite(RewritingContext context) @@ -177,7 +186,7 @@ private bool TryRewriteTagHelper(Block tagBlock, RewritingContext context) 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 @@ private bool TryRewriteTagHelper(Block tagBlock, RewritingContext context) { 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 @@ private bool TryRewriteTagHelper(Block tagBlock, RewritingContext context) 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,111 @@ private IEnumerable GetAttributeNames(Block tagBlock) if (childCount <= 1) { - return Enumerable.Empty(); + return Enumerable.Empty>(); } - var attributeChildren = new List(childCount - 1); - for (var i = 1; i < childCount; i++) - { - attributeChildren.Add(tagBlock.Children[i]); - } - var attributeNames = new List(); + _htmlAttributeTracker.Clear(); + + var attributes = _htmlAttributeTracker; - foreach (var child in attributeChildren) + for (var i = 1; i < childCount; i++) { + 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; } + + 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; + + 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 +433,8 @@ private IEnumerable GetAttributeNames(Block tagBlock) } } - 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 +442,15 @@ private IEnumerable GetAttributeNames(Block tagBlock) } } - 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 +744,7 @@ private static string GetTagName(Block tagBlock) { 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 3ad660a86..f21a9b74a 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,89 @@ public class TagHelperDescriptorFactoryTest protected static readonly string AssemblyName = TagHelperDescriptorFactoryTestAssembly.Name; + public static TheoryData RequiredAttributeParserData + { + get + { + Func plain = + (name, op) => new TagHelperRequiredAttributeDescriptor { Name = name, Operator = op }; + Func css = + (name, value, op) => new TagHelperRequiredAttributeDescriptor + { + Name = name, + Value = value, + Operator = op, + IsCSSSelector = true + }; + + return new TheoryData> + { + { null, Enumerable.Empty() }, + { "name", new[] { plain("name", '\0') } }, + { "name-*", new[] { plain("name-", '*') } }, + { " name-* ", new[] { plain("name-", '*') } }, + { + "asp-route-*,valid , name-* ,extra", + new[] + { + plain("asp-route-", '*'), + plain("valid", '\0'), + plain("name-", '*'), + plain("extra", '\0'), + } + }, + { "[name]", new[] { css("name", "", '\0') } }, + { "[ name ]", new[] { css("name", "", '\0') } }, + { " [ name ] ", new[] { css("name", "", '\0') } }, + { "[name=]", new[] { css("name", "", '=') } }, + { "[name ^=]", new[] { css("name", "", '^') } }, + { "[name=hello]", new[] { css("name", "hello", '=') } }, + { "[name= hello]", new[] { css("name", "hello", '=') } }, + { "[name='hello']", new[] { css("name", "hello", '=') } }, + { "[name=\"hello\"]", new[] { css("name", "hello", '=') } }, + { " [ name $= \" hello\" ] ", new[] { css("name", " hello", '$') } }, + { + "[name=\"hello\"],[other^=something ], [val = 'cool']", + new[] { css("name", "hello", '='), css("other", "something", '^'), css("val", "cool", '=') } + }, + { + "asp-route-*,[name=\"hello\"],valid ,[other^=something ], name-* ,[val = 'cool'],extra", + new[] + { + plain("asp-route-", '*'), + css("name", "hello", '='), + plain("valid", '\0'), + css("other", "something", '^'), + plain("name-", '*'), + css("val", "cool", '='), + plain("extra", '\0'), + } + }, + }; + } + } + + [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 +700,10 @@ public static TheoryData AttributeTargetData typeof(AttributeTargetingTagHelper).FullName, AssemblyName, attributes, - requiredAttributes: new[] { "class" }) + requiredAttributes: new[] + { + new TagHelperRequiredAttributeDescriptor { Name = "class" } + }) } }, { @@ -629,7 +715,11 @@ public static TheoryData AttributeTargetData typeof(MultiAttributeTargetingTagHelper).FullName, AssemblyName, attributes, - requiredAttributes: new[] { "class", "style" }) + requiredAttributes: new[] + { + new TagHelperRequiredAttributeDescriptor { Name = "class" }, + new TagHelperRequiredAttributeDescriptor { Name = "style" } + }) } }, { @@ -641,13 +731,20 @@ public static TheoryData AttributeTargetData 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 +756,10 @@ public static TheoryData AttributeTargetData typeof(InheritedAttributeTargetingTagHelper).FullName, AssemblyName, attributes, - requiredAttributes: new[] { "style" }) + requiredAttributes: new[] + { + new TagHelperRequiredAttributeDescriptor { Name = "style" } + }) } }, { @@ -671,7 +771,10 @@ public static TheoryData AttributeTargetData typeof(RequiredAttributeTagHelper).FullName, AssemblyName, attributes, - requiredAttributes: new[] { "class" }) + requiredAttributes: new[] + { + new TagHelperRequiredAttributeDescriptor { Name = "class" } + }) } }, { @@ -683,7 +786,10 @@ public static TheoryData AttributeTargetData typeof(InheritedRequiredAttributeTagHelper).FullName, AssemblyName, attributes, - requiredAttributes: new[] { "class" }) + requiredAttributes: new[] + { + new TagHelperRequiredAttributeDescriptor { Name = "class" } + }) } }, { @@ -695,13 +801,19 @@ public static TheoryData AttributeTargetData 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 +825,19 @@ public static TheoryData AttributeTargetData 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 +849,11 @@ public static TheoryData AttributeTargetData typeof(MultiRequiredAttributeTagHelper).FullName, AssemblyName, attributes, - requiredAttributes: new[] { "class", "style" }) + requiredAttributes: new[] + { + new TagHelperRequiredAttributeDescriptor { Name = "class" }, + new TagHelperRequiredAttributeDescriptor { Name = "style" } + }) } }, { @@ -743,13 +865,20 @@ public static TheoryData AttributeTargetData 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 +890,14 @@ public static TheoryData AttributeTargetData typeof(AttributeWildcardTargetingTagHelper).FullName, AssemblyName, attributes, - requiredAttributes: new[] { "class*" }) + requiredAttributes: new[] + { + new TagHelperRequiredAttributeDescriptor + { + Name = "class", + Operator = '*' + } + }) } }, { @@ -773,7 +909,19 @@ public static TheoryData AttributeTargetData typeof(MultiAttributeWildcardTargetingTagHelper).FullName, AssemblyName, attributes, - requiredAttributes: new[] { "class*", "style*" }) + requiredAttributes: new[] + { + new TagHelperRequiredAttributeDescriptor + { + Name = "class", + Operator = '*' + }, + new TagHelperRequiredAttributeDescriptor + { + Name = "style", + Operator = '*' + } + }) } }, }; @@ -1327,29 +1475,6 @@ public static TheoryData ValidNameData } } - [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 +2418,7 @@ protected static TagHelperDescriptor CreateTagHelperDescriptor( string typeName, string assemblyName, IEnumerable attributes = null, - IEnumerable requiredAttributes = null) + IEnumerable requiredAttributes = null) { return new TagHelperDescriptor { @@ -2301,7 +2426,7 @@ protected static TagHelperDescriptor CreateTagHelperDescriptor( 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 11adbd38b..21b901221 100644 --- a/test/Microsoft.AspNetCore.Razor.Test/CodeGenerators/CSharpTagHelperRenderingTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Test/CodeGenerators/CSharpTagHelperRenderingTest.cs @@ -21,6 +21,112 @@ public class CSharpTagHelperRenderingTest : TagHelperTestBase 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 + { + IsCSSSelector = true, + Name = "href", + Operator = '=', + Value = "~/", + } + }, + }, + new TagHelperDescriptor + { + TagName = "a", + TypeName = "TestNamespace.ATagHelperMultipleSelectors", + AssemblyName = "SomeAssembly", + RequiredAttributes = new[] + { + new TagHelperRequiredAttributeDescriptor + { + IsCSSSelector = true, + Name = "href", + Operator = '^', + Value = "~/", + }, + new TagHelperRequiredAttributeDescriptor + { + IsCSSSelector = true, + Name = "href", + Operator = '$', + Value = "?hello=world" + } + }, + }, + new TagHelperDescriptor + { + TagName = "input", + TypeName = "TestNamespace.InputTagHelper", + AssemblyName = "SomeAssembly", + Attributes = new TagHelperAttributeDescriptor[] + { + new TagHelperAttributeDescriptor("type", inputTypePropertyInfo), + }, + RequiredAttributes = new[] + { + new TagHelperRequiredAttributeDescriptor + { + IsCSSSelector = true, + Name = "type", + Operator = '=', + Value = "text" + } + }, + }, + new TagHelperDescriptor + { + TagName = "input", + TypeName = "TestNamespace.InputTagHelper2", + AssemblyName = "SomeAssembly", + Attributes = new TagHelperAttributeDescriptor[] + { + new TagHelperAttributeDescriptor("type", inputTypePropertyInfo), + }, + RequiredAttributes = new[] + { + new TagHelperRequiredAttributeDescriptor + { + Name = "ty", + Operator = '*' + } + }, + }, + new TagHelperDescriptor + { + TagName = "*", + TypeName = "TestNamespace.CatchAllTagHelper", + AssemblyName = "SomeAssembly", + RequiredAttributes = new[] + { + new TagHelperRequiredAttributeDescriptor + { + IsCSSSelector = true, + Name = "href", + Operator = '^', + Value = "~/" + } + }, + } + }; + } + } + private static IEnumerable EnumTagHelperDescriptors { get @@ -113,7 +219,7 @@ private static IEnumerable SymbolBoundTagHelperDescriptors TypeName = typeof(string).FullName }, }, - RequiredAttributes = new[] { "bound" }, + RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "bound" } }, }, }; } @@ -140,7 +246,10 @@ private static IEnumerable MinimizedTagHelpers_Descriptors IsStringProperty = true } }, - RequiredAttributes = new[] { "catchall-unbound-required" }, + RequiredAttributes = new[] + { + new TagHelperRequiredAttributeDescriptor { Name = "catchall-unbound-required" } + }, }, new TagHelperDescriptor { @@ -164,7 +273,11 @@ private static IEnumerable MinimizedTagHelpers_Descriptors 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 +327,7 @@ private static IEnumerable DuplicateTargetTagHelperDescript new TagHelperAttributeDescriptor("type", inputTypePropertyInfo), new TagHelperAttributeDescriptor("checked", inputCheckedPropertyInfo) }, - RequiredAttributes = new[] { "type" }, + RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "type" } }, }, new TagHelperDescriptor { @@ -226,7 +339,7 @@ private static IEnumerable DuplicateTargetTagHelperDescript new TagHelperAttributeDescriptor("type", inputTypePropertyInfo), new TagHelperAttributeDescriptor("checked", inputCheckedPropertyInfo) }, - RequiredAttributes = new[] { "checked" }, + RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "checked" } }, }, new TagHelperDescriptor { @@ -238,7 +351,7 @@ private static IEnumerable DuplicateTargetTagHelperDescript new TagHelperAttributeDescriptor("type", inputTypePropertyInfo), new TagHelperAttributeDescriptor("checked", inputCheckedPropertyInfo) }, - RequiredAttributes = new[] { "type" }, + RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "type" } }, }, new TagHelperDescriptor { @@ -250,7 +363,7 @@ private static IEnumerable DuplicateTargetTagHelperDescript new TagHelperAttributeDescriptor("type", inputTypePropertyInfo), new TagHelperAttributeDescriptor("checked", inputCheckedPropertyInfo) }, - RequiredAttributes = new[] { "checked" }, + RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "checked" } }, } }; } @@ -269,7 +382,7 @@ private static IEnumerable AttributeTargetingTagHelperDescr TagName = "p", TypeName = "TestNamespace.PTagHelper", AssemblyName = "SomeAssembly", - RequiredAttributes = new[] { "class" }, + RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "class" } }, }, new TagHelperDescriptor { @@ -280,7 +393,7 @@ private static IEnumerable AttributeTargetingTagHelperDescr { new TagHelperAttributeDescriptor("type", inputTypePropertyInfo) }, - RequiredAttributes = new[] { "type" }, + RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "type" } }, }, new TagHelperDescriptor { @@ -292,14 +405,18 @@ private static IEnumerable AttributeTargetingTagHelperDescr 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 +1891,7 @@ public static TheoryData RuntimeTimeTagHelperTestData // 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 e3651ed66..974a36417 100644 --- a/test/Microsoft.AspNetCore.Razor.Test/TagHelpers/TagHelperBlockRewriterTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Test/TagHelpers/TagHelperBlockRewriterTest.cs @@ -157,7 +157,7 @@ public void Rewrite_CanHandleSymbolBoundAttributes(string documentContent, Marku TypeName = typeof(string).FullName }, }, - RequiredAttributes = new[] { "bound" }, + RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "bound" } }, }, }; var descriptorProvider = new TagHelperDescriptorProvider(descriptors); @@ -3940,7 +3940,10 @@ public void Rewrite_UnderstandsMinimizedAttributes( IsStringProperty = true } }, - RequiredAttributes = new[] { "unbound-required" } + RequiredAttributes = new[] + { + new TagHelperRequiredAttributeDescriptor { Name = "unbound-required" } + } }, new TagHelperDescriptor { @@ -3957,7 +3960,10 @@ public void Rewrite_UnderstandsMinimizedAttributes( IsStringProperty = true } }, - RequiredAttributes = new[] { "bound-required-string" } + RequiredAttributes = new[] + { + new TagHelperRequiredAttributeDescriptor { Name = "bound-required-string" } + } }, new TagHelperDescriptor { @@ -3973,7 +3979,10 @@ public void Rewrite_UnderstandsMinimizedAttributes( 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 a5faa405e..371b03ac9 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 @@ public static TheoryData RequiredParentData [Theory] [MemberData(nameof(RequiredParentData))] - public void GetDescriptors_ReturnsDescriptorsWithRequiredAttributes( + public void GetDescriptors_ReturnsDescriptorsParentTags( string tagName, string parentTagName, IEnumerable availableDescriptors, @@ -85,7 +85,7 @@ public void GetDescriptors_ReturnsDescriptorsWithRequiredAttributes( // Act var resolvedDescriptors = provider.GetDescriptors( tagName, - attributeNames: Enumerable.Empty(), + attributes: Enumerable.Empty>(), parentTagName: parentTagName); // Assert @@ -101,131 +101,155 @@ public static TheoryData RequiredAttributeData 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", + Operator = '*' + } + } }; 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-", + Operator = '*', + } + } }; 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 @@ public static TheoryData RequiredAttributeData [MemberData(nameof(RequiredAttributeData))] public void GetDescriptors_ReturnsDescriptorsWithRequiredAttributes( string tagName, - IEnumerable providedAttributes, + IEnumerable> providedAttributes, IEnumerable availableDescriptors, IEnumerable expectedDescriptors) { @@ -265,7 +289,7 @@ public void GetDescriptors_ReturnsEmptyDescriptorsWithPrefixAsTagName() // Act var resolvedDescriptors = provider.GetDescriptors( tagName: "th", - attributeNames: Enumerable.Empty(), + attributes: Enumerable.Empty>(), parentTagName: "p"); // Assert @@ -284,11 +308,11 @@ public void GetDescriptors_OnlyUnderstandsSinglePrefix() // 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 @@ public void GetDescriptors_ReturnsCatchAllDescriptorsForPrefixedTags() // 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 @@ public void GetDescriptors_ReturnsDescriptorsForPrefixedTags() // Act var retrievedDescriptors = provider.GetDescriptors( tagName: "th:div", - attributeNames: Enumerable.Empty(), + attributes: Enumerable.Empty>(), parentTagName: "p"); // Assert @@ -354,7 +378,7 @@ public void GetDescriptors_ReturnsNothingForUnprefixedTags(string tagName) // Act var retrievedDescriptorsDiv = provider.GetDescriptors( tagName: "div", - attributeNames: Enumerable.Empty(), + attributes: Enumerable.Empty>(), parentTagName: "p"); // Assert @@ -383,7 +407,7 @@ public void GetDescriptors_ReturnsNothingForUnregisteredTags() // Act var retrievedDescriptors = provider.GetDescriptors( tagName: "foo", - attributeNames: Enumerable.Empty(), + attributes: Enumerable.Empty>(), parentTagName: "p"); // Assert @@ -418,11 +442,11 @@ public void GetDescriptors_ReturnsCatchAllsWithEveryTagName() // 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 @@ public void GetDescriptors_DuplicateDescriptorsAreNotPartOfTagHelperDescriptorPo // 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 408090eab..b866660d4 100644 --- a/test/Microsoft.AspNetCore.Razor.Test/TagHelpers/TagHelperDescriptorTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Test/TagHelpers/TagHelperDescriptorTest.cs @@ -22,7 +22,17 @@ public void TagHelperDescriptor_CanBeSerialized() 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" }, + new TagHelperRequiredAttributeDescriptor + { + IsCSSSelector = true, + Name = "required attribute two", + Value = "something", + Operator = '^', + } + }, AllowedChildren = new[] { "allowed child one" }, RequiredParent = "parent name", DesignTimeDescriptor = new TagHelperDesignTimeDescriptor @@ -41,7 +51,14 @@ public void TagHelperDescriptor_CanBeSerialized() $"\"{ 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.Value) }\":null," + + $"\"{ nameof(TagHelperRequiredAttributeDescriptor.Operator) }\":\"\\u0000\"," + + $"\"{ nameof(TagHelperRequiredAttributeDescriptor.IsCSSSelector) }\":false}}," + + $"{{\"{ nameof(TagHelperRequiredAttributeDescriptor.Name)}\":\"required attribute two\"," + + $"\"{ nameof(TagHelperRequiredAttributeDescriptor.Value) }\":\"something\"," + + $"\"{ nameof(TagHelperRequiredAttributeDescriptor.Operator) }\":\"^\"," + + $"\"{ nameof(TagHelperRequiredAttributeDescriptor.IsCSSSelector) }\":true}}]," + $"\"{ nameof(TagHelperDescriptor.AllowedChildren) }\":[\"allowed child one\"]," + $"\"{ nameof(TagHelperDescriptor.RequiredParent) }\":\"parent name\"," + $"\"{ nameof(TagHelperDescriptor.TagStructure) }\":0," + @@ -200,8 +217,15 @@ public void TagHelperDescriptor_CanBeDeserialized() $"\"{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.Value) }\":null," + + $"\"{ nameof(TagHelperRequiredAttributeDescriptor.Operator) }\":\"\\u0000\"," + + $"\"{ nameof(TagHelperRequiredAttributeDescriptor.IsCSSSelector) }\":false}}," + + $"{{\"{ nameof(TagHelperRequiredAttributeDescriptor.Name)}\":\"required attribute two\"," + + $"\"{ nameof(TagHelperRequiredAttributeDescriptor.Value) }\":\"something\"," + + $"\"{ nameof(TagHelperRequiredAttributeDescriptor.Operator) }\":\"^\"," + + $"\"{ nameof(TagHelperRequiredAttributeDescriptor.IsCSSSelector) }\":true}}]," + $"\"{ nameof(TagHelperDescriptor.AllowedChildren) }\":[\"allowed child one\",\"allowed child two\"]," + $"\"{ nameof(TagHelperDescriptor.RequiredParent) }\":\"parent name\"," + $"\"{nameof(TagHelperDescriptor.TagStructure)}\":2," + @@ -215,7 +239,17 @@ public void TagHelperDescriptor_CanBeDeserialized() 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" }, + new TagHelperRequiredAttributeDescriptor + { + IsCSSSelector = true, + Name = "required attribute two", + Value = "something", + Operator = '^', + } + }, AllowedChildren = new[] { "allowed child one", "allowed child two" }, RequiredParent = "parent name", DesignTimeDescriptor = new TagHelperDesignTimeDescriptor @@ -237,7 +271,7 @@ public void TagHelperDescriptor_CanBeDeserialized() 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 eea4e14d5..f9586621c 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.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 @@ public void Rewrite_RecoversWhenRequiredAttributeMismatchAndRestrictedChildren() TagName = "strong", TypeName = "StrongTagHelper", AssemblyName = "SomeAssembly", - RequiredAttributes = new[] { "required" }, + RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "required" } }, AllowedChildren = new[] { "br" } } }; @@ -1648,21 +1711,25 @@ public void Rewrite_RequiredAttributeDescriptorsCreateTagHelperBlocksCorrectly( 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 @@ public void Rewrite_NestedRequiredAttributeDescriptorsCreateTagHelperBlocksCorre 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 @@ public void Rewrite_RequiredAttributeDescriptorsCreateMalformedTagHelperBlocksCo 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 000000000..8c138fb2b --- /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-", + Operator = '*' + }, + "ROUTE-area", + "manage", + true + }, + { + new TagHelperRequiredAttributeDescriptor + { + Name = "route-", + Operator = '*' + }, + "routearea", + "manage", + false + }, + { + new TagHelperRequiredAttributeDescriptor + { + Name = "route-", + Operator = '*' + }, + "route-", + "manage", + false + }, + { + new TagHelperRequiredAttributeDescriptor + { + Name = "key", + IsCSSSelector = true, + }, + "KeY", + "value", + true + }, + { + new TagHelperRequiredAttributeDescriptor + { + Name = "key", + IsCSSSelector = true, + }, + "keys", + "value", + false + }, + { + new TagHelperRequiredAttributeDescriptor + { + Name = "key", + Value = "value", + Operator = '=', + IsCSSSelector = true, + }, + "key", + "value", + true + }, + { + new TagHelperRequiredAttributeDescriptor + { + Name = "key", + Value = "value", + Operator = '=', + IsCSSSelector = true, + }, + "key", + "Value", + false + }, + { + new TagHelperRequiredAttributeDescriptor + { + Name = "class", + Value = "btn", + Operator = '^', + IsCSSSelector = true, + }, + "class", + "btn btn-success", + true + }, + { + new TagHelperRequiredAttributeDescriptor + { + Name = "class", + Value = "btn", + Operator = '^', + IsCSSSelector = true, + }, + "class", + "BTN btn-success", + false + }, + { + new TagHelperRequiredAttributeDescriptor + { + Name = "href", + Value = "#navigate", + Operator = '$', + IsCSSSelector = true, + }, + "href", + "/home/index#navigate", + true + }, + { + new TagHelperRequiredAttributeDescriptor + { + Name = "href", + Value = "#navigate", + Operator = '$', + IsCSSSelector = true, + }, + "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.Matches(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 000000000..ade78d581 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Test/TestFiles/CodeGenerator/Output/CSSSelectorTagHelperAttributes.cs @@ -0,0 +1,259 @@ +#pragma checksum "CSSSelectorTagHelperAttributes.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "a6f31278c7c6eb906e9aa7ab2d616fd3de92d846" +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 global::TestNamespace.ATagHelperMultipleSelectors __TestNamespace_ATagHelperMultipleSelectors = null; + private global::TestNamespace.InputTagHelper __TestNamespace_InputTagHelper = null; + private global::TestNamespace.InputTagHelper2 __TestNamespace_InputTagHelper2 = null; + #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("href", Html.Raw("~/")); + __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("href", Html.Raw("~/hello")); + __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("href", Html.Raw("~/?hello=world")); + __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("href", Html.Raw("~/?hello=world@false")); + __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_InputTagHelper.Type = "text"; + __tagHelperExecutionContext.AddTagHelperAttribute("type", __TestNamespace_InputTagHelper.Type); + __TestNamespace_InputTagHelper2.Type = __TestNamespace_InputTagHelper.Type; + __tagHelperExecutionContext.AddHtmlAttribute("value", Html.Raw("2 TagHelpers")); + __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_InputTagHelper2.Type = "texty"; + __tagHelperExecutionContext.AddTagHelperAttribute("type", __TestNamespace_InputTagHelper2.Type); + __tagHelperExecutionContext.AddHtmlAttribute("value", Html.Raw("2 TagHelpers")); + __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_InputTagHelper2.Type = "checkbox"; + __tagHelperExecutionContext.AddTagHelperAttribute("type", __TestNamespace_InputTagHelper2.Type); + __tagHelperExecutionContext.AddHtmlAttribute("value", Html.Raw("1 TagHelper")); + __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 000000000..1b35dad4b --- /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