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;
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())); }