private static TryParseResult TryParseBlock(
            string tagName,
            Block block,
            IEnumerable <TagHelperDescriptor> descriptors,
            ErrorSink errorSink,
            HashSet <string> processedBoundAttributeNames)
        {
            // TODO: Accept more than just spans: https://github.com/aspnet/Razor/issues/96.
            // The first child will only ever NOT be a Span if a user is doing something like:
            // <input @checked />

            var childSpan = block.Children.First() as Span;

            if (childSpan == null || childSpan.Kind != SpanKindInternal.Markup)
            {
                var location   = new SourceSpan(block.Start, block.Length);
                var diagnostic = RazorDiagnosticFactory.CreateParsing_TagHelpersCannotHaveCSharpInTagDeclaration(location, tagName);
                errorSink.OnError(diagnostic);

                return(null);
            }

            var builder = new BlockBuilder(block);

            // If there's only 1 child it means that it's plain text inside of the attribute.
            // i.e. <div class="plain text in attribute">
            if (builder.Children.Count == 1)
            {
                return(TryParseSpan(childSpan, descriptors, errorSink, processedBoundAttributeNames));
            }

            var nameSymbols = childSpan
                              .Symbols
                              .OfType <HtmlSymbol>()
                              .SkipWhile(symbol => !HtmlMarkupParser.IsValidAttributeNameSymbol(symbol)) // Skip prefix
                              .TakeWhile(nameSymbol => HtmlMarkupParser.IsValidAttributeNameSymbol(nameSymbol))
                              .Select(nameSymbol => nameSymbol.Content);

            var name = string.Concat(nameSymbols);

            if (string.IsNullOrEmpty(name))
            {
                var location   = new SourceSpan(childSpan.Start, childSpan.Length);
                var diagnostic = RazorDiagnosticFactory.CreateParsing_TagHelperAttributesMustHaveAName(location, tagName);
                errorSink.OnError(diagnostic);

                return(null);
            }

            // Have a name now. Able to determine correct isBoundNonStringAttribute value.
            var result = CreateTryParseResult(name, descriptors, processedBoundAttributeNames);

            var firstChild = builder.Children[0] as Span;

            if (firstChild != null && firstChild.Symbols[0] is HtmlSymbol)
            {
                var htmlSymbol = firstChild.Symbols[firstChild.Symbols.Count - 1] as HtmlSymbol;
                switch (htmlSymbol.Type)
                {
                case HtmlSymbolType.Equals:
                    if (builder.Children.Count == 2 &&
                        builder.Children[1] is Span value &&
                        value.Kind == SpanKindInternal.Markup)
                    {
                        // Attribute value is a string literal. Eg: <tag my-attribute=foo />.
                        result.AttributeStructure = AttributeStructure.NoQuotes;
                    }
                    else
                    {
                        // Could be an expression, treat NoQuotes and DoubleQuotes equivalently. We purposefully do not persist NoQuotes
                        // ValueStyles at code generation time to protect users from rendering dynamic content with spaces
                        // that can break attributes.
                        // Ex: <tag my-attribute=@value /> where @value results in the test "hello world".
                        // This way, the above code would render <tag my-attribute="hello world" />.
                        result.AttributeStructure = AttributeStructure.DoubleQuotes;
                    }
                    break;
Exemple #2
0
        private static SyntaxList <RazorSyntaxNode> GetRewrittenChildren(
            string tagName,
            bool validStructure,
            MarkupTagBlockSyntax tagBlock,
            TagHelperBinding bindingResult,
            RazorParserFeatureFlags featureFlags,
            ErrorSink errorSink,
            RazorSourceDocument source)
        {
            var tagHelperBuilder = SyntaxListBuilder <RazorSyntaxNode> .Create();

            var processedBoundAttributeNames = new HashSet <string>(StringComparer.OrdinalIgnoreCase);

            if (tagBlock.Children.Count == 1)
            {
                // Tag with no attributes. We have nothing to rewrite here.
                return(tagBlock.Children);
            }

            // Add the tag start
            tagHelperBuilder.Add(tagBlock.Children.First());

            // We skip the first child "<tagname" and take everything up to the ending portion of the tag ">" or "/>".
            // If the tag does not have a valid structure then there's no close angle to ignore.
            var tokenOffset = validStructure ? 1 : 0;

            for (var i = 1; i < tagBlock.Children.Count - tokenOffset; i++)
            {
                var            isMinimized           = false;
                var            attributeNameLocation = SourceLocation.Undefined;
                var            child = tagBlock.Children[i];
                TryParseResult result;
                if (child is MarkupAttributeBlockSyntax attributeBlock)
                {
                    attributeNameLocation = attributeBlock.Name.GetSourceLocation(source);
                    result = TryParseAttribute(
                        tagName,
                        attributeBlock,
                        bindingResult.Descriptors,
                        errorSink,
                        processedBoundAttributeNames);
                    tagHelperBuilder.Add(result.RewrittenAttribute);
                }
                else if (child is MarkupMinimizedAttributeBlockSyntax minimizedAttributeBlock)
                {
                    isMinimized           = true;
                    attributeNameLocation = minimizedAttributeBlock.Name.GetSourceLocation(source);
                    result = TryParseMinimizedAttribute(
                        tagName,
                        minimizedAttributeBlock,
                        bindingResult.Descriptors,
                        errorSink,
                        processedBoundAttributeNames);
                    tagHelperBuilder.Add(result.RewrittenAttribute);
                }
                else if (child is CSharpCodeBlockSyntax)
                {
                    // TODO: Accept more than just Markup attributes: https://github.com/aspnet/Razor/issues/96.
                    // Something like:
                    // <input @checked />
                    var location   = new SourceSpan(child.GetSourceLocation(source), child.FullWidth);
                    var diagnostic = RazorDiagnosticFactory.CreateParsing_TagHelpersCannotHaveCSharpInTagDeclaration(location, tagName);
                    errorSink.OnError(diagnostic);

                    result = null;
                }
                else if (child is MarkupTextLiteralSyntax)
                {
                    // If the original span content was whitespace it ultimately means the tag
                    // that owns this "attribute" is malformed and is expecting a user to type a new attribute.
                    // ex: <myTH class="btn"| |
                    var literalContent = child.GetContent();
                    if (!string.IsNullOrWhiteSpace(literalContent))
                    {
                        var location   = child.GetSourceSpan(source);
                        var diagnostic = RazorDiagnosticFactory.CreateParsing_TagHelperAttributeListMustBeWellFormed(location);
                        errorSink.OnError(diagnostic);
                    }
                    result = null;
                }
                else
                {
                    result = null;
                }

                // Only want to track the attribute if we succeeded in parsing its corresponding Block/Span.
                if (result == null)
                {
                    // Error occurred while parsing the attribute. Don't try parsing the rest to avoid misleading errors.
                    for (var j = i; j < tagBlock.Children.Count; j++)
                    {
                        tagHelperBuilder.Add(tagBlock.Children[j]);
                    }

                    return(tagHelperBuilder.ToList());
                }

                // Check if it's a non-boolean bound attribute that is minimized or if it's a bound
                // non-string attribute that has null or whitespace content.
                var isValidMinimizedAttribute = featureFlags.AllowMinimizedBooleanTagHelperAttributes && result.IsBoundBooleanAttribute;
                if ((isMinimized &&
                     result.IsBoundAttribute &&
                     !isValidMinimizedAttribute) ||
                    (!isMinimized &&
                     result.IsBoundNonStringAttribute &&
                     string.IsNullOrWhiteSpace(GetAttributeValueContent(result.RewrittenAttribute))))
                {
                    var errorLocation    = new SourceSpan(attributeNameLocation, result.AttributeName.Length);
                    var propertyTypeName = GetPropertyType(result.AttributeName, bindingResult.Descriptors);
                    var diagnostic       = RazorDiagnosticFactory.CreateTagHelper_EmptyBoundAttribute(errorLocation, result.AttributeName, tagName, propertyTypeName);
                    errorSink.OnError(diagnostic);
                }

                // Check if the attribute was a prefix match for a tag helper dictionary property but the
                // dictionary key would be the empty string.
                if (result.IsMissingDictionaryKey)
                {
                    var errorLocation = new SourceSpan(attributeNameLocation, result.AttributeName.Length);
                    var diagnostic    = RazorDiagnosticFactory.CreateParsing_TagHelperIndexerAttributeNameMustIncludeKey(errorLocation, result.AttributeName, tagName);
                    errorSink.OnError(diagnostic);
                }
            }

            if (validStructure)
            {
                // Add the tag end.
                tagHelperBuilder.Add(tagBlock.Children[tagBlock.Children.Count - 1]);
            }

            return(tagHelperBuilder.ToList());
        }
        public static MarkupTagHelperStartTagSyntax Rewrite(
            string tagName,
            RazorParserFeatureFlags featureFlags,
            MarkupStartTagSyntax startTag,
            TagHelperBinding bindingResult,
            ErrorSink errorSink,
            RazorSourceDocument source)
        {
            var processedBoundAttributeNames = new HashSet <string>(StringComparer.OrdinalIgnoreCase);

            var attributes       = startTag.Attributes;
            var attributeBuilder = SyntaxListBuilder <RazorSyntaxNode> .Create();

            for (var i = 0; i < startTag.Attributes.Count; i++)
            {
                var            isMinimized           = false;
                var            attributeNameLocation = SourceLocation.Undefined;
                var            child = startTag.Attributes[i];
                TryParseResult result;
                if (child is MarkupAttributeBlockSyntax attributeBlock)
                {
                    attributeNameLocation = attributeBlock.Name.GetSourceLocation(source);
                    result = TryParseAttribute(
                        tagName,
                        attributeBlock,
                        bindingResult.Descriptors,
                        errorSink,
                        processedBoundAttributeNames);
                    attributeBuilder.Add(result.RewrittenAttribute);
                }
                else if (child is MarkupMinimizedAttributeBlockSyntax minimizedAttributeBlock)
                {
                    isMinimized           = true;
                    attributeNameLocation = minimizedAttributeBlock.Name.GetSourceLocation(source);
                    result = TryParseMinimizedAttribute(
                        tagName,
                        minimizedAttributeBlock,
                        bindingResult.Descriptors,
                        errorSink,
                        processedBoundAttributeNames);
                    attributeBuilder.Add(result.RewrittenAttribute);
                }
                else if (child is MarkupMiscAttributeContentSyntax miscContent)
                {
                    foreach (var contentChild in miscContent.Children)
                    {
                        if (contentChild is CSharpCodeBlockSyntax codeBlock)
                        {
                            // TODO: Accept more than just Markup attributes: https://github.com/aspnet/Razor/issues/96.
                            // Something like:
                            // <input @checked />
                            var location   = new SourceSpan(codeBlock.GetSourceLocation(source), codeBlock.FullWidth);
                            var diagnostic = RazorDiagnosticFactory.CreateParsing_TagHelpersCannotHaveCSharpInTagDeclaration(location, tagName);
                            errorSink.OnError(diagnostic);
                            break;
                        }
                        else
                        {
                            // If the original span content was whitespace it ultimately means the tag
                            // that owns this "attribute" is malformed and is expecting a user to type a new attribute.
                            // ex: <myTH class="btn"| |
                            var literalContent = contentChild.GetContent();
                            if (!string.IsNullOrWhiteSpace(literalContent))
                            {
                                var location   = contentChild.GetSourceSpan(source);
                                var diagnostic = RazorDiagnosticFactory.CreateParsing_TagHelperAttributeListMustBeWellFormed(location);
                                errorSink.OnError(diagnostic);
                                break;
                            }
                        }
                    }

                    result = null;
                }
                else
                {
                    result = null;
                }

                // Only want to track the attribute if we succeeded in parsing its corresponding Block/Span.
                if (result == null)
                {
                    // Error occurred while parsing the attribute. Don't try parsing the rest to avoid misleading errors.
                    for (var j = i; j < startTag.Attributes.Count; j++)
                    {
                        attributeBuilder.Add(startTag.Attributes[j]);
                    }

                    break;
                }

                // Check if it's a non-boolean bound attribute that is minimized or if it's a bound
                // non-string attribute that has null or whitespace content.
                var isValidMinimizedAttribute = featureFlags.AllowMinimizedBooleanTagHelperAttributes && result.IsBoundBooleanAttribute;
                if ((isMinimized &&
                     result.IsBoundAttribute &&
                     !isValidMinimizedAttribute) ||
                    (!isMinimized &&
                     result.IsBoundNonStringAttribute &&
                     string.IsNullOrWhiteSpace(GetAttributeValueContent(result.RewrittenAttribute))))
                {
                    var errorLocation    = new SourceSpan(attributeNameLocation, result.AttributeName.Length);
                    var propertyTypeName = GetPropertyType(result.AttributeName, bindingResult.Descriptors);
                    var diagnostic       = RazorDiagnosticFactory.CreateTagHelper_EmptyBoundAttribute(errorLocation, result.AttributeName, tagName, propertyTypeName);
                    errorSink.OnError(diagnostic);
                }

                // Check if the attribute was a prefix match for a tag helper dictionary property but the
                // dictionary key would be the empty string.
                if (result.IsMissingDictionaryKey)
                {
                    var errorLocation = new SourceSpan(attributeNameLocation, result.AttributeName.Length);
                    var diagnostic    = RazorDiagnosticFactory.CreateParsing_TagHelperIndexerAttributeNameMustIncludeKey(errorLocation, result.AttributeName, tagName);
                    errorSink.OnError(diagnostic);
                }
            }

            if (attributeBuilder.Count > 0)
            {
                // This means we rewrote something. Use the new set of attributes.
                attributes = attributeBuilder.ToList();
            }

            var tagHelperStartTag = SyntaxFactory.MarkupTagHelperStartTag(
                startTag.OpenAngle, startTag.Bang, startTag.Name, attributes, startTag.ForwardSlash, startTag.CloseAngle);

            return(tagHelperStartTag.WithSpanContext(startTag.GetSpanContext()));
        }