From b390ae0c1cf608155a5a3640cd7db1a3d160d7f9 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Wed, 18 Apr 2018 13:06:21 -0400 Subject: [PATCH] Rewrite of HTML handling for Blazor This change replaces the parsing of HTML that we perform during the code generation phase, which parsing of HTML during the IR lowering phase. The main benefit of this change is that the structure of the HTML is reflected in the IR tree, allowing us to do more more advance transformations. As an example, see how the the handling of ` - IntermediateToken - (136:4,13 [2] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n - IntermediateToken - (138:5,0 [6] x:\dir\subdir\Test\TestComponent.cshtml) - Html - + HtmlElement - (0:0,0 [144] x:\dir\subdir\Test\TestComponent.cshtml) - div + HtmlContent - (5:0,5 [6] x:\dir\subdir\Test\TestComponent.cshtml) + IntermediateToken - (5:0,5 [6] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n + HtmlElement - (11:1,4 [125] x:\dir\subdir\Test\TestComponent.cshtml) - script + HtmlAttribute - - - + HtmlAttributeValue - - + IntermediateToken - - Html - some/url.js + HtmlAttribute - - - + HtmlAttributeValue - - + IntermediateToken - - Html - + HtmlContent - (78:1,71 [49] x:\dir\subdir\Test\TestComponent.cshtml) + IntermediateToken - (78:1,71 [49] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n some text\n some more text\n + HtmlContent - (136:4,13 [2] x:\dir\subdir\Test\TestComponent.cshtml) + IntermediateToken - (136:4,13 [2] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithCSharpExpression/TestComponent.ir.txt b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithCSharpExpression/TestComponent.ir.txt index 51819fa264..9e4744ba27 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithCSharpExpression/TestComponent.ir.txt +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithCSharpExpression/TestComponent.ir.txt @@ -8,10 +8,10 @@ Document - MethodDeclaration - - protected override - void - BuildRenderTree CSharpCode - IntermediateToken - - CSharp - base.BuildRenderTree(builder); - HtmlContent - (0:0,0 [18] x:\dir\subdir\Test\TestComponent.cshtml) - IntermediateToken - (0:0,0 [4] x:\dir\subdir\Test\TestComponent.cshtml) - Html -

- IntermediateToken - (4:0,4 [5] x:\dir\subdir\Test\TestComponent.cshtml) - Html - Hello - IntermediateToken - (9:0,9 [5] x:\dir\subdir\Test\TestComponent.cshtml) - Html -

+ HtmlElement - (0:0,0 [14] x:\dir\subdir\Test\TestComponent.cshtml) - h1 + HtmlContent - (4:0,4 [5] x:\dir\subdir\Test\TestComponent.cshtml) + IntermediateToken - (4:0,4 [5] x:\dir\subdir\Test\TestComponent.cshtml) - Html - Hello + HtmlContent - (14:0,14 [4] x:\dir\subdir\Test\TestComponent.cshtml) IntermediateToken - (14:0,14 [4] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n\n CSharpExpression - (20:2,2 [10] x:\dir\subdir\Test\TestComponent.cshtml) IntermediateToken - (20:2,2 [10] x:\dir\subdir\Test\TestComponent.cshtml) - CSharp - "My value" diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithComponent/TestComponent.ir.txt b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithComponent/TestComponent.ir.txt index e912a49e9f..e0bfe1b5ee 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithComponent/TestComponent.ir.txt +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithComponent/TestComponent.ir.txt @@ -8,12 +8,9 @@ Document - MethodDeclaration - - protected override - void - BuildRenderTree CSharpCode - IntermediateToken - - CSharp - base.BuildRenderTree(builder); - HtmlContent - (31:1,0 [18] x:\dir\subdir\Test\TestComponent.cshtml) - IntermediateToken - (31:1,0 [4] x:\dir\subdir\Test\TestComponent.cshtml) - Html -

- IntermediateToken - (35:1,4 [5] x:\dir\subdir\Test\TestComponent.cshtml) - Html - Hello - IntermediateToken - (40:1,9 [5] x:\dir\subdir\Test\TestComponent.cshtml) - Html -

+ HtmlElement - (31:1,0 [14] x:\dir\subdir\Test\TestComponent.cshtml) - h1 + HtmlContent - (35:1,4 [5] x:\dir\subdir\Test\TestComponent.cshtml) + IntermediateToken - (35:1,4 [5] x:\dir\subdir\Test\TestComponent.cshtml) - Html - Hello + HtmlContent - (45:1,14 [4] x:\dir\subdir\Test\TestComponent.cshtml) IntermediateToken - (45:1,14 [4] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n\n - TagHelper - (49:3,0 [22] x:\dir\subdir\Test\TestComponent.cshtml) - SomeOtherComponent - TagMode.SelfClosing - ComponentOpenExtensionNode - - Test.SomeOtherComponent - ComponentBodyExtensionNode - - ComponentCloseExtensionNode - + ComponentExtensionNode - (49:3,0 [22] x:\dir\subdir\Test\TestComponent.cshtml) - SomeOtherComponent - Test.SomeOtherComponent diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithDirective/TestComponent.ir.txt b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithDirective/TestComponent.ir.txt index 3feef6521b..e140153564 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithDirective/TestComponent.ir.txt +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithDirective/TestComponent.ir.txt @@ -9,7 +9,6 @@ Document - MethodDeclaration - - protected override - void - BuildRenderTree CSharpCode - IntermediateToken - - CSharp - base.BuildRenderTree(builder); - HtmlContent - (0:0,0 [18] x:\dir\subdir\Test\TestComponent.cshtml) - IntermediateToken - (0:0,0 [4] x:\dir\subdir\Test\TestComponent.cshtml) - Html -

- IntermediateToken - (4:0,4 [5] x:\dir\subdir\Test\TestComponent.cshtml) - Html - Hello - IntermediateToken - (9:0,9 [5] x:\dir\subdir\Test\TestComponent.cshtml) - Html -

+ HtmlElement - (0:0,0 [14] x:\dir\subdir\Test\TestComponent.cshtml) - h1 + HtmlContent - (4:0,4 [5] x:\dir\subdir\Test\TestComponent.cshtml) + IntermediateToken - (4:0,4 [5] x:\dir\subdir\Test\TestComponent.cshtml) - Html - Hello diff --git a/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/ComponentDocumentRewritePassTest.cs b/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/ComponentDocumentRewritePassTest.cs new file mode 100644 index 0000000000..6faab2841e --- /dev/null +++ b/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/ComponentDocumentRewritePassTest.cs @@ -0,0 +1,446 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Intermediate; +using Xunit; + + +namespace Microsoft.AspNetCore.Blazor.Razor +{ + public class ComponentDocumentRewritePassTest + { + public ComponentDocumentRewritePassTest() + { + var test = TagHelperDescriptorBuilder.Create("test", "test"); + test.TagMatchingRule(b => b.TagName = "test"); + + TagHelpers = new List() + { + test.Build(), + }; + + Pass = new ComponentDocumentRewritePass(); + Engine = RazorProjectEngine.Create( + BlazorExtensionInitializer.DefaultConfiguration, + RazorProjectFileSystem.Create(Environment.CurrentDirectory), + b => + { + b.Features.Add(new ComponentDocumentClassifierPass()); + b.Features.Add(Pass); + b.Features.Add(new StaticTagHelperFeature() { TagHelpers = TagHelpers, }); + }).Engine; + } + + private RazorEngine Engine { get; } + + private ComponentDocumentRewritePass Pass { get; } + + private List TagHelpers { get; } + + [Fact] + public void Execute_RewritesHtml_Basic() + { + // Arrange + var document = CreateDocument(@" + + + Hello, World! + +"); + + var documentNode = Lower(document); + + // Act + Pass.Execute(document, documentNode); + + // Assert + var method = documentNode.FindPrimaryMethod(); + Assert.Collection( + method.Children, + c => Assert.IsType(c), + c => NodeAssert.Whitespace(c), + c => NodeAssert.Element(c, "html")); + + var html = NodeAssert.Element(method.Children[2], "html"); + Assert.Equal(2, html.Source.Value.AbsoluteIndex); + Assert.Equal(1, html.Source.Value.LineIndex); + Assert.Equal(0, html.Source.Value.CharacterIndex); + Assert.Equal(68, html.Source.Value.Length); + Assert.Collection( + html.Children, + c => NodeAssert.Whitespace(c), + c => NodeAssert.Element(c, "head"), + c => NodeAssert.Whitespace(c)); + + var head = NodeAssert.Element(html.Children[1], "head"); + Assert.Equal(12, head.Source.Value.AbsoluteIndex); + Assert.Equal(2, head.Source.Value.LineIndex); + Assert.Equal(2, head.Source.Value.CharacterIndex); + Assert.Equal(49, head.Source.Value.Length); + Assert.Collection( + head.Children, + c => NodeAssert.Attribute(c, "cool", "beans"), + c => NodeAssert.Content(c, "Hello, World!")); + } + + [Fact] + public void Execute_RewritesHtml_Mixed() + { + // Arrange + var document = CreateDocument(@" + + + +"); + + var documentNode = Lower(document); + + // Act + Pass.Execute(document, documentNode); + + // Assert + var method = documentNode.FindPrimaryMethod(); + Assert.Collection( + method.Children, + c => Assert.IsType(c), + c => NodeAssert.Whitespace(c), + c => NodeAssert.Element(c, "html")); + + var html = NodeAssert.Element(method.Children[2], "html"); + Assert.Equal(2, html.Source.Value.AbsoluteIndex); + Assert.Equal(1, html.Source.Value.LineIndex); + Assert.Equal(0, html.Source.Value.CharacterIndex); + Assert.Equal(81, html.Source.Value.Length); + Assert.Collection( + html.Children, + c => NodeAssert.Whitespace(c), + c => NodeAssert.Element(c, "head"), + c => NodeAssert.Whitespace(c)); + + var head = NodeAssert.Element(html.Children[1], "head"); + Assert.Equal(12, head.Source.Value.AbsoluteIndex); + Assert.Equal(2, head.Source.Value.LineIndex); + Assert.Equal(2, head.Source.Value.CharacterIndex); + Assert.Equal(62, head.Source.Value.Length); + Assert.Collection( + head.Children, + c => NodeAssert.Attribute(c, "cool", "beans"), + c => NodeAssert.CSharpAttribute(c, "csharp", "yes"), + c => Assert.IsType(c), + c => NodeAssert.Whitespace(c)); + + var mixed = Assert.IsType(head.Children[2]); + Assert.Collection( + mixed.Children, + c => Assert.IsType(c), + c => Assert.IsType(c)); + } + + [Fact] + public void Execute_RewritesHtml_WithCode() + { + // Arrange + var document = CreateDocument(@" + + @if (some_bool) + { + + @hello + + } +"); + + var documentNode = Lower(document); + + // Act + Pass.Execute(document, documentNode); + + // Assert + var method = documentNode.FindPrimaryMethod(); + Assert.Collection( + method.Children, + c => Assert.IsType(c), + c => NodeAssert.Whitespace(c), + c => NodeAssert.Element(c, "html")); + + var html = NodeAssert.Element(method.Children[2], "html"); + Assert.Equal(2, html.Source.Value.AbsoluteIndex); + Assert.Equal(1, html.Source.Value.LineIndex); + Assert.Equal(0, html.Source.Value.CharacterIndex); + Assert.Equal(90, html.Source.Value.Length); + Assert.Collection( + html.Children, + c => NodeAssert.Whitespace(c), + c => Assert.IsType(c), + c => Assert.IsType(c), + c => NodeAssert.Whitespace(c), + c => NodeAssert.Element(c, "head"), + c => NodeAssert.Whitespace(c), + c => Assert.IsType(c)); + + var head = NodeAssert.Element(html.Children[4], "head"); + Assert.Equal(36, head.Source.Value.AbsoluteIndex); + Assert.Equal(4, head.Source.Value.LineIndex); + Assert.Equal(2, head.Source.Value.CharacterIndex); + Assert.Equal(42, head.Source.Value.Length); + Assert.Collection( + head.Children, + c => NodeAssert.Attribute(c, "cool", "beans"), + c => NodeAssert.Whitespace(c), + c => Assert.IsType(c), + c => NodeAssert.Whitespace(c)); + } + + [Fact] + public void Execute_RewritesHtml_TagHelper() + { + // Arrange + var document = CreateDocument(@" +@addTagHelper ""*, test"" + + + + Hello, World! + + +"); + + var documentNode = Lower(document); + + // Act + Pass.Execute(document, documentNode); + + // Assert + var method = documentNode.FindPrimaryMethod(); + Assert.Collection( + method.Children, + c => Assert.IsType(c), + c => NodeAssert.Whitespace(c), + c => Assert.IsType(c), + c => NodeAssert.Element(c, "html")); + + var html = NodeAssert.Element(method.Children[3], "html"); + Assert.Equal(27, html.Source.Value.AbsoluteIndex); + Assert.Equal(2, html.Source.Value.LineIndex); + Assert.Equal(0, html.Source.Value.CharacterIndex); + Assert.Equal(95, html.Source.Value.Length); + Assert.Collection( + html.Children, + c => NodeAssert.Whitespace(c), + c => Assert.IsType(c), + c => NodeAssert.Whitespace(c)); + + var body = html.Children + .OfType().Single().Children + .OfType().Single(); + + Assert.Collection( + body.Children, + c => NodeAssert.Whitespace(c), + c => NodeAssert.Element(c, "head"), + c => NodeAssert.Whitespace(c)); + + var head = body.Children[1]; + Assert.Equal(49, head.Source.Value.AbsoluteIndex); + Assert.Equal(4, head.Source.Value.LineIndex); + Assert.Equal(4, head.Source.Value.CharacterIndex); + Assert.Equal(53, head.Source.Value.Length); + Assert.Collection( + head.Children, + c => NodeAssert.Attribute(c, "cool", "beans"), + c => NodeAssert.Content(c, "Hello, World!")); + } + + [Fact] + public void Execute_RewritesHtml_UnbalancedClosingTagAtTopLevel() + { + // Arrange + var document = CreateDocument(@" +"); + + var documentNode = Lower(document); + + // Act + Pass.Execute(document, documentNode); + + // Assert + var method = documentNode.FindPrimaryMethod(); + Assert.Collection( + method.Children, + c => Assert.IsType(c), + c => NodeAssert.Whitespace(c), + c => NodeAssert.Element(c, "html")); + + var html = NodeAssert.Element(method.Children[2], "html"); + Assert.Equal(2, html.Source.Value.AbsoluteIndex); + Assert.Equal(1, html.Source.Value.LineIndex); + Assert.Equal(0, html.Source.Value.CharacterIndex); + Assert.Equal(7, html.Source.Value.Length); + + var diagnostic = Assert.Single(html.Diagnostics); + Assert.Same(BlazorDiagnosticFactory.UnexpectedClosingTag.Id, diagnostic.Id); + Assert.Equal(html.Source, diagnostic.Span); + } + + [Fact] + public void Execute_RewritesHtml_MismatchedClosingTag() + { + // Arrange + var document = CreateDocument(@" + +
+ +"); + + var documentNode = Lower(document); + + // Act + Pass.Execute(document, documentNode); + + // Assert + var method = documentNode.FindPrimaryMethod(); + Assert.Collection( + method.Children, + c => Assert.IsType(c), + c => NodeAssert.Whitespace(c), + c => NodeAssert.Element(c, "html")); + + var html = NodeAssert.Element(method.Children[2], "html"); + Assert.Collection( + html.Children, + c => NodeAssert.Whitespace(c), + c => NodeAssert.Element(c, "div"), + c => NodeAssert.Whitespace(c)); + + var div = NodeAssert.Element(html.Children[1], "div"); + Assert.Equal(12, div.Source.Value.AbsoluteIndex); + Assert.Equal(2, div.Source.Value.LineIndex); + Assert.Equal(2, div.Source.Value.CharacterIndex); + Assert.Equal(5, div.Source.Value.Length); + + var diagnostic = Assert.Single(div.Diagnostics); + Assert.Same(BlazorDiagnosticFactory.MismatchedClosingTag.Id, diagnostic.Id); + Assert.Equal(21,diagnostic.Span.AbsoluteIndex); + Assert.Equal(3, diagnostic.Span.LineIndex); + Assert.Equal(2, diagnostic.Span.CharacterIndex); + Assert.Equal(7, diagnostic.Span.Length); + } + + [Fact] + public void Execute_RewritesHtml_MalformedHtmlAtEnd() + { + // Arrange + var document = CreateDocument(@" + Assert.IsType(c), + c => NodeAssert.Whitespace(c), + c => NodeAssert.Content(c, " +
"); + + var documentNode = Lower(document); + + // Act + Pass.Execute(document, documentNode); + + // Assert + var method = documentNode.FindPrimaryMethod(); + Assert.Collection( + method.Children, + c => Assert.IsType(c), + c => NodeAssert.Whitespace(c), + c => NodeAssert.Element(c, "html")); + + var html = NodeAssert.Element(method.Children[2], "html"); + Assert.Collection( + html.Children, + c => NodeAssert.Whitespace(c), + c => NodeAssert.Element(c, "div")); + + var diagnostic = Assert.Single(html.Diagnostics); + Assert.Same(BlazorDiagnosticFactory.UnclosedTag.Id, diagnostic.Id); + Assert.Equal(2, diagnostic.Span.AbsoluteIndex); + Assert.Equal(1, diagnostic.Span.LineIndex); + Assert.Equal(0, diagnostic.Span.CharacterIndex); + Assert.Equal(6, diagnostic.Span.Length); + + var div = NodeAssert.Element(html.Children[1], "div"); + + diagnostic = Assert.Single(div.Diagnostics); + Assert.Same(BlazorDiagnosticFactory.UnclosedTag.Id, diagnostic.Id); + Assert.Equal(12, diagnostic.Span.AbsoluteIndex); + Assert.Equal(2, diagnostic.Span.LineIndex); + Assert.Equal(2, diagnostic.Span.CharacterIndex); + Assert.Equal(5, diagnostic.Span.Length); + } + + private RazorCodeDocument CreateDocument(string content) + { + // Normalize newlines since we are testing lengths of things. + content = content.Replace("\r", ""); + content = content.Replace("\n", "\r\n"); + + var source = RazorSourceDocument.Create(content, "test.cshtml"); + return RazorCodeDocument.Create(source); + } + + private DocumentIntermediateNode Lower(RazorCodeDocument codeDocument) + { + for (var i = 0; i < Engine.Phases.Count; i++) + { + var phase = Engine.Phases[i]; + if (phase is IRazorDocumentClassifierPhase) + { + break; + } + + phase.Execute(codeDocument); + } + + var document = codeDocument.GetDocumentIntermediateNode(); + Engine.Features.OfType().Single().Execute(codeDocument, document); + return document; + } + + private class StaticTagHelperFeature : ITagHelperFeature + { + public RazorEngine Engine { get; set; } + + public List TagHelpers { get; set; } + + public IReadOnlyList GetDescriptors() + { + return TagHelpers; + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test.csproj b/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test.csproj index f18032558d..9c9ce00ace 100644 --- a/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test.csproj +++ b/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test.csproj @@ -10,6 +10,7 @@ used to compile this assembly. --> true + 7.1 diff --git a/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/NodeAssert.cs b/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/NodeAssert.cs new file mode 100644 index 0000000000..f5b3a7b6fc --- /dev/null +++ b/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/NodeAssert.cs @@ -0,0 +1,126 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Text; +using Microsoft.AspNetCore.Razor.Language.Intermediate; +using Xunit; + +namespace Microsoft.AspNetCore.Blazor.Razor +{ + internal static class NodeAssert + { + public static HtmlAttributeIntermediateNode Attribute(IntermediateNode node, string attributeName, string attributeValue) + { + Assert.NotNull(node); + + var attributeNode = Assert.IsType(node); + Assert.Equal(attributeName, attributeNode.AttributeName); + + var attributeValueNode = Assert.IsType(Assert.Single(attributeNode.Children)); + var actual = new StringBuilder(); + for (var i = 0; i < attributeValueNode.Children.Count; i++) + { + var token = Assert.IsType(attributeValueNode.Children[i]); + Assert.Equal(TokenKind.Html, token.Kind); + actual.Append(token.Content); + } + + Assert.Equal(attributeValue, actual.ToString()); + + return attributeNode; + } + + public static HtmlAttributeIntermediateNode Attribute(IntermediateNodeCollection nodes, string attributeName, string attributeValue) + { + Assert.NotNull(nodes); + return Attribute(Assert.Single(nodes), attributeName, attributeValue); + } + + public static HtmlContentIntermediateNode Content(IntermediateNode node, string content, bool trim = true) + { + Assert.NotNull(node); + + var contentNode = Assert.IsType(node); + + var actual = new StringBuilder(); + for (var i = 0; i < contentNode.Children.Count; i++) + { + var token = Assert.IsType(contentNode.Children[i]); + Assert.Equal(TokenKind.Html, token.Kind); + actual.Append(token.Content); + } + + Assert.Equal(content, trim ? actual.ToString().Trim() : actual.ToString()); + return contentNode; + } + + public static HtmlContentIntermediateNode Content(IntermediateNodeCollection nodes, string content, bool trim = true) + { + Assert.NotNull(nodes); + return Content(Assert.Single(nodes), content, trim); + } + + public static HtmlAttributeIntermediateNode CSharpAttribute(IntermediateNode node, string attributeName, string attributeValue) + { + Assert.NotNull(node); + + var attributeNode = Assert.IsType(node); + Assert.Equal(attributeName, attributeNode.AttributeName); + + var attributeValueNode = Assert.IsType(Assert.Single(attributeNode.Children)); + var actual = new StringBuilder(); + for (var i = 0; i < attributeValueNode.Children.Count; i++) + { + var token = Assert.IsType(attributeValueNode.Children[i]); + Assert.Equal(TokenKind.CSharp, token.Kind); + actual.Append(token.Content); + } + + Assert.Equal(attributeValue, actual.ToString()); + + return attributeNode; + } + + public static HtmlAttributeIntermediateNode CSharpAttribute(IntermediateNodeCollection nodes, string attributeName, string attributeValue) + { + Assert.NotNull(nodes); + return Attribute(Assert.Single(nodes), attributeName, attributeValue); + } + + public static HtmlElementIntermediateNode Element(IntermediateNode node, string tagName) + { + Assert.NotNull(node); + + var elementNode = Assert.IsType(node); + Assert.Equal(tagName, elementNode.TagName); + return elementNode; + } + + public static HtmlElementIntermediateNode Element(IntermediateNodeCollection nodes, string tagName) + { + Assert.NotNull(nodes); + return Element(Assert.Single(nodes), tagName); + } + + public static HtmlContentIntermediateNode Whitespace(IntermediateNode node) + { + Assert.NotNull(node); + + var contentNode = Assert.IsType(node); + for (var i = 0; i < contentNode.Children.Count; i++) + { + var token = Assert.IsType(contentNode.Children[i]); + Assert.Equal(TokenKind.Html, token.Kind); + Assert.True(string.IsNullOrWhiteSpace(token.Content)); + } + + return contentNode; + } + + public static HtmlContentIntermediateNode Whitespace(IntermediateNodeCollection nodes) + { + Assert.NotNull(nodes); + return Whitespace(Assert.Single(nodes)); + } + } +}