public void OptimizeSpan(Span span) { Require.NotNull(span, nameof(span)); var builder = new SpanBuilder(span); builder.ClearSymbols(); var prevType = HtmlSymbolType.Unknown; foreach (ISymbol item in span.Symbols) { var sym = item as HtmlSymbol; if (sym == null) { // On ne touche pas les éléments qui ne sont pas de type HtmlSymbol. builder.Accept(item); continue; } // FIXME: On perd toute information contextuelle. Peut-être pour remédier à ce problème // on pourrait ré-utiliser un HtmlSymbol. builder.Accept(new Symbol_ { Content = OptimizeContent(sym, prevType) }); prevType = sym.Type; } span.ReplaceWith(builder); }
public void IsCSharpOpenCurlyBrace_MultipleSymbols_ReturnFalse() { // Arrange var childBuilder = new SpanBuilder(SourceLocation.Zero); childBuilder.Accept(SyntaxFactory.Token(SyntaxKind.Identifier, "hello")); childBuilder.Accept(SyntaxFactory.Token(SyntaxKind.Comma, ",")); var child = childBuilder.Build(); // Act var result = DefaultRazorIndentationFactsService.IsCSharpOpenCurlyBrace(child); // Assert Assert.False(result); }
public void IsCSharpOpenCurlyBrace_MultipleSymbols_ReturnFalse() { // Arrange var childBuilder = new SpanBuilder(SourceLocation.Zero); childBuilder.Accept(new CSharpSymbol("hello", CSharpSymbolType.Identifier)); childBuilder.Accept(new CSharpSymbol(",", CSharpSymbolType.Comma)); var child = childBuilder.Build(); // Act var result = DefaultRazorIndentationFactsService.IsCSharpOpenCurlyBrace(child); // Assert Assert.False(result); }
public override void BuildSpan(SpanBuilder span, SourceLocation start, string content) { foreach (ISymbol sym in Language.TokenizeString(start, content)) { span.Accept(sym); } }
public void AddSpanAddsSpanToCurrentBlockBuilder() { // Arrange var factory = SpanFactory.CreateCsHtml(); var mockListener = new Mock<ParserVisitor>(); var context = SetupTestContext("phoo"); var builder = new SpanBuilder() { Kind = SpanKind.Code }; builder.Accept(new CSharpSymbol(1, 0, 1, "foo", CSharpSymbolType.Identifier)); var added = builder.Build(); using (context.StartBlock(BlockType.Functions)) { context.AddSpan(added); } var expected = new BlockBuilder() { Type = BlockType.Functions, }; expected.Children.Add(added); // Assert ParserTestBase.EvaluateResults(context.CompleteParse(), expected.Build()); }
public override void VisitSpan(Span span) { if (span.Kind == SpanKind.Markup) { var content = span.Content; content = _minifier.Minify(content); var builder = new SpanBuilder { CodeGenerator = span.CodeGenerator, EditHandler = span.EditHandler, Kind = span.Kind, Start = span.Start }; var symbol = new MarkupSymbol { Content = content }; builder.Accept(symbol); span.ReplaceWith(builder); } base.VisitSpan(span); }
public void AddSpanAddsSpanToCurrentBlockBuilder() { // Arrange var factory = SpanFactory.CreateCsHtml(); Mock <ParserVisitor> mockListener = new Mock <ParserVisitor>(); var context = SetupTestContext("phoo"); var builder = new SpanBuilder() { Kind = SpanKind.Code }; builder.Accept(new CSharpSymbol(1, 0, 1, "foo", CSharpSymbolType.Identifier)); var added = builder.Build(); using (context.StartBlock(BlockType.Functions)) { context.AddSpan(added); } var expected = new BlockBuilder() { Type = BlockType.Functions, }; expected.Children.Add(added); // Assert ParserTestBase.EvaluateResults(context.CompleteParse(), expected.Build()); }
public void AddLiteralChunk(string literal, SyntaxTreeNode association) { // If the previous chunk was also a LiteralChunk, append the content of the current node to the previous one. var literalChunk = _lastChunk as LiteralChunk; if (literalChunk != null) { // Literal chunks are always associated with Spans var lastSpan = (Span)literalChunk.Association; var currentSpan = (Span)association; var builder = new SpanBuilder(lastSpan); foreach (var symbol in currentSpan.Symbols) { builder.Accept(symbol); } literalChunk.Association = builder.Build(); literalChunk.Text += literal; } else { AddChunk(new LiteralChunk { Text = literal, }, association); } }
public void GetEditedContent_Span_ReturnsNewContent() { // Arrange var builder = new SpanBuilder(new SourceLocation(0, 0, 0)); builder.Accept(new RawTextSymbol(new SourceLocation(0, 0, 0), "Hello, ")); builder.Accept(new RawTextSymbol(new SourceLocation(7, 0, 7), "World")); var span = new Span(builder); var change = new SourceChange(2, 2, "heyo"); // Act var result = change.GetEditedContent(span); // Act Assert.Equal("Heheyoo, World", result); }
public void GetOrigninalText_SpanIsOwner_ReturnsContent_ZeroLengthSpan() { // Arrange var builder = new SpanBuilder(new SourceLocation(13, 0, 0)); builder.Accept(SyntaxFactory.Token(SyntaxKind.Unknown, "Hello, ")); builder.Accept(SyntaxFactory.Token(SyntaxKind.Unknown, "World")); var span = new Span(builder); var change = new SourceChange(15, 0, "heyo"); // Act var result = change.GetOriginalText(span); // Act Assert.Equal(string.Empty, result); }
public void GetEditedContent_Span_ReturnsNewContent() { // Arrange var builder = new SpanBuilder(new SourceLocation(0, 0, 0)); builder.Accept(SyntaxFactory.Token(SyntaxKind.Unknown, "Hello, ")); builder.Accept(SyntaxFactory.Token(SyntaxKind.Unknown, "World")); var span = new Span(builder); var change = new SourceChange(2, 2, "heyo"); // Act var result = change.GetEditedContent(span); // Act Assert.Equal("Heheyoo, World", result); }
public void GetOffSet_SpanIsOwner_ReturnsOffset() { // Arrange var builder = new SpanBuilder(new SourceLocation(13, 0, 0)); builder.Accept(new RawTextSymbol(new SourceLocation(13, 0, 13), "Hello, ")); builder.Accept(new RawTextSymbol(new SourceLocation(20, 0, 20), "World")); var span = new Span(builder); var change = new SourceChange(15, 2, "heyo"); // Act var result = change.GetOffset(span); // Act Assert.Equal(2, 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 KeyValuePair <string, SyntaxTreeNode> ParseSpan( Span span, IReadOnlyDictionary <string, string> attributeValueTypes) { var afterEquals = false; var builder = new SpanBuilder { CodeGenerator = span.CodeGenerator, EditHandler = span.EditHandler, Kind = span.Kind }; var htmlSymbols = span.Symbols.OfType <HtmlSymbol>().ToArray(); var symbolOffset = 1; 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 (name == null && symbol.Type == HtmlSymbolType.Text) { name = symbol.Content; } else if (afterEquals) { builder.Accept(symbol); } else if (symbol.Type == HtmlSymbolType.Equals) { // We've found an '=' symbol, this means that the coming symbols will either be a quote // or value (in the case that the value is unquoted). // Spaces after/before the equal symbol are not yet supported: // https://github.com/aspnet/Razor/issues/123 // TODO: Handle malformed tags, if there's an '=' then there MUST be a value. // https://github.com/aspnet/Razor/issues/104 // Check for attribute start values, aka single or double quote if (IsQuote(htmlSymbols[i + 1])) { // Move past the attribute start so we can accept the true value. i++; } else { // Set the symbol offset to 0 so we don't attempt to skip an end quote that doesn't exist. symbolOffset = 0; } afterEquals = true; } } return(CreateMarkupAttribute(name, builder, attributeValueTypes)); }
public void GetOrigninalText_SpanIsOwner_ReturnsContent_ZeroLengthSpan() { // Arrange var builder = new SpanBuilder(new SourceLocation(13, 0, 0)); builder.Accept(new RawTextSymbol(new SourceLocation(13, 0, 13), "Hello, ")); builder.Accept(new RawTextSymbol(new SourceLocation(20, 0, 20), "World")); var span = new Span(builder); var change = new SourceChange(15, 0, "heyo"); // Act var result = change.GetOriginalText(span); // Act Assert.Equal(string.Empty, result); }
public static Span BuildSpan(string content, SpanKind kind) { SpanBuilder builder = new SpanBuilder(); builder.Kind = kind; builder.Accept(new HtmlSymbol(new SourceLocation(), content, HtmlSymbolType.Text)); return(builder.Build()); }
public void GetOffSet_SpanIsOwner_ReturnsOffset() { // Arrange var builder = new SpanBuilder(new SourceLocation(13, 0, 0)); builder.Accept(SyntaxFactory.Token(SyntaxKind.Unknown, "Hello, ")); builder.Accept(SyntaxFactory.Token(SyntaxKind.Unknown, "World")); var span = new Span(builder); var change = new SourceChange(15, 2, "heyo"); // Act var result = change.GetOffset(span); // Act Assert.Equal(2, result); }
private static void VisitSymbol(ISymbol currentSymbol, ISymbol previousSymbol, SpanBuilder builder) { if (IsSymbolWhiteSpaceOrNewLine(currentSymbol, out var currentHtmlSymbol)) { if (IsSymbolWhiteSpaceOrNewLine(previousSymbol, out var _)) { // both current and previous symbols are whitespace/newline, we can skip current symbol return; } // current symbol is whitespace/newline, previous is not, we'll replace current with the smallest var replacementSymbol = GetReplacementSymbol(currentHtmlSymbol); builder.Accept(replacementSymbol); return; } builder.Accept(currentSymbol); }
public void GetOffSet_SpanIsNotOwnerOfChange_ThrowsException(int absoluteIndex, int length) { // Arrange var builder = new SpanBuilder(new SourceLocation(13, 0, 0)); builder.Accept(new RawTextSymbol(new SourceLocation(13, 0, 13), "Hello, ")); builder.Accept(new RawTextSymbol(new SourceLocation(20, 0, 20), "World")); var span = new Span(builder); var change = new SourceChange(12, 2, "heyo"); var expected = $"The node '{span}' is not the owner of change '{change}'."; // Act & Assert var exception = Assert.Throws <InvalidOperationException>(() => { change.GetOffset(span); }); Assert.Equal(expected, exception.Message); }
public SpanConstructor(SpanKind kind, IEnumerable<ISymbol> symbols) { Builder = new SpanBuilder(); Builder.Kind = kind; Builder.EditHandler = SpanEditHandler.CreateDefault(TestTokenizer); foreach (ISymbol sym in symbols) { Builder.Accept(sym); } }
public SpanConstructor(SpanKind kind, IEnumerable <ISymbol> symbols) { Builder = new SpanBuilder(); Builder.Kind = kind; Builder.EditHandler = SpanEditHandler.CreateDefault(TestTokenizer); foreach (ISymbol sym in symbols) { Builder.Accept(sym); } }
public void GetOffSet_SpanIsNotOwnerOfChange_ThrowsException() { // Arrange var builder = new SpanBuilder(new SourceLocation(13, 0, 0)); builder.Accept(SyntaxFactory.Token(SyntaxKind.Unknown, "Hello, ")); builder.Accept(SyntaxFactory.Token(SyntaxKind.Unknown, "World")); var span = new Span(builder); var change = new SourceChange(12, 2, "heyo"); var expected = $"The node '{span}' is not the owner of change '{change}'."; // Act & Assert var exception = Assert.Throws <InvalidOperationException>(() => { change.GetOffset(span); }); Assert.Equal(expected, exception.Message); }
private static Span GetSpan(SourceLocation start, string content) { var spanBuilder = new SpanBuilder(start); var tokens = CSharpLanguageCharacteristics.Instance.TokenizeString(content).ToArray(); foreach (var token in tokens) { spanBuilder.Accept(token); } var span = spanBuilder.Build(); return(span); }
public void IsCSharpOpenCurlyBrace_SpanWithLeftBrace_ReturnTrue() { // Arrange var childBuilder = new SpanBuilder(SourceLocation.Zero); childBuilder.Accept(SyntaxFactory.Token(SyntaxKind.LeftBrace, "{")); var child = childBuilder.Build(); // Act var result = DefaultRazorIndentationFactsService.IsCSharpOpenCurlyBrace(child); // Assert Assert.True(result); }
public void IsCSharpOpenCurlyBrace_SpanWithUnsupportedSymbolType_ReturnFalse(string content, object symbolTypeObject) { // Arrange var symbolType = (SyntaxKind)symbolTypeObject; var childBuilder = new SpanBuilder(SourceLocation.Zero); childBuilder.Accept(SyntaxFactory.Token(symbolType, content)); var child = childBuilder.Build(); // Act var result = DefaultRazorIndentationFactsService.IsCSharpOpenCurlyBrace(child); // Assert Assert.False(result); }
public void IsCSharpOpenCurlyBrace_SpanWithHtmlSymbol_ReturnFalse() { // Arrange var childBuilder = new SpanBuilder(SourceLocation.Zero); childBuilder.Accept(new HtmlToken("hello", HtmlTokenType.Text)); var child = childBuilder.Build(); // Act var result = DefaultRazorIndentationFactsService.IsCSharpOpenCurlyBrace(child); // Assert Assert.False(result); }
public void IsCSharpOpenCurlyBrace_SpanWithLeftBrace_ReturnTrue() { // Arrange var childBuilder = new SpanBuilder(SourceLocation.Zero); childBuilder.Accept(new CSharpSymbol("{", CSharpSymbolType.LeftBrace)); var child = childBuilder.Build(); // Act var result = DefaultRazorIndentationFactsService.IsCSharpOpenCurlyBrace(child); // Assert Assert.True(result); }
private static Span CreateCodeSpan(string code) { // TODO: Correctly reconstruct the syntax tree with correct symbols var builder = new SpanBuilder(); builder.Kind = SpanKind.Code; builder.Accept( new HtmlSymbol( new SourceLocation(0, 0, 0), code, HtmlSymbolType.Text ) ); return(builder.Build()); }
protected virtual SpanBuilder UpdateSpan(Span target, TextChange normalizedChange) { var newContent = normalizedChange.ApplyChange(target); var newSpan = new SpanBuilder(target); newSpan.ClearSymbols(); foreach (ISymbol sym in Tokenizer(newContent)) { sym.OffsetStart(target.Start); newSpan.Accept(sym); } if (target.Next != null) { var newEnd = SourceLocationTracker.CalculateNewLocation(target.Start, newContent); target.Next.ChangeStart(newEnd); } return(newSpan); }
public Span Span(SpanKind kind, params ISymbol[] symbols) { var builder = new SpanBuilder(); builder.Kind = kind; foreach (var symbol in symbols) { builder.Accept(symbol); } var span = builder.Build(); if (_last != null) { span.Previous = _last; _last.Next = span; } _last = span; return(span); }
public override void VisitSpan(Span span) { if (span.Kind == SpanKind.Markup) { string content = span.Content; content = _minifier.Minify(content); // We replace the content with the minified markup // and then let the CSharp/VB generator do their jobs. var builder = new SpanBuilder { CodeGenerator = span.CodeGenerator, EditHandler = span.EditHandler, Kind = span.Kind, Start = span.Start }; var symbol = new MarkupSymbol { Content = content }; builder.Accept(symbol); span.ReplaceWith(builder); } base.VisitSpan(span); }
public void OptimizeBlock(BlockBuilder block) { Require.NotNull(block, nameof(block)); for (int i = 0; i < block.Children.Count; i++) { var span = block.Children[i] as Span; if (!IsMarkup(span)) { // On ne touche pas les éléments qui ne sont pas de type Markup. continue; } // Si on est déjà arrivé à la dernière position, on récupère le contenu directement. string content = i == block.Children.Count - 1 ? span.Content : RemoveMarkupAndMergeContentAfter(block, span.Content, ref i); if (String.IsNullOrWhiteSpace(content)) { // Le contenu n'est constitué que d'espaces blancs. block.Children.RemoveAt(i); continue; } // On optimise le contenu et on recrée l'élément. var builder = new SpanBuilder(span); builder.ClearSymbols(); // FIXME: On perd toute information contextuelle. builder.Accept(new Symbol_ { Content = BustWhiteSpaces(content) }); span.ReplaceWith(builder); } }
protected virtual SpanBuilder UpdateSpan(Span target, TextChange normalizedChange) { var newContent = normalizedChange.ApplyChange(target); var newSpan = new SpanBuilder(target); newSpan.ClearSymbols(); foreach (ISymbol sym in Tokenizer(newContent)) { sym.OffsetStart(target.Start); newSpan.Accept(sym); } if (target.Next != null) { var newEnd = SourceLocationTracker.CalculateNewLocation(target.Start, newContent); target.Next.ChangeStart(newEnd); } return newSpan; }
// 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); }
// 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; }