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 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 override void VisitBlock(Block block) //{ // BlockBuilder parent = null; // if (_blocks.Count > 0) // { // parent = _blocks.Peek(); // } // BlockBuilder newBlock = new BlockBuilder(block); // newBlock.Children.Clear(); // _blocks.Push(newBlock); // if (block.Type == BlockType.Expression && parent != null) // { // VisitExpressionBlock(block, parent); // } // else // { // base.VisitBlock(block); // } // if (_blocks.Count > 1) // { // parent.Children.Add(_blocks.Pop().Build()); // } //} //public override void VisitSpan(Span span) //{ // Debug.Assert(_blocks.Count > 0); // _blocks.Peek().Children.Add(span); //} protected override SyntaxTreeNode RewriteBlock(BlockBuilder parent, Block block) { BlockBuilder newBlock = new BlockBuilder(block); newBlock.Children.Clear(); Span ws = block.Children.FirstOrDefault() as Span; IEnumerable <SyntaxTreeNode> newNodes = block.Children; if (ws.Content.All(Char.IsWhiteSpace)) { // Add this node to the parent SpanBuilder builder = new SpanBuilder(ws); builder.ClearSymbols(); FillSpan(builder, ws.Start, ws.Content); parent.Children.Add(builder.Build()); // Remove the old whitespace node newNodes = block.Children.Skip(1); } foreach (SyntaxTreeNode node in newNodes) { newBlock.Children.Add(node); } return(newBlock.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 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()); }
protected override SyntaxTreeNode RewriteSpan(BlockBuilder parent, Span span) { var b = new SpanBuilder(span); var old = (LiteralAttributeCodeGenerator)span.CodeGenerator; b.CodeGenerator = old.ValueGenerator != null ? new PreprocessedLiteralAttributeCodeGenerator(old.Prefix, old.ValueGenerator) : new PreprocessedLiteralAttributeCodeGenerator(old.Prefix, old.Value); return(b.Build()); }
// 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) { 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(new KeyValuePair <string, SyntaxTreeNode>(name, builder.Build())); }
protected override SyntaxTreeNode RewriteBlock(BlockBuilder parent, Block block) { // Collect the content of this node string content = String.Concat(block.Children.Cast <Span>().Select(s => s.Content)); // Create a new span containing this content SpanBuilder span = new SpanBuilder(); FillSpan(span, block.Children.Cast <Span>().First().Start, content); return(span.Build()); }
protected override SyntaxTreeNode RewriteBlock(BlockBuilder parent, Block block) { // Collect the content of this node var content = string.Concat(block.Children.Cast<Span>().Select(s => s.Content)); // Create a new span containing this content var span = new SpanBuilder(); span.EditHandler = new SpanEditHandler(HtmlTokenizer.Tokenize); FillSpan(span, block.Children.Cast<Span>().First().Start, content); return span.Build(); }
protected override SyntaxTreeNode RewriteBlock(BlockBuilder parent, Block block) { // Collect the content of this node var content = string.Concat(block.Children.Cast <Span>().Select(s => s.Content)); // Create a new span containing this content var span = new SpanBuilder(); span.EditHandler = new SpanEditHandler(HtmlTokenizer.Tokenize); FillSpan(span, block.Children.Cast <Span>().First().Start, content); return(span.Build()); }
public void IsCSharpOpenCurlyBrace_SpanWithHtmlSymbol_ReturnFalse() { // Arrange var childBuilder = new SpanBuilder(SourceLocation.Zero); childBuilder.Accept(SyntaxFactory.Token(SyntaxKind.Text, "hello")); 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(SyntaxFactory.Token(SyntaxKind.LeftBrace, "{")); var child = childBuilder.Build(); // Act var result = DefaultRazorIndentationFactsService.IsCSharpOpenCurlyBrace(child); // Assert Assert.True(result); }
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); }
private static Span CreateMarkupAttribute(SpanBuilder builder, bool isBoundNonStringAttribute) { Debug.Assert(builder != null); // 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 (isBoundNonStringAttribute) { builder.Kind = SpanKind.Code; } return(builder.Build()); }
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_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); }
protected override SyntaxTreeNode RewriteSpan(BlockBuilder parent, Span span) { // Only rewrite if we have a previous that is also markup (CanRewrite does this check for us!) var previous = parent.Children.LastOrDefault() as Span; if (previous == null || !CanRewrite(previous)) { return span; } // Merge spans parent.Children.Remove(previous); var merged = new SpanBuilder(); FillSpan(merged, previous.Start, previous.Content + span.Content); return merged.Build(); }
public void IsCSharpOpenCurlyBrace_MultipleSymbols_ReturnFalse() { // Arrange var childBuilder = new SpanBuilder(SourceLocation.Zero); childBuilder.Accept(new CSharpToken("hello", CSharpTokenType.Identifier)); childBuilder.Accept(new CSharpToken(",", CSharpTokenType.Comma)); var child = childBuilder.Build(); // Act var result = DefaultRazorIndentationFactsService.IsCSharpOpenCurlyBrace(child); // Assert Assert.False(result); }
protected override SyntaxTreeNode RewriteSpan(BlockBuilder parent, Span span) { // Only rewrite if we have a previous that is also markup (CanRewrite does this check for us!) Span previous = parent.Children.LastOrDefault() as Span; if (previous == null || !CanRewrite(previous)) { return(span); } // Merge spans parent.Children.Remove(previous); SpanBuilder merged = new SpanBuilder(); FillSpan(merged, previous.Start, previous.Content + span.Content); return(merged.Build()); }
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()); }
private static KeyValuePair <string, SyntaxTreeNode> CreateMarkupAttribute( string name, SpanBuilder builder, IReadOnlyDictionary <string, string> attributeValueTypes) { string attributeTypeName; // If the attribute was requested by the tag helper and doesn't happen to be a string then we need to treat // its value as code. Any non-string value can be any C# value so we need to ensure the SyntaxTreeNode // reflects that. if (attributeValueTypes.TryGetValue(name, out attributeTypeName) && !IsStringAttribute(attributeTypeName)) { builder.Kind = SpanKind.Code; } return(new KeyValuePair <string, SyntaxTreeNode>(name, builder.Build())); }
private static SyntaxTreeNode CreateMarkupAttribute(SpanBuilder builder, bool isBoundNonStringAttribute) { Span value = null; // Builder will be null in the case of minimized attributes if (builder != null) { // 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 (isBoundNonStringAttribute) { builder.Kind = SpanKind.Code; } value = builder.Build(); } return(value); }
protected override SyntaxTreeNode RewriteBlock(BlockBuilder parent, Block block) { // Collect the content of this node var builder = new StringBuilder(); for (var i = 0; i < block.Children.Count; i++) { var childSpan = (Span)block.Children[i]; builder.Append(childSpan.Content); } // Create a new span containing this content var span = new SpanBuilder(); span.EditHandler = new SpanEditHandler(HtmlTokenizer.Tokenize); Debug.Assert(block.Children.Count > 0); var start = ((Span)block.Children[0]).Start; FillSpan(span, start, builder.ToString()); return(span.Build()); }
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); }
protected override SyntaxTreeNode RewriteBlock(BlockBuilder parent, Block block) { var newBlock = new BlockBuilder(block); newBlock.Children.Clear(); var ws = block.Children.FirstOrDefault() as Span; IEnumerable<SyntaxTreeNode> newNodes = block.Children; if (ws.Content.All(Char.IsWhiteSpace)) { // Add this node to the parent var builder = new SpanBuilder(ws); builder.ClearSymbols(); FillSpan(builder, ws.Start, ws.Content); parent.Children.Add(builder.Build()); // Remove the old whitespace node newNodes = block.Children.Skip(1); } foreach (SyntaxTreeNode node in newNodes) { newBlock.Children.Add(node); } return newBlock.Build(); }
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; }
private static Block RebuildCodeGenerators(Block block) { var builder = new BlockBuilder(block); var isDynamic = builder.CodeGenerator is DynamicAttributeBlockCodeGenerator; // We don't want any attribute specific logic here, null out the block code generator. if (isDynamic || builder.CodeGenerator is AttributeBlockCodeGenerator) { builder.CodeGenerator = BlockCodeGenerator.Null; } for (var i = 0; i < builder.Children.Count; i++) { var child = builder.Children[i]; if (child.IsBlock) { // The child is a block, recurse down into the block to rebuild its children builder.Children[i] = RebuildCodeGenerators((Block)child); } else { var childSpan = (Span)child; ISpanCodeGenerator newCodeGenerator = null; var literalGenerator = childSpan.CodeGenerator as LiteralAttributeCodeGenerator; if (literalGenerator != null) { if (literalGenerator.ValueGenerator == null || literalGenerator.ValueGenerator.Value == null) { newCodeGenerator = new MarkupCodeGenerator(); } else { newCodeGenerator = literalGenerator.ValueGenerator.Value; } } else if (isDynamic && childSpan.CodeGenerator == SpanCodeGenerator.Null) { // Usually the dynamic code generator handles rendering the null code generators underneath // it. This doesn't make sense in terms of tag helpers though, we need to change null code // generators to markup code generators. newCodeGenerator = new MarkupCodeGenerator(); } // If we have a new code generator we'll need to re-build the child if (newCodeGenerator != null) { var childSpanBuilder = new SpanBuilder(childSpan) { CodeGenerator = newCodeGenerator }; builder.Children[i] = childSpanBuilder.Build(); } } } return(builder.Build()); }
private static Block RebuildChunkGenerators(Block block, bool isBound) { var builder = new BlockBuilder(block); // Don't want to rebuild unbound dynamic attributes. They need to run through the conditional attribute // removal system at runtime. A conditional attribute at the parse tree rewriting level is defined by // having at least 1 child with a DynamicAttributeBlockChunkGenerator. if (!isBound && block.Children.Any( child => child.IsBlock && ((Block)child).ChunkGenerator is DynamicAttributeBlockChunkGenerator)) { // The parent chunk generator must be removed because it's normally responsible for conditionally // generating the attribute prefix (class=") and suffix ("). The prefix and suffix concepts aren't // applicable for the TagHelper use case since the attributes are put into a dictionary like object as // name value pairs. builder.ChunkGenerator = ParentChunkGenerator.Null; return(builder.Build()); } var isDynamic = builder.ChunkGenerator is DynamicAttributeBlockChunkGenerator; // We don't want any attribute specific logic here, null out the block chunk generator. if (isDynamic || builder.ChunkGenerator is AttributeBlockChunkGenerator) { builder.ChunkGenerator = ParentChunkGenerator.Null; } for (var i = 0; i < builder.Children.Count; i++) { var child = builder.Children[i]; if (child.IsBlock) { // The child is a block, recurse down into the block to rebuild its children builder.Children[i] = RebuildChunkGenerators((Block)child, isBound); } else { var childSpan = (Span)child; ISpanChunkGenerator newChunkGenerator = null; var literalGenerator = childSpan.ChunkGenerator as LiteralAttributeChunkGenerator; if (literalGenerator != null) { if (literalGenerator.ValueGenerator == null || literalGenerator.ValueGenerator.Value == null) { newChunkGenerator = new MarkupChunkGenerator(); } else { newChunkGenerator = literalGenerator.ValueGenerator.Value; } } else if (isDynamic && childSpan.ChunkGenerator == SpanChunkGenerator.Null) { // Usually the dynamic chunk generator handles creating the null chunk generators underneath // it. This doesn't make sense in terms of tag helpers though, we need to change null code // generators to markup chunk generators. newChunkGenerator = new MarkupChunkGenerator(); } // If we have a new chunk generator we'll need to re-build the child if (newChunkGenerator != null) { var childSpanBuilder = new SpanBuilder(childSpan) { ChunkGenerator = newChunkGenerator }; builder.Children[i] = childSpanBuilder.Build(); } } } return(builder.Build()); }
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); }
private static Span CreateMarkupAttribute(SpanBuilder builder, bool isBoundNonStringAttribute) { Debug.Assert(builder != null); // 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 (isBoundNonStringAttribute) { builder.Kind = SpanKind.Code; } return builder.Build(); }
private static Block RebuildChunkGenerators(Block block, bool isBound) { var builder = new BlockBuilder(block); // Don't want to rebuild unbound dynamic attributes. They need to run through the conditional attribute // removal system at runtime. A conditional attribute at the parse tree rewriting level is defined by // having at least 1 child with a DynamicAttributeBlockChunkGenerator. if (!isBound && block.Children.Any( child => child.IsBlock && ((Block)child).ChunkGenerator is DynamicAttributeBlockChunkGenerator)) { // The parent chunk generator must be removed because it's normally responsible for conditionally // generating the attribute prefix (class=") and suffix ("). The prefix and suffix concepts aren't // applicable for the TagHelper use case since the attributes are put into a dictionary like object as // name value pairs. builder.ChunkGenerator = ParentChunkGenerator.Null; return builder.Build(); } var isDynamic = builder.ChunkGenerator is DynamicAttributeBlockChunkGenerator; // We don't want any attribute specific logic here, null out the block chunk generator. if (isDynamic || builder.ChunkGenerator is AttributeBlockChunkGenerator) { builder.ChunkGenerator = ParentChunkGenerator.Null; } for (var i = 0; i < builder.Children.Count; i++) { var child = builder.Children[i]; if (child.IsBlock) { // The child is a block, recurse down into the block to rebuild its children builder.Children[i] = RebuildChunkGenerators((Block)child, isBound); } else { var childSpan = (Span)child; ISpanChunkGenerator newChunkGenerator = null; var literalGenerator = childSpan.ChunkGenerator as LiteralAttributeChunkGenerator; if (literalGenerator != null) { if (literalGenerator.ValueGenerator == null || literalGenerator.ValueGenerator.Value == null) { newChunkGenerator = new MarkupChunkGenerator(); } else { newChunkGenerator = literalGenerator.ValueGenerator.Value; } } else if (isDynamic && childSpan.ChunkGenerator == SpanChunkGenerator.Null) { // Usually the dynamic chunk generator handles creating the null chunk generators underneath // it. This doesn't make sense in terms of tag helpers though, we need to change null code // generators to markup chunk generators. newChunkGenerator = new MarkupChunkGenerator(); } // If we have a new chunk generator we'll need to re-build the child if (newChunkGenerator != null) { var childSpanBuilder = new SpanBuilder(childSpan) { ChunkGenerator = newChunkGenerator }; builder.Children[i] = childSpanBuilder.Build(); } } } return builder.Build(); }