private static TryParseResult TryParseBlock( string tagName, Block block, IEnumerable <TagHelperDescriptor> descriptors, ErrorSink errorSink) { // 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 != SpanKind.Markup) { errorSink.OnError( block.Start, RazorResources.FormatTagHelpers_CannotHaveCSharpInTagDeclaration(tagName), block.Length); 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)); } 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)) { errorSink.OnError( childSpan.Start, RazorResources.FormatTagHelpers_AttributesMustHaveAName(tagName), childSpan.Length); return(null); } // Have a name now. Able to determine correct isBoundNonStringAttribute value. var result = CreateTryParseResult(name, descriptors); 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) { // 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" />. case HtmlSymbolType.Equals: case HtmlSymbolType.DoubleQuote: result.AttributeValueStyle = HtmlAttributeValueStyle.DoubleQuotes; break; case HtmlSymbolType.SingleQuote: result.AttributeValueStyle = HtmlAttributeValueStyle.SingleQuotes; break; default: result.AttributeValueStyle = HtmlAttributeValueStyle.Minimized; break; } } // Remove first child i.e. foo=" builder.Children.RemoveAt(0); // Grabbing last child to check if the attribute value is quoted. var endNode = block.Children.Last(); if (!endNode.IsBlock) { var endSpan = (Span)endNode; // In some malformed cases e.g. <p bar="false', the last Span (false' in the ex.) may contain more // than a single HTML symbol. Do not ignore those other symbols. var symbolCount = endSpan.Symbols.Count(); var endSymbol = symbolCount == 1 ? (HtmlSymbol)endSpan.Symbols.First() : null; // Checking to see if it's a quoted attribute, if so we should remove end quote if (endSymbol != null && IsQuote(endSymbol)) { builder.Children.RemoveAt(builder.Children.Count - 1); } } // We need to rebuild the chunk generators of the builder and its children (this is needed to // ensure we don't do special attribute chunk generation since this is a tag helper). block = RebuildChunkGenerators(builder.Build(), result.IsBoundAttribute); // If there's only 1 child at this point its value could be a simple markup span (treated differently than // block level elements for attributes). if (block.Children.Count() == 1) { var child = block.Children.First() as Span; if (child != null) { // After pulling apart the block we just have a value span. var spanBuilder = new SpanBuilder(child); result.AttributeValueNode = CreateMarkupAttribute(spanBuilder, result.IsBoundNonStringAttribute); return(result); } } var isFirstSpan = true; result.AttributeValueNode = ConvertToMarkupAttributeBlock( block, (parentBlock, span) => { // If the attribute was requested by a tag helper but the corresponding property was not a // string, then treat its value as code. A non-string value can be any C# value so we need // to ensure the SyntaxTreeNode reflects that. if (result.IsBoundNonStringAttribute) { // For bound non-string attributes, we'll only allow a transition span to appear at the very // beginning of the attribute expression. All later transitions would appear as code so that // they are part of the generated output. E.g. // key="@value" -> MyTagHelper.key = value // key=" @value" -> MyTagHelper.key = @value // key="1 + @case" -> MyTagHelper.key = 1 + @case // key="@int + @case" -> MyTagHelper.key = int + @case // key="@(a + b) -> MyTagHelper.key = a + b // key="4 + @(a + b)" -> MyTagHelper.key = 4 + @(a + b) if (isFirstSpan && span.Kind == SpanKind.Transition) { // do nothing. } else { var spanBuilder = new SpanBuilder(span); if (parentBlock.Type == BlockType.Expression && (spanBuilder.Kind == SpanKind.Transition || spanBuilder.Kind == SpanKind.MetaCode)) { // Change to a MarkupChunkGenerator so that the '@' \ parenthesis is generated as part of the output. spanBuilder.ChunkGenerator = new MarkupChunkGenerator(); } spanBuilder.Kind = SpanKind.Code; span = spanBuilder.Build(); } } isFirstSpan = false; return(span); }); return(result); }
// This method handles cases when the attribute is a simple span attribute such as // class="something moresomething". This does not handle complex attributes such as // class="@myclass". Therefore the span.Content is equivalent to the entire attribute. private static TryParseResult TryParseSpan( Span span, IEnumerable <TagHelperDescriptor> descriptors, ErrorSink errorSink) { var afterEquals = false; var builder = new SpanBuilder { ChunkGenerator = span.ChunkGenerator, EditHandler = span.EditHandler, Kind = span.Kind }; // Will contain symbols that represent a single attribute value: <input| class="btn"| /> var htmlSymbols = span.Symbols.OfType <HtmlSymbol>().ToArray(); var capturedAttributeValueStart = false; var attributeValueStartLocation = span.Start; // Default to DoubleQuotes. We purposefully do not persist NoQuotes ValueStyle to stay consistent with the // TryParseBlock() variation of attribute parsing. var attributeValueStyle = HtmlAttributeValueStyle.DoubleQuotes; // The symbolOffset is initialized to 0 to expect worst case: "class=". If a quote is found later on for // the attribute value the symbolOffset is adjusted accordingly. var symbolOffset = 0; string name = null; // Iterate down through the symbols to find the name and the start of the value. // We subtract the symbolOffset so we don't accept an ending quote of a span. for (var i = 0; i < htmlSymbols.Length - symbolOffset; i++) { var symbol = htmlSymbols[i]; if (afterEquals) { // We've captured all leading whitespace, the attribute name, and an equals with an optional // quote/double quote. We're now at: " asp-for='|...'" or " asp-for=|..." // The goal here is to capture all symbols until the end of the attribute. Note this will not // consume an ending quote due to the symbolOffset. // When symbols are accepted into SpanBuilders, their locations get altered to be offset by the // parent which is why we need to mark our start location prior to adding the symbol. // This is needed to know the location of the attribute value start within the document. if (!capturedAttributeValueStart) { capturedAttributeValueStart = true; attributeValueStartLocation = span.Start + symbol.Start; } builder.Accept(symbol); } else if (name == null && HtmlMarkupParser.IsValidAttributeNameSymbol(symbol)) { // We've captured all leading whitespace prior to the attribute name. // We're now at: " |asp-for='...'" or " |asp-for=..." // The goal here is to capture the attribute name. var symbolContents = htmlSymbols .Skip(i) // Skip prefix .TakeWhile(nameSymbol => HtmlMarkupParser.IsValidAttributeNameSymbol(nameSymbol)) .Select(nameSymbol => nameSymbol.Content); // Move the indexer past the attribute name symbols. i += symbolContents.Count() - 1; name = string.Concat(symbolContents); attributeValueStartLocation = SourceLocation.Advance(attributeValueStartLocation, name); } else if (symbol.Type == HtmlSymbolType.Equals) { // We've captured all leading whitespace and the attribute name. // We're now at: " asp-for|='...'" or " asp-for|=..." // The goal here is to consume the equal sign and the optional single/double-quote. // The coming symbols will either be a quote or value (in the case that the value is unquoted). SourceLocation symbolStartLocation; // Skip the whitespace preceding the start of the attribute value. do { i++; // Start from the symbol after '='. } while (i < htmlSymbols.Length && (htmlSymbols[i].Type == HtmlSymbolType.WhiteSpace || htmlSymbols[i].Type == HtmlSymbolType.NewLine)); // Check for attribute start values, aka single or double quote if (i < htmlSymbols.Length && IsQuote(htmlSymbols[i])) { if (htmlSymbols[i].Type == HtmlSymbolType.SingleQuote) { attributeValueStyle = HtmlAttributeValueStyle.SingleQuotes; } symbolStartLocation = htmlSymbols[i].Start; // If there's a start quote then there must be an end quote to be valid, skip it. symbolOffset = 1; } else { // We are at the symbol after equals. Go back to equals to ensure we don't skip past that symbol. i--; symbolStartLocation = symbol.Start; } attributeValueStartLocation = span.Start + symbolStartLocation + new SourceLocation(absoluteIndex: 1, lineIndex: 0, characterIndex: 1); afterEquals = true; } else if (symbol.Type == HtmlSymbolType.WhiteSpace) { // We're at the start of the attribute, this branch may be hit on the first iterations of // the loop since the parser separates attributes with their spaces included as symbols. // We're at: "| asp-for='...'" or "| asp-for=..." // Note: This will not be hit even for situations like asp-for ="..." because the core Razor // parser currently does not know how to handle attributes in that format. This will be addressed // by https://github.com/aspnet/Razor/issues/123. attributeValueStartLocation = SourceLocation.Advance(attributeValueStartLocation, symbol.Content); } } // After all symbols have been added we need to set the builders start position so we do not indirectly // modify each symbol's Start location. builder.Start = attributeValueStartLocation; if (name == null) { // We couldn't find a name, 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"| | if (!string.IsNullOrWhiteSpace(span.Content)) { errorSink.OnError( span.Start, RazorResources.TagHelperBlockRewriter_TagHelperAttributeListMustBeWellFormed, span.Content.Length); } return(null); } var result = CreateTryParseResult(name, descriptors); // If we're not after an equal then we should treat the value as if it were a minimized attribute. Span attributeValue = null; if (afterEquals) { attributeValue = CreateMarkupAttribute(builder, result.IsBoundNonStringAttribute); } else { attributeValueStyle = HtmlAttributeValueStyle.Minimized; } result.AttributeValueNode = attributeValue; result.AttributeValueStyle = attributeValueStyle; return(result); }