Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow complex content in component parameters #9742

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,6 @@ private static void ProcessAttribute(IntermediateNode parent, IntermediateNode n
// We don't support this.
issueDiagnostic = true;
}
else if (node.Children.Count > 1)
{
// This is the common case for 'mixed' content
issueDiagnostic = true;
}

if (issueDiagnostic)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
using Microsoft.AspNetCore.Razor.Language.Extensions;
Expand All @@ -17,7 +16,7 @@
namespace Microsoft.AspNetCore.Razor.Language.Components;

// Based on the DesignTimeNodeWriter from Razor repo.
internal class ComponentDesignTimeNodeWriter : ComponentNodeWriter
internal sealed class ComponentDesignTimeNodeWriter : ComponentNodeWriter
{
private readonly ScopeStack _scopeStack = new ScopeStack();

Expand Down Expand Up @@ -758,12 +757,6 @@ private void WritePropertyAccess(CodeRenderingContext context, ComponentAttribut

private void WriteComponentAttributeInnards(CodeRenderingContext context, ComponentAttributeIntermediateNode node, bool canTypeCheck)
{
if (node.Children.Count > 1)
{
Debug.Assert(node.HasDiagnostics, "We should have reported an error for mixed content.");
// We render the children anyway, so tooling works.
}

// We limit component attributes to simple cases. However there is still a lot of complexity
// to handle here, since there are a few different cases for how an attribute might be structured.
//
Expand All @@ -788,9 +781,6 @@ private void WriteComponentAttributeInnards(CodeRenderingContext context, Compon
//
// Or a CSharpExpressionIntermediateNode when the attribute has an explicit transition like:
// <MyComponent Value="@value" />
//
// Of a list of tokens directly in the attribute.
var tokens = GetCSharpTokens(node);

if ((node.BoundAttribute?.IsDelegateProperty() ?? false) ||
(node.BoundAttribute?.IsChildContentProperty() ?? false))
Expand All @@ -805,9 +795,9 @@ private void WriteComponentAttributeInnards(CodeRenderingContext context, Compon
}
context.CodeWriter.WriteLine();

for (var i = 0; i < tokens.Count; i++)
foreach (var token in GetCSharpTokens(node))
{
WriteCSharpToken(context, tokens[i]);
WriteCSharpToken(context, token);
}

if (canTypeCheck)
Expand Down Expand Up @@ -861,9 +851,9 @@ private void WriteComponentAttributeInnards(CodeRenderingContext context, Compon

context.CodeWriter.WriteLine();

for (var i = 0; i < tokens.Count; i++)
foreach (var token in GetCSharpTokens(node))
{
WriteCSharpToken(context, tokens[i]);
WriteCSharpToken(context, token);
}

context.CodeWriter.Write(")");
Expand Down Expand Up @@ -895,10 +885,7 @@ private void WriteComponentAttributeInnards(CodeRenderingContext context, Compon
context.CodeWriter.Write("(");
}

for (var i = 0; i < tokens.Count; i++)
{
WriteCSharpToken(context, tokens[i]);
}
WriteAttributeValue(context, node.FindDescendantNodes<IntermediateToken>());

if (canTypeCheck && NeedsTypeCheck(node))
{
Expand Down Expand Up @@ -1131,11 +1118,6 @@ private void WriteSplatInnards(CodeRenderingContext context, SplatIntermediateNo

public sealed override void WriteFormName(CodeRenderingContext context, FormNameIntermediateNode node)
{
if (node.Children.Count > 1)
{
Debug.Assert(node.HasDiagnostics, "We should have reported an error for mixed content.");
}

foreach (var token in node.FindDescendantNodes<IntermediateToken>())
{
if (token.IsCSharp)
Expand Down Expand Up @@ -1247,7 +1229,7 @@ public override void WriteRenderMode(CodeRenderingContext context, RenderModeInt
});
}

private void WriteCSharpToken(CodeRenderingContext context, IntermediateToken token)
protected override void WriteCSharpToken(CodeRenderingContext context, IntermediateToken token)
{
if (string.IsNullOrWhiteSpace(token.Content))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,101 @@ protected static void WriteAddComponentRenderMode(CodeRenderingContext context,
context.CodeWriter.WriteLine();
}

protected abstract void WriteCSharpToken(CodeRenderingContext context, IntermediateToken token);

// There are a few cases here, we need to handle:
// - Pure HTML
// - Pure CSharp
// - Mixed HTML and CSharp
//
// Only the mixed case is complicated, we want to turn it into code that will concatenate
// the values into a string at runtime.
protected void WriteAttributeValue(CodeRenderingContext context, IReadOnlyList<IntermediateToken> tokens)
{
if (tokens == null)
{
throw new ArgumentNullException(nameof(tokens));
}

var writer = context.CodeWriter;
var hasHtml = false;
var hasCSharp = false;
for (var i = 0; i < tokens.Count; i++)
{
if (tokens[i].IsCSharp)
{
hasCSharp |= true;
}
else
{
hasHtml |= true;
}
}

if (hasHtml && hasCSharp)
{
// If it's a C# expression, we have to wrap it in parens, otherwise things like ternary
// expressions don't compose with concatenation. However, this is a little complicated
// because C# tokens themselves aren't guaranteed to be distinct expressions. We want
// to treat all contiguous C# tokens as a single expression.
var insideCSharp = false;
for (var i = 0; i < tokens.Count; i++)
{
var token = tokens[i];
if (token.IsCSharp)
{
if (!insideCSharp)
{
if (i != 0)
{
writer.Write(" + ");
}

writer.Write("(");
insideCSharp = true;
}

WriteCSharpToken(context, token);
}
else
{
if (insideCSharp)
{
writer.Write(")");
insideCSharp = false;
}

if (i != 0)
{
writer.Write(" + ");
}

writer.WriteStringLiteral(token.Content);
}
}

if (insideCSharp)
{
writer.Write(")");
}
}
else if (hasCSharp)
{
foreach (var token in tokens)
{
WriteCSharpToken(context, token);
}
}
else if (hasHtml)
{
writer.WriteStringLiteral(string.Join("", tokens.Select(t => t.Content)));
}
else
{
throw new InvalidOperationException("Found attribute whose value is neither HTML nor CSharp");
}
}

protected class TypeInferenceMethodParameter
{
public string SeqName { get; private set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Components;
/// <summary>
/// Generates the C# code corresponding to Razor source document contents.
/// </summary>
internal class ComponentRuntimeNodeWriter : ComponentNodeWriter
internal sealed class ComponentRuntimeNodeWriter : ComponentNodeWriter
{
private readonly List<IntermediateToken> _currentAttributeValues = new List<IntermediateToken>();
private readonly ScopeStack _scopeStack = new ScopeStack();
Expand Down Expand Up @@ -630,12 +630,6 @@ public override void WriteComponentAttribute(CodeRenderingContext context, Compo

private void WriteComponentAttributeInnards(CodeRenderingContext context, ComponentAttributeIntermediateNode node, bool canTypeCheck)
{
if (node.Children.Count > 1)
{
Debug.Assert(node.HasDiagnostics, "We should have reported an error for mixed content.");
// We render the children anyway, so tooling works.
}

if (node.AttributeStructure == AttributeStructure.Minimized)
{
// Minimized attributes always map to 'true'
Expand All @@ -650,7 +644,6 @@ private void WriteComponentAttributeInnards(CodeRenderingContext context, Compon
else
{
// See comments in ComponentDesignTimeNodeWriter for a description of the cases that are possible.
var tokens = GetCSharpTokens(node);
if ((node.BoundAttribute?.IsDelegateProperty() ?? false) ||
(node.BoundAttribute?.IsChildContentProperty() ?? false))
{
Expand All @@ -662,9 +655,9 @@ private void WriteComponentAttributeInnards(CodeRenderingContext context, Compon
context.CodeWriter.Write("(");
}

for (var i = 0; i < tokens.Count; i++)
foreach (var token in GetCSharpTokens(node))
{
WriteCSharpToken(context, tokens[i]);
WriteCSharpToken(context, token);
}

if (canTypeCheck)
Expand Down Expand Up @@ -711,9 +704,9 @@ private void WriteComponentAttributeInnards(CodeRenderingContext context, Compon
context.CodeWriter.Write("this");
context.CodeWriter.Write(", ");

for (var i = 0; i < tokens.Count; i++)
foreach (var token in GetCSharpTokens(node))
{
WriteCSharpToken(context, tokens[i]);
WriteCSharpToken(context, token);
}

context.CodeWriter.Write(")");
Expand Down Expand Up @@ -742,10 +735,7 @@ private void WriteComponentAttributeInnards(CodeRenderingContext context, Compon
context.CodeWriter.Write("(");
}

for (var i = 0; i < tokens.Count; i++)
{
WriteCSharpToken(context, tokens[i]);
}
WriteAttributeValue(context, node.FindDescendantNodes<IntermediateToken>());

if (canTypeCheck && NeedsTypeCheck(node))
{
Expand Down Expand Up @@ -945,11 +935,6 @@ private void WriteSplatInnards(CodeRenderingContext context, SplatIntermediateNo

public sealed override void WriteFormName(CodeRenderingContext context, FormNameIntermediateNode node)
{
if (node.Children.Count > 1)
{
Debug.Assert(node.HasDiagnostics, "We should have reported an error for mixed content.");
}

// string __formName = expression;
context.CodeWriter.Write("string ");
context.CodeWriter.Write(_scopeStack.FormNameVarName);
Expand Down Expand Up @@ -1103,98 +1088,9 @@ private static string GetHtmlContent(HtmlContentIntermediateNode node)
return builder.ToString();
}

// There are a few cases here, we need to handle:
// - Pure HTML
// - Pure CSharp
// - Mixed HTML and CSharp
//
// Only the mixed case is complicated, we want to turn it into code that will concatenate
// the values into a string at runtime.

private static void WriteAttributeValue(CodeRenderingContext context, IReadOnlyList<IntermediateToken> tokens)
protected override void WriteCSharpToken(CodeRenderingContext context, IntermediateToken token)
{
if (tokens == null)
{
throw new ArgumentNullException(nameof(tokens));
}

var writer = context.CodeWriter;
var hasHtml = false;
var hasCSharp = false;
for (var i = 0; i < tokens.Count; i++)
{
if (tokens[i].IsCSharp)
{
hasCSharp |= true;
}
else
{
hasHtml |= true;
}
}

if (hasHtml && hasCSharp)
{
// If it's a C# expression, we have to wrap it in parens, otherwise things like ternary
// expressions don't compose with concatenation. However, this is a little complicated
// because C# tokens themselves aren't guaranteed to be distinct expressions. We want
// to treat all contiguous C# tokens as a single expression.
var insideCSharp = false;
for (var i = 0; i < tokens.Count; i++)
{
var token = tokens[i];
if (token.IsCSharp)
{
if (!insideCSharp)
{
if (i != 0)
{
writer.Write(" + ");
}

writer.Write("(");
insideCSharp = true;
}

WriteCSharpToken(context, token);
}
else
{
if (insideCSharp)
{
writer.Write(")");
insideCSharp = false;
}

if (i != 0)
{
writer.Write(" + ");
}

writer.WriteStringLiteral(token.Content);
}
}

if (insideCSharp)
{
writer.Write(")");
}
}
else if (hasCSharp)
{
foreach (var token in tokens)
{
WriteCSharpToken(context, token);
}
}
else if (hasHtml)
{
writer.WriteStringLiteral(string.Join("", tokens.Select(t => t.Content)));
}
else
{
throw new InvalidOperationException("Found attribute whose value is neither HTML nor CSharp");
}
WriteCSharpToken(context, token, includeLinePragma: true);
}

private static void WriteCSharpToken(CodeRenderingContext context, IntermediateToken token, bool includeLinePragma = true)
Expand Down
Loading