From 7e40427ffe0180dadd3d5d46ad01e1eb13d91cd3 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Mon, 15 Jan 2018 22:42:50 +0000 Subject: [PATCH] Razor compilation: Support text literals and C# code --- .../Engine/BlazorIntermediateNodeWriter.cs | 72 +- .../Engine/BlazorRazorEngine.cs | 3 + .../Engine/CodeWriterExtensions.cs | 647 ++++++++++++++++++ .../Microsoft.Blazor.Build.Test.csproj | 3 + .../RazorCompilerTest.cs | 68 +- .../Microsoft.Blazor.Test.csproj | 3 + .../RenderTreeBuilderTest.cs | 1 + test/Microsoft.Blazor.Test/RendererTest.cs | 1 + .../AssertNode.cs | 4 +- 9 files changed, 796 insertions(+), 6 deletions(-) create mode 100644 src/Microsoft.Blazor.Build/Core/RazorCompilation/Engine/CodeWriterExtensions.cs rename test/{Microsoft.Blazor.Test => shared}/AssertNode.cs (96%) diff --git a/src/Microsoft.Blazor.Build/Core/RazorCompilation/Engine/BlazorIntermediateNodeWriter.cs b/src/Microsoft.Blazor.Build/Core/RazorCompilation/Engine/BlazorIntermediateNodeWriter.cs index d8c84b8c22..5a81d8ebe9 100644 --- a/src/Microsoft.Blazor.Build/Core/RazorCompilation/Engine/BlazorIntermediateNodeWriter.cs +++ b/src/Microsoft.Blazor.Build/Core/RazorCompilation/Engine/BlazorIntermediateNodeWriter.cs @@ -3,6 +3,10 @@ using Microsoft.AspNetCore.Razor.Language.CodeGeneration; using Microsoft.AspNetCore.Razor.Language.Intermediate; +using Microsoft.Blazor.RenderTree; +using System; +using System.Linq; +using System.Text; namespace Microsoft.Blazor.Build.Core.RazorCompilation.Engine { @@ -11,6 +15,8 @@ namespace Microsoft.Blazor.Build.Core.RazorCompilation.Engine /// internal class BlazorIntermediateNodeWriter : IntermediateNodeWriter { + private const string builderVarName = "builder"; + public override void BeginWriterScope(CodeRenderingContext context, string writer) { throw new System.NotImplementedException(nameof(BeginWriterScope)); @@ -23,7 +29,34 @@ namespace Microsoft.Blazor.Build.Core.RazorCompilation.Engine public override void WriteCSharpCode(CodeRenderingContext context, CSharpCodeIntermediateNode node) { - throw new System.NotImplementedException(nameof(WriteCSharpCode)); + var isWhitespaceStatement = true; + for (var i = 0; i < node.Children.Count; i++) + { + var token = node.Children[i] as IntermediateToken; + if (token == null || !string.IsNullOrWhiteSpace(token.Content)) + { + isWhitespaceStatement = false; + break; + } + } + + if (isWhitespaceStatement) + { + return; + } + + for (var i = 0; i < node.Children.Count; i++) + { + if (node.Children[i] is IntermediateToken token && token.IsCSharp) + { + context.CodeWriter.Write(token.Content); + } + else + { + // There may be something else inside the statement like an extension node. + context.RenderNode(node.Children[i]); + } + } } public override void WriteCSharpCodeAttributeValue(CodeRenderingContext context, CSharpCodeAttributeValueIntermediateNode node) @@ -33,7 +66,26 @@ namespace Microsoft.Blazor.Build.Core.RazorCompilation.Engine public override void WriteCSharpExpression(CodeRenderingContext context, CSharpExpressionIntermediateNode node) { - throw new System.NotImplementedException(nameof(WriteCSharpExpression)); + // Render as text node. Later, we'll need to add different handling for expressions + // that appear inside elements, e.g., + context.CodeWriter + .WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.AddText)}"); + + for (var i = 0; i < node.Children.Count; i++) + { + if (node.Children[i] is IntermediateToken token && token.IsCSharp) + { + context.CodeWriter.Write(token.Content); + } + else + { + // There may be something else inside the expression like a Template or another extension node. + throw new NotImplementedException("Unsupported: CSharpExpression with child node that isn't a CSharp node"); + } + } + + context.CodeWriter + .WriteEndMethodInvocation(); } public override void WriteCSharpExpressionAttributeValue(CodeRenderingContext context, CSharpExpressionAttributeValueIntermediateNode node) @@ -53,12 +105,26 @@ namespace Microsoft.Blazor.Build.Core.RazorCompilation.Engine public override void WriteHtmlContent(CodeRenderingContext context, HtmlContentIntermediateNode node) { - context.CodeWriter.Write("/* HTML content */"); + context.CodeWriter + .WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.AddText)}") + .WriteStringLiteral(GetContent(node)) + .WriteEndMethodInvocation(); } public override void WriteUsingDirective(CodeRenderingContext context, UsingDirectiveIntermediateNode node) { throw new System.NotImplementedException(nameof(WriteUsingDirective)); } + + private static string GetContent(HtmlContentIntermediateNode node) + { + var builder = new StringBuilder(); + var htmlTokens = node.Children.OfType().Where(t => t.IsHtml); + foreach (var htmlToken in htmlTokens) + { + builder.Append(htmlToken.Content); + } + return builder.ToString(); + } } } diff --git a/src/Microsoft.Blazor.Build/Core/RazorCompilation/Engine/BlazorRazorEngine.cs b/src/Microsoft.Blazor.Build/Core/RazorCompilation/Engine/BlazorRazorEngine.cs index 8fa4b4facb..41865e7522 100644 --- a/src/Microsoft.Blazor.Build/Core/RazorCompilation/Engine/BlazorRazorEngine.cs +++ b/src/Microsoft.Blazor.Build/Core/RazorCompilation/Engine/BlazorRazorEngine.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Extensions; using Microsoft.Blazor.Components; using System.Linq; @@ -21,6 +22,8 @@ namespace Microsoft.Blazor.Build.Core.RazorCompilation.Engine _engine = RazorEngine.Create(configure => { + FunctionsDirective.Register(configure); + configure.SetBaseType(typeof(BlazorComponent).FullName); configure.Phases.Remove( diff --git a/src/Microsoft.Blazor.Build/Core/RazorCompilation/Engine/CodeWriterExtensions.cs b/src/Microsoft.Blazor.Build/Core/RazorCompilation/Engine/CodeWriterExtensions.cs new file mode 100644 index 0000000000..e1c8e3c011 --- /dev/null +++ b/src/Microsoft.Blazor.Build/Core/RazorCompilation/Engine/CodeWriterExtensions.cs @@ -0,0 +1,647 @@ +// 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 Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.CodeGeneration; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; + +// Copied directly from https://github.com/aspnet/Razor/blob/ff40124594b58b17988d50841175430a4b73d1a9/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/CodeWriterExtensions.cs +// (other than the namespace change) because it's internal + +namespace Microsoft.Blazor.Build.Core.RazorCompilation.Engine +{ + internal static class CodeWriterExtensions + { + private const string InstanceMethodFormat = "{0}.{1}"; + + private static readonly char[] CStyleStringLiteralEscapeChars = + { + '\r', + '\t', + '\"', + '\'', + '\\', + '\0', + '\n', + '\u2028', + '\u2029', + }; + + public static bool IsAtBeginningOfLine(this CodeWriter writer) + { + return writer.Length == 0 || writer[writer.Length - 1] == '\n'; + } + + public static CodeWriter WritePadding(this CodeWriter writer, int offset, SourceSpan? span, CodeRenderingContext context) + { + if (span == null) + { + return writer; + } + + var basePadding = CalculatePadding(); + var resolvedPadding = Math.Max(basePadding - offset, 0); + + if (context.Options.IndentWithTabs) + { + // Avoid writing directly to the StringBuilder here, that will throw off the manual indexing + // done by the base class. + var tabs = resolvedPadding / context.Options.IndentSize; + for (var i = 0; i < tabs; i++) + { + writer.Write("\t"); + } + + var spaces = resolvedPadding % context.Options.IndentSize; + for (var i = 0; i < spaces; i++) + { + writer.Write(" "); + } + } + else + { + for (var i = 0; i < resolvedPadding; i++) + { + writer.Write(" "); + } + } + + return writer; + + int CalculatePadding() + { + var spaceCount = 0; + for (var i = span.Value.AbsoluteIndex - 1; i >= 0; i--) + { + var @char = context.SourceDocument[i]; + if (@char == '\n' || @char == '\r') + { + break; + } + else if (@char == '\t') + { + spaceCount += context.Options.IndentSize; + } + else + { + spaceCount++; + } + } + + return spaceCount; + } + } + + public static CodeWriter WriteVariableDeclaration(this CodeWriter writer, string type, string name, string value) + { + writer.Write(type).Write(" ").Write(name); + if (!string.IsNullOrEmpty(value)) + { + writer.Write(" = ").Write(value); + } + else + { + writer.Write(" = null"); + } + + writer.WriteLine(";"); + + return writer; + } + + public static CodeWriter WriteBooleanLiteral(this CodeWriter writer, bool value) + { + return writer.Write(value.ToString().ToLowerInvariant()); + } + + public static CodeWriter WriteStartAssignment(this CodeWriter writer, string name) + { + return writer.Write(name).Write(" = "); + } + + public static CodeWriter WriteParameterSeparator(this CodeWriter writer) + { + return writer.Write(", "); + } + + public static CodeWriter WriteStartNewObject(this CodeWriter writer, string typeName) + { + return writer.Write("new ").Write(typeName).Write("("); + } + + public static CodeWriter WriteStringLiteral(this CodeWriter writer, string literal) + { + if (literal.Length >= 256 && literal.Length <= 1500 && literal.IndexOf('\0') == -1) + { + WriteVerbatimStringLiteral(writer, literal); + } + else + { + WriteCStyleStringLiteral(writer, literal); + } + + return writer; + } + + public static CodeWriter WriteUsing(this CodeWriter writer, string name) + { + return WriteUsing(writer, name, endLine: true); + } + + public static CodeWriter WriteUsing(this CodeWriter writer, string name, bool endLine) + { + writer.Write("using "); + writer.Write(name); + + if (endLine) + { + writer.WriteLine(";"); + } + + return writer; + } + + public static CodeWriter WriteLineNumberDirective(this CodeWriter writer, SourceSpan span) + { + if (writer.Length >= writer.NewLine.Length && !IsAtBeginningOfLine(writer)) + { + writer.WriteLine(); + } + + var lineNumberAsString = (span.LineIndex + 1).ToString(CultureInfo.InvariantCulture); + return writer.Write("#line ").Write(lineNumberAsString).Write(" \"").Write(span.FilePath).WriteLine("\""); + } + + public static CodeWriter WriteStartMethodInvocation(this CodeWriter writer, string methodName) + { + writer.Write(methodName); + + return writer.Write("("); + } + + public static CodeWriter WriteEndMethodInvocation(this CodeWriter writer) + { + return WriteEndMethodInvocation(writer, endLine: true); + } + + public static CodeWriter WriteEndMethodInvocation(this CodeWriter writer, bool endLine) + { + writer.Write(")"); + if (endLine) + { + writer.WriteLine(";"); + } + + return writer; + } + + // Writes a method invocation for the given instance name. + public static CodeWriter WriteInstanceMethodInvocation( + this CodeWriter writer, + string instanceName, + string methodName, + params string[] parameters) + { + if (instanceName == null) + { + throw new ArgumentNullException(nameof(instanceName)); + } + + if (methodName == null) + { + throw new ArgumentNullException(nameof(methodName)); + } + + return WriteInstanceMethodInvocation(writer, instanceName, methodName, endLine: true, parameters: parameters); + } + + // Writes a method invocation for the given instance name. + public static CodeWriter WriteInstanceMethodInvocation( + this CodeWriter writer, + string instanceName, + string methodName, + bool endLine, + params string[] parameters) + { + if (instanceName == null) + { + throw new ArgumentNullException(nameof(instanceName)); + } + + if (methodName == null) + { + throw new ArgumentNullException(nameof(methodName)); + } + + return WriteMethodInvocation( + writer, + string.Format(CultureInfo.InvariantCulture, InstanceMethodFormat, instanceName, methodName), + endLine, + parameters); + } + + public static CodeWriter WriteStartInstanceMethodInvocation(this CodeWriter writer, string instanceName, string methodName) + { + if (instanceName == null) + { + throw new ArgumentNullException(nameof(instanceName)); + } + + if (methodName == null) + { + throw new ArgumentNullException(nameof(methodName)); + } + + return WriteStartMethodInvocation( + writer, + string.Format(CultureInfo.InvariantCulture, InstanceMethodFormat, instanceName, methodName)); + } + + public static CodeWriter WriteField(this CodeWriter writer, IList modifiers, string typeName, string fieldName) + { + if (modifiers == null) + { + throw new ArgumentNullException(nameof(modifiers)); + } + + if (typeName == null) + { + throw new ArgumentNullException(nameof(typeName)); + } + + if (fieldName == null) + { + throw new ArgumentNullException(nameof(fieldName)); + } + + for (var i = 0; i < modifiers.Count; i++) + { + writer.Write(modifiers[i]); + writer.Write(" "); + } + + writer.Write(typeName); + writer.Write(" "); + writer.Write(fieldName); + writer.Write(";"); + writer.WriteLine(); + + return writer; + } + + public static CodeWriter WriteMethodInvocation(this CodeWriter writer, string methodName, params string[] parameters) + { + return WriteMethodInvocation(writer, methodName, endLine: true, parameters: parameters); + } + + public static CodeWriter WriteMethodInvocation(this CodeWriter writer, string methodName, bool endLine, params string[] parameters) + { + return + WriteStartMethodInvocation(writer, methodName) + .Write(string.Join(", ", parameters)) + .WriteEndMethodInvocation(endLine); + } + + public static CodeWriter WriteAutoPropertyDeclaration(this CodeWriter writer, IList modifiers, string typeName, string propertyName) + { + if (modifiers == null) + { + throw new ArgumentNullException(nameof(modifiers)); + } + + if (typeName == null) + { + throw new ArgumentNullException(nameof(typeName)); + } + + if (propertyName == null) + { + throw new ArgumentNullException(nameof(propertyName)); + } + + for (var i = 0; i < modifiers.Count; i++) + { + writer.Write(modifiers[i]); + writer.Write(" "); + } + + writer.Write(typeName); + writer.Write(" "); + writer.Write(propertyName); + writer.Write(" { get; set; }"); + writer.WriteLine(); + + return writer; + } + + public static CSharpCodeWritingScope BuildScope(this CodeWriter writer) + { + return new CSharpCodeWritingScope(writer); + } + + public static CSharpCodeWritingScope BuildLambda(this CodeWriter writer, params string[] parameterNames) + { + return BuildLambda(writer, async: false, parameterNames: parameterNames); + } + + public static CSharpCodeWritingScope BuildAsyncLambda(this CodeWriter writer, params string[] parameterNames) + { + return BuildLambda(writer, async: true, parameterNames: parameterNames); + } + + private static CSharpCodeWritingScope BuildLambda(CodeWriter writer, bool async, string[] parameterNames) + { + if (async) + { + writer.Write("async"); + } + + writer.Write("(").Write(string.Join(", ", parameterNames)).Write(") => "); + + var scope = new CSharpCodeWritingScope(writer); + + return scope; + } + + public static CSharpCodeWritingScope BuildNamespace(this CodeWriter writer, string name) + { + writer.Write("namespace ").WriteLine(name); + + return new CSharpCodeWritingScope(writer); + } + + public static CSharpCodeWritingScope BuildClassDeclaration( + this CodeWriter writer, + IList modifiers, + string name, + string baseType, + IEnumerable interfaces) + { + for (var i = 0; i < modifiers.Count; i++) + { + writer.Write(modifiers[i]); + writer.Write(" "); + } + + writer.Write("class "); + writer.Write(name); + + var hasBaseType = !string.IsNullOrEmpty(baseType); + var hasInterfaces = interfaces != null && interfaces.Count() > 0; + + if (hasBaseType || hasInterfaces) + { + writer.Write(" : "); + + if (hasBaseType) + { + writer.Write(baseType); + + if (hasInterfaces) + { + WriteParameterSeparator(writer); + } + } + + if (hasInterfaces) + { + writer.Write(string.Join(", ", interfaces)); + } + } + + writer.WriteLine(); + + return new CSharpCodeWritingScope(writer); + } + + public static CSharpCodeWritingScope BuildMethodDeclaration( + this CodeWriter writer, + string accessibility, + string returnType, + string name, + IEnumerable> parameters) + { + writer.Write(accessibility) + .Write(" ") + .Write(returnType) + .Write(" ") + .Write(name) + .Write("(") + .Write(string.Join(", ", parameters.Select(p => p.Key + " " + p.Value))) + .WriteLine(")"); + + return new CSharpCodeWritingScope(writer); + } + + public static IDisposable BuildLinePragma(this CodeWriter writer, SourceSpan? span) + { + if (string.IsNullOrEmpty(span?.FilePath)) + { + // Can't build a valid line pragma without a file path. + return NullDisposable.Default; + } + + return new LinePragmaWriter(writer, span.Value); + } + + private static void WriteVerbatimStringLiteral(CodeWriter writer, string literal) + { + writer.Write("@\""); + + // We need to suppress indenting during the writing of the string's content. A + // verbatim string literal could contain newlines that don't get escaped. + var indent = writer.CurrentIndent; + writer.CurrentIndent = 0; + + // We need to find the index of each '"' (double-quote) to escape it. + var start = 0; + int end; + while ((end = literal.IndexOf('\"', start)) > -1) + { + writer.Write(literal, start, end - start); + + writer.Write("\"\""); + + start = end + 1; + } + + Debug.Assert(end == -1); // We've hit all of the double-quotes. + + // Write the remainder after the last double-quote. + writer.Write(literal, start, literal.Length - start); + + writer.Write("\""); + + writer.CurrentIndent = indent; + } + + private static void WriteCStyleStringLiteral(CodeWriter writer, string literal) + { + // From CSharpCodeGenerator.QuoteSnippetStringCStyle in CodeDOM + writer.Write("\""); + + // We need to find the index of each escapable character to escape it. + var start = 0; + int end; + while ((end = literal.IndexOfAny(CStyleStringLiteralEscapeChars, start)) > -1) + { + writer.Write(literal, start, end - start); + + switch (literal[end]) + { + case '\r': + writer.Write("\\r"); + break; + case '\t': + writer.Write("\\t"); + break; + case '\"': + writer.Write("\\\""); + break; + case '\'': + writer.Write("\\\'"); + break; + case '\\': + writer.Write("\\\\"); + break; + case '\0': + writer.Write("\\\0"); + break; + case '\n': + writer.Write("\\n"); + break; + case '\u2028': + case '\u2029': + writer.Write("\\u"); + writer.Write(((int)literal[end]).ToString("X4", CultureInfo.InvariantCulture)); + break; + default: + Debug.Assert(false, "Unknown escape character."); + break; + } + + start = end + 1; + } + + Debug.Assert(end == -1); // We've hit all of chars that need escaping. + + // Write the remainder after the last escaped char. + writer.Write(literal, start, literal.Length - start); + + writer.Write("\""); + } + + public struct CSharpCodeWritingScope : IDisposable + { + private CodeWriter _writer; + private bool _autoSpace; + private int _tabSize; + private int _startIndent; + + public CSharpCodeWritingScope(CodeWriter writer, int tabSize = 4, bool autoSpace = true) + { + _writer = writer; + _autoSpace = true; + _tabSize = tabSize; + _startIndent = -1; // Set in WriteStartScope + + WriteStartScope(); + } + + public void Dispose() + { + WriteEndScope(); + } + + private void WriteStartScope() + { + TryAutoSpace(" "); + + _writer.WriteLine("{"); + _writer.CurrentIndent += _tabSize; + _startIndent = _writer.CurrentIndent; + } + + private void WriteEndScope() + { + TryAutoSpace(_writer.NewLine); + + // Ensure the scope hasn't been modified + if (_writer.CurrentIndent == _startIndent) + { + _writer.CurrentIndent -= _tabSize; + } + + _writer.WriteLine("}"); + } + + private void TryAutoSpace(string spaceCharacter) + { + if (_autoSpace && + _writer.Length > 0 && + !char.IsWhiteSpace(_writer[_writer.Length - 1])) + { + _writer.Write(spaceCharacter); + } + } + } + + private class LinePragmaWriter : IDisposable + { + private readonly CodeWriter _writer; + private readonly int _startIndent; + + public LinePragmaWriter(CodeWriter writer, SourceSpan span) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + _writer = writer; + _startIndent = _writer.CurrentIndent; + _writer.CurrentIndent = 0; + WriteLineNumberDirective(writer, span); + } + + public void Dispose() + { + // Need to add an additional line at the end IF there wasn't one already written. + // This is needed to work with the C# editor's handling of #line ... + var endsWithNewline = _writer.Length > 0 && _writer[_writer.Length - 1] == '\n'; + + // Always write at least 1 empty line to potentially separate code from pragmas. + _writer.WriteLine(); + + // Check if the previous empty line wasn't enough to separate code from pragmas. + if (!endsWithNewline) + { + _writer.WriteLine(); + } + + _writer + .WriteLine("#line default") + .WriteLine("#line hidden"); + + _writer.CurrentIndent = _startIndent; + } + } + + private class NullDisposable : IDisposable + { + public static readonly NullDisposable Default = new NullDisposable(); + + private NullDisposable() + { + } + + public void Dispose() + { + } + } + } +} diff --git a/test/Microsoft.Blazor.Build.Test/Microsoft.Blazor.Build.Test.csproj b/test/Microsoft.Blazor.Build.Test/Microsoft.Blazor.Build.Test.csproj index 6ca55b304f..60ad93da79 100644 --- a/test/Microsoft.Blazor.Build.Test/Microsoft.Blazor.Build.Test.csproj +++ b/test/Microsoft.Blazor.Build.Test/Microsoft.Blazor.Build.Test.csproj @@ -17,6 +17,9 @@ + + + diff --git a/test/Microsoft.Blazor.Build.Test/RazorCompilerTest.cs b/test/Microsoft.Blazor.Build.Test/RazorCompilerTest.cs index 8273b396e3..5b91c4235b 100644 --- a/test/Microsoft.Blazor.Build.Test/RazorCompilerTest.cs +++ b/test/Microsoft.Blazor.Build.Test/RazorCompilerTest.cs @@ -3,6 +3,9 @@ using Microsoft.Blazor.Build.Core.RazorCompilation; using Microsoft.Blazor.Components; +using Microsoft.Blazor.Rendering; +using Microsoft.Blazor.RenderTree; +using Microsoft.Blazor.Test.Shared; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using System; @@ -65,7 +68,7 @@ namespace Microsoft.Blazor.Build.Test [InlineData("Dir1\\Dir2\\MyFile.cs", "Test.Base.Dir1.Dir2", "MyFile")] public void CreatesClassWithCorrectNameAndNamespace(string relativePath, string expectedNamespace, string expectedClassName) { - // Arrange/Acts + // Arrange/Act var result = CompileToAssembly( "x:\\dir\\subdir", relativePath, @@ -82,6 +85,63 @@ namespace Microsoft.Blazor.Build.Test }); } + [Fact] + public void CanRenderPlainText() + { + // Arrange + var treeBuilder = new RenderTreeBuilder(new TestRenderer()); + + // Arrange/Act + var component = CompileToComponent("Some plain text"); + component.BuildRenderTree(treeBuilder); + + // Assert + Assert.Collection(treeBuilder.GetNodes(), + node => AssertNode.Text(node, "Some plain text")); + } + + [Fact] + public void CanUseCSharpFunctionsBlock() + { + // Arrange/Act + var component = CompileToComponent(@" + @foreach(var item in items) { + @item + } + @functions { + string[] items = new[] { ""First"", ""Second"", ""Third"" }; + } + "); + + // Assert + var nodes = GetRenderTree(component).Where(NotWhitespace); + Assert.Collection(nodes, + node => AssertNode.Text(node, "First"), + node => AssertNode.Text(node, "Second"), + node => AssertNode.Text(node, "Third")); + } + + private static bool NotWhitespace(RenderTreeNode node) + => node.NodeType != RenderTreeNodeType.Text + || !string.IsNullOrWhiteSpace(node.TextContent); + + private static ArraySegment GetRenderTree(IComponent component) + { + var treeBuilder = new RenderTreeBuilder(new TestRenderer()); + component.BuildRenderTree(treeBuilder); + return treeBuilder.GetNodes(); + } + + private static IComponent CompileToComponent(string cshtmlSource) + { + var testComponentTypeName = "TestComponent"; + var testComponentNamespace = "Test"; + var assemblyResult = CompileToAssembly("c:\\ignored", $"{testComponentTypeName}.cshtml", cshtmlSource, testComponentNamespace); + Assert.Empty(assemblyResult.Diagnostics); + var testComponentType = assemblyResult.Assembly.GetType($"{testComponentNamespace}.{testComponentTypeName}"); + return (IComponent)Activator.CreateInstance(testComponentType); + } + private static CompileToAssemblyResult CompileToAssembly(string cshtmlRootPath, string cshtmlRelativePath, string cshtmlContent, string outputNamespace) { var csharpResult = CompileToCSharp(cshtmlRootPath, cshtmlRelativePath, cshtmlContent, outputNamespace); @@ -168,5 +228,11 @@ namespace Microsoft.Blazor.Build.Test public string VerboseLog { get; set; } public IEnumerable Diagnostics { get; set; } } + + private class TestRenderer : Renderer + { + protected override void UpdateDisplay(int componentId, ArraySegment renderTree) + => throw new NotImplementedException(); + } } } diff --git a/test/Microsoft.Blazor.Test/Microsoft.Blazor.Test.csproj b/test/Microsoft.Blazor.Test/Microsoft.Blazor.Test.csproj index 1227b792bf..e2a7e23098 100644 --- a/test/Microsoft.Blazor.Test/Microsoft.Blazor.Test.csproj +++ b/test/Microsoft.Blazor.Test/Microsoft.Blazor.Test.csproj @@ -15,6 +15,9 @@ + + + diff --git a/test/Microsoft.Blazor.Test/RenderTreeBuilderTest.cs b/test/Microsoft.Blazor.Test/RenderTreeBuilderTest.cs index 7bf0e407c5..6102a4ff31 100644 --- a/test/Microsoft.Blazor.Test/RenderTreeBuilderTest.cs +++ b/test/Microsoft.Blazor.Test/RenderTreeBuilderTest.cs @@ -4,6 +4,7 @@ using Microsoft.Blazor.Components; using Microsoft.Blazor.Rendering; using Microsoft.Blazor.RenderTree; +using Microsoft.Blazor.Test.Shared; using System; using System.Linq; using Xunit; diff --git a/test/Microsoft.Blazor.Test/RendererTest.cs b/test/Microsoft.Blazor.Test/RendererTest.cs index f273f8301d..4faf92a5bb 100644 --- a/test/Microsoft.Blazor.Test/RendererTest.cs +++ b/test/Microsoft.Blazor.Test/RendererTest.cs @@ -7,6 +7,7 @@ using System.Linq; using Microsoft.Blazor.Components; using Microsoft.Blazor.Rendering; using Microsoft.Blazor.RenderTree; +using Microsoft.Blazor.Test.Shared; using Xunit; namespace Microsoft.Blazor.Test diff --git a/test/Microsoft.Blazor.Test/AssertNode.cs b/test/shared/AssertNode.cs similarity index 96% rename from test/Microsoft.Blazor.Test/AssertNode.cs rename to test/shared/AssertNode.cs index 6aa2d3a2de..cdd9d27735 100644 --- a/test/Microsoft.Blazor.Test/AssertNode.cs +++ b/test/shared/AssertNode.cs @@ -5,9 +5,9 @@ using Microsoft.Blazor.Components; using Microsoft.Blazor.RenderTree; using Xunit; -namespace Microsoft.Blazor.Test +namespace Microsoft.Blazor.Test.Shared { - public static class AssertNode + internal static class AssertNode { public static void Text(RenderTreeNode node, string textContent) {