Razor compilation: Support text literals and C# code

This commit is contained in:
Steve Sanderson 2018-01-15 22:42:50 +00:00
parent 3ccdc1d16f
commit 7e40427ffe
9 changed files with 796 additions and 6 deletions

View File

@ -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
/// </summary>
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., <a href="@url">
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<IntermediateToken>().Where(t => t.IsHtml);
foreach (var htmlToken in htmlTokens)
{
builder.Append(htmlToken.Content);
}
return builder.ToString();
}
}
}

View File

@ -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(

View File

@ -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<string> 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<string> 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<string> modifiers,
string name,
string baseType,
IEnumerable<string> 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<KeyValuePair<string, string>> 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()
{
}
}
}
}

View File

@ -17,6 +17,9 @@
<ItemGroup>
<ProjectReference Include="..\..\samples\StandaloneApp\StandaloneApp.csproj" />
<ProjectReference Include="..\..\src\Microsoft.Blazor.Build\Microsoft.Blazor.Build.csproj" />
<!-- Shared sources -->
<Compile Include="..\shared\AssertNode.cs" />
</ItemGroup>
</Project>

View File

@ -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<RenderTreeNode> 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<Diagnostic> Diagnostics { get; set; }
}
private class TestRenderer : Renderer
{
protected override void UpdateDisplay(int componentId, ArraySegment<RenderTreeNode> renderTree)
=> throw new NotImplementedException();
}
}
}

View File

@ -15,6 +15,9 @@
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Blazor\Microsoft.Blazor.csproj" />
<!-- Shared sources -->
<Compile Include="..\shared\AssertNode.cs" />
</ItemGroup>
</Project>

View File

@ -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;

View File

@ -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

View File

@ -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)
{