Razor compilation: Support text literals and C# code
This commit is contained in:
parent
3ccdc1d16f
commit
7e40427ffe
|
|
@ -3,6 +3,10 @@
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
|
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
|
||||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
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
|
namespace Microsoft.Blazor.Build.Core.RazorCompilation.Engine
|
||||||
{
|
{
|
||||||
|
|
@ -11,6 +15,8 @@ namespace Microsoft.Blazor.Build.Core.RazorCompilation.Engine
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal class BlazorIntermediateNodeWriter : IntermediateNodeWriter
|
internal class BlazorIntermediateNodeWriter : IntermediateNodeWriter
|
||||||
{
|
{
|
||||||
|
private const string builderVarName = "builder";
|
||||||
|
|
||||||
public override void BeginWriterScope(CodeRenderingContext context, string writer)
|
public override void BeginWriterScope(CodeRenderingContext context, string writer)
|
||||||
{
|
{
|
||||||
throw new System.NotImplementedException(nameof(BeginWriterScope));
|
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)
|
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)
|
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)
|
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)
|
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)
|
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)
|
public override void WriteUsingDirective(CodeRenderingContext context, UsingDirectiveIntermediateNode node)
|
||||||
{
|
{
|
||||||
throw new System.NotImplementedException(nameof(WriteUsingDirective));
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// 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;
|
||||||
|
using Microsoft.AspNetCore.Razor.Language.Extensions;
|
||||||
using Microsoft.Blazor.Components;
|
using Microsoft.Blazor.Components;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
|
|
@ -21,6 +22,8 @@ namespace Microsoft.Blazor.Build.Core.RazorCompilation.Engine
|
||||||
|
|
||||||
_engine = RazorEngine.Create(configure =>
|
_engine = RazorEngine.Create(configure =>
|
||||||
{
|
{
|
||||||
|
FunctionsDirective.Register(configure);
|
||||||
|
|
||||||
configure.SetBaseType(typeof(BlazorComponent).FullName);
|
configure.SetBaseType(typeof(BlazorComponent).FullName);
|
||||||
|
|
||||||
configure.Phases.Remove(
|
configure.Phases.Remove(
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,6 +17,9 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\samples\StandaloneApp\StandaloneApp.csproj" />
|
<ProjectReference Include="..\..\samples\StandaloneApp\StandaloneApp.csproj" />
|
||||||
<ProjectReference Include="..\..\src\Microsoft.Blazor.Build\Microsoft.Blazor.Build.csproj" />
|
<ProjectReference Include="..\..\src\Microsoft.Blazor.Build\Microsoft.Blazor.Build.csproj" />
|
||||||
|
|
||||||
|
<!-- Shared sources -->
|
||||||
|
<Compile Include="..\shared\AssertNode.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@
|
||||||
|
|
||||||
using Microsoft.Blazor.Build.Core.RazorCompilation;
|
using Microsoft.Blazor.Build.Core.RazorCompilation;
|
||||||
using Microsoft.Blazor.Components;
|
using Microsoft.Blazor.Components;
|
||||||
|
using Microsoft.Blazor.Rendering;
|
||||||
|
using Microsoft.Blazor.RenderTree;
|
||||||
|
using Microsoft.Blazor.Test.Shared;
|
||||||
using Microsoft.CodeAnalysis;
|
using Microsoft.CodeAnalysis;
|
||||||
using Microsoft.CodeAnalysis.CSharp;
|
using Microsoft.CodeAnalysis.CSharp;
|
||||||
using System;
|
using System;
|
||||||
|
|
@ -65,7 +68,7 @@ namespace Microsoft.Blazor.Build.Test
|
||||||
[InlineData("Dir1\\Dir2\\MyFile.cs", "Test.Base.Dir1.Dir2", "MyFile")]
|
[InlineData("Dir1\\Dir2\\MyFile.cs", "Test.Base.Dir1.Dir2", "MyFile")]
|
||||||
public void CreatesClassWithCorrectNameAndNamespace(string relativePath, string expectedNamespace, string expectedClassName)
|
public void CreatesClassWithCorrectNameAndNamespace(string relativePath, string expectedNamespace, string expectedClassName)
|
||||||
{
|
{
|
||||||
// Arrange/Acts
|
// Arrange/Act
|
||||||
var result = CompileToAssembly(
|
var result = CompileToAssembly(
|
||||||
"x:\\dir\\subdir",
|
"x:\\dir\\subdir",
|
||||||
relativePath,
|
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)
|
private static CompileToAssemblyResult CompileToAssembly(string cshtmlRootPath, string cshtmlRelativePath, string cshtmlContent, string outputNamespace)
|
||||||
{
|
{
|
||||||
var csharpResult = CompileToCSharp(cshtmlRootPath, cshtmlRelativePath, cshtmlContent, outputNamespace);
|
var csharpResult = CompileToCSharp(cshtmlRootPath, cshtmlRelativePath, cshtmlContent, outputNamespace);
|
||||||
|
|
@ -168,5 +228,11 @@ namespace Microsoft.Blazor.Build.Test
|
||||||
public string VerboseLog { get; set; }
|
public string VerboseLog { get; set; }
|
||||||
public IEnumerable<Diagnostic> Diagnostics { get; set; }
|
public IEnumerable<Diagnostic> Diagnostics { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class TestRenderer : Renderer
|
||||||
|
{
|
||||||
|
protected override void UpdateDisplay(int componentId, ArraySegment<RenderTreeNode> renderTree)
|
||||||
|
=> throw new NotImplementedException();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,9 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\src\Microsoft.Blazor\Microsoft.Blazor.csproj" />
|
<ProjectReference Include="..\..\src\Microsoft.Blazor\Microsoft.Blazor.csproj" />
|
||||||
|
|
||||||
|
<!-- Shared sources -->
|
||||||
|
<Compile Include="..\shared\AssertNode.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
using Microsoft.Blazor.Components;
|
using Microsoft.Blazor.Components;
|
||||||
using Microsoft.Blazor.Rendering;
|
using Microsoft.Blazor.Rendering;
|
||||||
using Microsoft.Blazor.RenderTree;
|
using Microsoft.Blazor.RenderTree;
|
||||||
|
using Microsoft.Blazor.Test.Shared;
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ using System.Linq;
|
||||||
using Microsoft.Blazor.Components;
|
using Microsoft.Blazor.Components;
|
||||||
using Microsoft.Blazor.Rendering;
|
using Microsoft.Blazor.Rendering;
|
||||||
using Microsoft.Blazor.RenderTree;
|
using Microsoft.Blazor.RenderTree;
|
||||||
|
using Microsoft.Blazor.Test.Shared;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.Blazor.Test
|
namespace Microsoft.Blazor.Test
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,9 @@ using Microsoft.Blazor.Components;
|
||||||
using Microsoft.Blazor.RenderTree;
|
using Microsoft.Blazor.RenderTree;
|
||||||
using Xunit;
|
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)
|
public static void Text(RenderTreeNode node, string textContent)
|
||||||
{
|
{
|
||||||
Loading…
Reference in New Issue