public override SyntaxNode VisitMarkupElement(MarkupElementSyntax node) { if (IsPartOfStartTag(node)) { // If this element is inside a start tag, it is some sort of malformed case like // <p @do { someattribute=\"btn\"></p>, where the end "p" tag is inside the start "p" tag. // We don't want to do tag helper parsing for this tag. return(base.VisitMarkupElement(node)); } MarkupTagHelperStartTagSyntax tagHelperStart = null; MarkupTagHelperEndTagSyntax tagHelperEnd = null; TagHelperInfo tagHelperInfo = null; // Visit the start tag. var startTag = (MarkupStartTagSyntax)Visit(node.StartTag); if (startTag != null) { var tagName = startTag.GetTagName(); if (TryRewriteTagHelperStart(startTag, out tagHelperStart, out tagHelperInfo)) { // This is a tag helper. if (tagHelperInfo.TagMode == TagMode.SelfClosing || tagHelperInfo.TagMode == TagMode.StartTagOnly) { var tagHelperElement = SyntaxFactory.MarkupTagHelperElement(tagHelperStart, body: new SyntaxList <RazorSyntaxNode>(), endTag: null); var rewrittenTagHelper = tagHelperElement.WithTagHelperInfo(tagHelperInfo); if (node.Body.Count == 0) { return(rewrittenTagHelper); } // This tag contains a body which needs to be moved to the parent. var rewrittenNodes = SyntaxListBuilder <RazorSyntaxNode> .Create(); rewrittenNodes.Add(rewrittenTagHelper); var rewrittenBody = VisitList(node.Body); rewrittenNodes.AddRange(rewrittenBody); return(SyntaxFactory.MarkupElement(startTag: null, body: rewrittenNodes.ToList(), endTag: null)); } else if (node.EndTag == null) { // Start tag helper with no corresponding end tag. _errorSink.OnError( RazorDiagnosticFactory.CreateParsing_TagHelperFoundMalformedTagHelper( new SourceSpan(SourceLocationTracker.Advance(startTag.GetSourceLocation(_source), "<"), tagName.Length), tagName)); } else { // Tag helper start tag. Keep track. var tracker = new TagHelperTracker(_tagHelperPrefix, tagHelperInfo); _trackerStack.Push(tracker); } } else { // Non-TagHelper tag. ValidateParentAllowsPlainStartTag(startTag); if (!startTag.IsSelfClosing() && !startTag.IsVoidElement()) { var tracker = new TagTracker(tagName, isTagHelper: false); _trackerStack.Push(tracker); } } } // Visit body between start and end tags. var body = VisitList(node.Body); // Visit end tag. var endTag = (MarkupEndTagSyntax)Visit(node.EndTag); if (endTag != null) { var tagName = endTag.GetTagName(); if (TryRewriteTagHelperEnd(endTag, out tagHelperEnd)) { // This is a tag helper if (startTag == null) { // The end tag helper has no corresponding start tag, create an error. _errorSink.OnError( RazorDiagnosticFactory.CreateParsing_TagHelperFoundMalformedTagHelper( new SourceSpan(SourceLocationTracker.Advance(endTag.GetSourceLocation(_source), "</"), tagName.Length), tagName)); } } else { // Non tag helper end tag. if (startTag == null) { // Standalone end tag. We may need to error if it is not supposed to be here. // If there was a corresponding start tag, we would have already added this error. ValidateParentAllowsPlainEndTag(endTag); } else { // Since a start tag exists, we must already be tracking it. // Pop the stack as we're done with the end tag. _trackerStack.Pop(); } } } if (tagHelperInfo != null) { // If we get here it means this element was rewritten as a tag helper. var tagHelperElement = SyntaxFactory.MarkupTagHelperElement(tagHelperStart, body, tagHelperEnd); return(tagHelperElement.WithTagHelperInfo(tagHelperInfo)); } // There was no matching tag helper for this element. Return. return(node.Update(startTag, body, endTag)); }
private static SyntaxList <RazorSyntaxNode> GetRewrittenMarkupStartTagChildren(MarkupStartTagSyntax node) { // Rewrites the children of the start tag to look like the legacy syntax tree. if (node.IsMarkupTransition) { var tokens = node.DescendantNodes().Where(n => n is SyntaxToken token && !token.IsMissing).Cast <SyntaxToken>().ToArray(); var tokenBuilder = SyntaxListBuilder <SyntaxToken> .Create(); tokenBuilder.AddRange(tokens, 0, tokens.Length); var markupTransition = SyntaxFactory.MarkupTransition(tokenBuilder.ToList()).Green.CreateRed(node, node.Position); var spanContext = node.GetSpanContext(); if (spanContext != null) { markupTransition = markupTransition.WithSpanContext(spanContext); } var builder = new SyntaxListBuilder(1); builder.Add(markupTransition); return(new SyntaxList <RazorSyntaxNode>(builder.ToListNode().CreateRed(node, node.Position))); } SpanContext latestSpanContext = null; var children = node.Children; var newChildren = new SyntaxListBuilder(children.Count); var literals = new List <MarkupTextLiteralSyntax>(); foreach (var child in children) { if (child is MarkupTextLiteralSyntax literal) { literals.Add(literal); latestSpanContext = literal.GetSpanContext() ?? latestSpanContext; } else if (child is MarkupMiscAttributeContentSyntax miscContent) { foreach (var contentChild in miscContent.Children) { if (contentChild is MarkupTextLiteralSyntax contentLiteral) { literals.Add(contentLiteral); latestSpanContext = contentLiteral.GetSpanContext() ?? latestSpanContext; } else { // Pop stack AddLiteralIfExists(); newChildren.Add(contentChild); } } } else { AddLiteralIfExists(); newChildren.Add(child); } } AddLiteralIfExists(); return(new SyntaxList <RazorSyntaxNode>(newChildren.ToListNode().CreateRed(node, node.Position))); void AddLiteralIfExists() { if (literals.Count > 0) { var mergedLiteral = SyntaxUtilities.MergeTextLiterals(literals.ToArray()); mergedLiteral = mergedLiteral.WithSpanContext(latestSpanContext); literals.Clear(); latestSpanContext = null; newChildren.Add(mergedLiteral); } } }
private static TryParseResult TryParseAttribute( string tagName, MarkupAttributeBlockSyntax attributeBlock, IEnumerable <TagHelperDescriptor> descriptors, ErrorSink errorSink, HashSet <string> processedBoundAttributeNames) { // Have a name now. Able to determine correct isBoundNonStringAttribute value. var result = CreateTryParseResult(attributeBlock.Name.GetContent(), descriptors, processedBoundAttributeNames); if (attributeBlock.ValuePrefix == null) { // We are purposefully not persisting NoQuotes even for unbound attributes because it is still possible to // rewrite the values that introduces a space like in UrlResolutionTagHelper. // The other case is it 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; } else { var lastToken = attributeBlock.ValuePrefix.GetLastToken(); switch (lastToken.Kind) { case SyntaxKind.DoubleQuote: result.AttributeStructure = AttributeStructure.DoubleQuotes; break; case SyntaxKind.SingleQuote: result.AttributeStructure = AttributeStructure.SingleQuotes; break; default: result.AttributeStructure = AttributeStructure.Minimized; break; } } var attributeValue = attributeBlock.Value; if (attributeValue == null) { var builder = SyntaxListBuilder <RazorSyntaxNode> .Create(); // Add a marker for attribute value when there are no quotes like, <p class= > builder.Add(SyntaxFactory.MarkupTextLiteral(new SyntaxList <SyntaxToken>())); attributeValue = SyntaxFactory.GenericBlock(builder.ToList()); } var rewrittenValue = RewriteAttributeValue(result, attributeValue); if (result.IsDirectiveAttribute) { // Directive attributes have a different syntax. result.RewrittenAttribute = RewriteToDirectiveAttribute(attributeBlock, result, rewrittenValue); return(result); } else { var rewritten = SyntaxFactory.MarkupTagHelperAttribute( attributeBlock.NamePrefix, attributeBlock.Name, attributeBlock.NameSuffix, attributeBlock.EqualsToken, attributeBlock.ValuePrefix, rewrittenValue, attributeBlock.ValueSuffix); rewritten = rewritten.WithTagHelperAttributeInfo( new TagHelperAttributeInfo(result.AttributeName, parameterName: null, result.AttributeStructure, result.IsBoundAttribute, isDirectiveAttribute: false)); result.RewrittenAttribute = rewritten; return(result); } }
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())); }
private static SyntaxNode CreateNode(IEnumerable <TNode> nodes) { if (nodes == null) { return(null); } var builder = (nodes is ICollection <TNode> collection) ? new SyntaxListBuilder <TNode>(collection.Count) : SyntaxListBuilder <TNode> .Create(); foreach (var node in nodes) { builder.Add(node); } return(builder.ToList().Node); }
private static SyntaxNode?CreateNode(IEnumerable <TNode>?nodes) { if (nodes == null) { return(null); } var collection = nodes as ICollection <TNode>; var builder = (collection != null) ? new SyntaxListBuilder <TNode>(collection.Count) : SyntaxListBuilder <TNode> .Create(); foreach (TNode node in nodes) { builder.Add(node); } return(builder.ToList().Node); }
private void VisitAttributeValue(SyntaxNode node) { if (node == null) { return; } IReadOnlyList <SyntaxNode> children = node.ChildNodes(); var position = node.Position; if (children.First() is MarkupBlockSyntax markupBlock && markupBlock.Children.Count == 2 && markupBlock.Children[0] is MarkupTextLiteralSyntax && markupBlock.Children[1] is MarkupEphemeralTextLiteralSyntax) { // This is a special case when we have an attribute like attr="@@foo". // In this case, we want the foo to be written out as HtmlContent and not HtmlAttributeValue. Visit(markupBlock); children = children.Skip(1).ToList(); position = children.Count > 0 ? children[0].Position : position; } if (children.All(c => c is MarkupLiteralAttributeValueSyntax)) { var literalAttributeValueNodes = children.Cast <MarkupLiteralAttributeValueSyntax>().ToArray(); var valueTokens = SyntaxListBuilder <SyntaxToken> .Create(); for (var i = 0; i < literalAttributeValueNodes.Length; i++) { var mergedValue = MergeAttributeValue(literalAttributeValueNodes[i]); valueTokens.AddRange(mergedValue.LiteralTokens); } var rewritten = SyntaxFactory.MarkupTextLiteral(valueTokens.ToList()).Green.CreateRed(node.Parent, position); Visit(rewritten); } else if (children.All(c => c is MarkupTextLiteralSyntax)) { var builder = SyntaxListBuilder <SyntaxToken> .Create(); var markupLiteralArray = children.Cast <MarkupTextLiteralSyntax>(); foreach (var literal in markupLiteralArray) { builder.AddRange(literal.LiteralTokens); } var rewritten = SyntaxFactory.MarkupTextLiteral(builder.ToList()).Green.CreateRed(node.Parent, position); Visit(rewritten); } else if (children.All(c => c is CSharpExpressionLiteralSyntax)) { var builder = SyntaxListBuilder <SyntaxToken> .Create(); var expressionLiteralArray = children.Cast <CSharpExpressionLiteralSyntax>(); SpanContext context = null; foreach (var literal in expressionLiteralArray) { context = literal.GetSpanContext(); builder.AddRange(literal.LiteralTokens); } var rewritten = SyntaxFactory.CSharpExpressionLiteral(builder.ToList()).Green.CreateRed(node.Parent, position); rewritten = context != null?rewritten.WithSpanContext(context) : rewritten; Visit(rewritten); } else { Visit(node); } }
private SyntaxList <RazorSyntaxNode> GetLegacyChildren() { // This method returns the children of this start tag in legacy format. // This is needed to generate the same classified spans as the legacy syntax tree. var builder = new SyntaxListBuilder(5); var tokens = SyntaxListBuilder <SyntaxToken> .Create(); var context = this.GetSpanContext(); // We want to know if this tag contains non-whitespace attribute content to set the appropriate AcceptedCharacters. // The prefix of a start tag(E.g '|<foo| attr>') will have 'Any' accepted characters if non-whitespace attribute content exists. var acceptsAnyContext = new SpanContext(context.ChunkGenerator, SpanEditHandler.CreateDefault()); acceptsAnyContext.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.Any; var containsAttributesContent = false; foreach (var attribute in Attributes) { if (!string.IsNullOrWhiteSpace(attribute.GetContent())) { containsAttributesContent = true; break; } } if (!OpenAngle.IsMissing) { tokens.Add(OpenAngle); } if (Bang != null) { builder.Add(SyntaxFactory.MarkupTextLiteral(tokens.Consume()).WithSpanContext(acceptsAnyContext)); tokens.Add(Bang); var acceptsNoneContext = new SpanContext(context.ChunkGenerator, SpanEditHandler.CreateDefault()); acceptsNoneContext.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.None; builder.Add(SyntaxFactory.RazorMetaCode(tokens.Consume()).WithSpanContext(acceptsNoneContext)); } if (!Name.IsMissing) { tokens.Add(Name); } builder.Add(SyntaxFactory.MarkupTextLiteral(tokens.Consume()).WithSpanContext(containsAttributesContent ? acceptsAnyContext : context)); builder.AddRange(Attributes); if (ForwardSlash != null) { tokens.Add(ForwardSlash); } if (!CloseAngle.IsMissing) { tokens.Add(CloseAngle); } if (tokens.Count > 0) { builder.Add(SyntaxFactory.MarkupTextLiteral(tokens.Consume()).WithSpanContext(context)); } return(new SyntaxList <RazorSyntaxNode>(builder.ToListNode().CreateRed(this, Position))); }
private static SyntaxList <RazorSyntaxNode> GetRewrittenChildren( string tagName, bool validStructure, MarkupStartTagSyntax 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 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 < 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()); }