Code dump of Blazor compiler BindTagHelperDescriptorProvider
This change adds all of the code from the Blazor/Components compiler into
the Razor language assembly. Minimal refactoring or integration work has
been done, that is next :)
All of the code compiles and all unit tests pass, except the one I had
to skip until integration work is done.
\n\nCommit migrated from aa5c82ca5e
This commit is contained in:
parent
c37dada0dc
commit
51d19d1745
|
|
@ -0,0 +1,517 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Razor.Language.Extensions;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language.Components
|
||||
{
|
||||
internal class BindLoweringPass : IntermediateNodePassBase, IRazorOptimizationPass
|
||||
{
|
||||
// Run after event handler pass
|
||||
public override int Order => 100;
|
||||
|
||||
protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
|
||||
{
|
||||
var @namespace = documentNode.FindPrimaryNamespace();
|
||||
var @class = documentNode.FindPrimaryClass();
|
||||
if (@namespace == null || @class == null)
|
||||
{
|
||||
// Nothing to do, bail. We can't function without the standard structure.
|
||||
return;
|
||||
}
|
||||
|
||||
// For each bind *usage* we need to rewrite the tag helper node to map to basic constructs.
|
||||
var references = documentNode.FindDescendantReferences<TagHelperPropertyIntermediateNode>();
|
||||
|
||||
var parents = new HashSet<IntermediateNode>();
|
||||
for (var i = 0; i < references.Count; i++)
|
||||
{
|
||||
parents.Add(references[i].Parent);
|
||||
}
|
||||
|
||||
foreach (var parent in parents)
|
||||
{
|
||||
ProcessDuplicates(parent);
|
||||
}
|
||||
|
||||
for (var i = 0; i < references.Count; i++)
|
||||
{
|
||||
var reference = references[i];
|
||||
var node = (TagHelperPropertyIntermediateNode)reference.Node;
|
||||
|
||||
if (!reference.Parent.Children.Contains(node))
|
||||
{
|
||||
// This node was removed as a duplicate, skip it.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (node.TagHelper.IsBindTagHelper() && node.AttributeName.StartsWith("bind"))
|
||||
{
|
||||
// Workaround for https://github.com/aspnet/Blazor/issues/703
|
||||
var rewritten = RewriteUsage(reference.Parent, node);
|
||||
reference.Remove();
|
||||
|
||||
for (var j = 0; j < rewritten.Length; j++)
|
||||
{
|
||||
reference.Parent.Children.Add(rewritten[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessDuplicates(IntermediateNode node)
|
||||
{
|
||||
// Reverse order because we will remove nodes.
|
||||
//
|
||||
// Each 'property' node could be duplicated if there are multiple tag helpers that match that
|
||||
// particular attribute. This is common in our approach, which relies on 'fallback' tag helpers
|
||||
// that overlap with more specific ones.
|
||||
for (var i = node.Children.Count - 1; i >= 0; i--)
|
||||
{
|
||||
// For each usage of the general 'fallback' bind tag helper, it could duplicate
|
||||
// the usage of a more specific one. Look for duplicates and remove the fallback.
|
||||
var attribute = node.Children[i] as TagHelperPropertyIntermediateNode;
|
||||
if (attribute != null &&
|
||||
attribute.TagHelper != null &&
|
||||
attribute.TagHelper.IsFallbackBindTagHelper())
|
||||
{
|
||||
for (var j = 0; j < node.Children.Count; j++)
|
||||
{
|
||||
var duplicate = node.Children[j] as TagHelperPropertyIntermediateNode;
|
||||
if (duplicate != null &&
|
||||
duplicate.TagHelper != null &&
|
||||
duplicate.TagHelper.IsBindTagHelper() &&
|
||||
duplicate.AttributeName == attribute.AttributeName &&
|
||||
!object.ReferenceEquals(attribute, duplicate))
|
||||
{
|
||||
// Found a duplicate - remove the 'fallback' in favor of the
|
||||
// more specific tag helper.
|
||||
node.Children.RemoveAt(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also treat the general <input bind="..." /> as a 'fallback' for that case and remove it.
|
||||
// This is a workaround for a limitation where you can't write a tag helper that binds only
|
||||
// when a specific attribute is **not** present.
|
||||
if (attribute != null &&
|
||||
attribute.TagHelper != null &&
|
||||
attribute.TagHelper.IsInputElementFallbackBindTagHelper())
|
||||
{
|
||||
for (var j = 0; j < node.Children.Count; j++)
|
||||
{
|
||||
var duplicate = node.Children[j] as TagHelperPropertyIntermediateNode;
|
||||
if (duplicate != null &&
|
||||
duplicate.TagHelper != null &&
|
||||
duplicate.TagHelper.IsInputElementBindTagHelper() &&
|
||||
duplicate.AttributeName == attribute.AttributeName &&
|
||||
!object.ReferenceEquals(attribute, duplicate))
|
||||
{
|
||||
// Found a duplicate - remove the 'fallback' input tag helper in favor of the
|
||||
// more specific tag helper.
|
||||
node.Children.RemoveAt(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we still have duplicates at this point then they are genuine conflicts.
|
||||
var duplicates = node.Children
|
||||
.OfType<TagHelperPropertyIntermediateNode>()
|
||||
.GroupBy(p => p.AttributeName)
|
||||
.Where(g => g.Count() > 1);
|
||||
|
||||
foreach (var duplicate in duplicates)
|
||||
{
|
||||
node.Diagnostics.Add(ComponentDiagnosticFactory.CreateBindAttribute_Duplicates(
|
||||
node.Source,
|
||||
duplicate.Key,
|
||||
duplicate.ToArray()));
|
||||
foreach (var property in duplicate)
|
||||
{
|
||||
node.Children.Remove(property);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IntermediateNode[] RewriteUsage(IntermediateNode parent, TagHelperPropertyIntermediateNode node)
|
||||
{
|
||||
// Bind works similarly to a macro, it always expands to code that the user could have written.
|
||||
//
|
||||
// For the nodes that are related to the bind-attribute rewrite them to look like a pair of
|
||||
// 'normal' HTML attributes similar to the following transformation.
|
||||
//
|
||||
// Input: <MyComponent bind-Value="@currentCount" />
|
||||
// Output: <MyComponent Value ="...<get the value>..." ValueChanged ="... <set the value>..." />
|
||||
//
|
||||
// This means that the expression that appears inside of 'bind' must be an LValue or else
|
||||
// there will be errors. In general the errors that come from C# in this case are good enough
|
||||
// to understand the problem.
|
||||
//
|
||||
// The BindMethods calls are required in this case because to give us a good experience. They
|
||||
// use overloading to ensure that can get an Action<object> that will convert and set an arbitrary
|
||||
// value.
|
||||
//
|
||||
// We also assume that the element will be treated as a component for now because
|
||||
// multiple passes handle 'special' tag helpers. We have another pass that translates
|
||||
// a tag helper node back into 'regular' element when it doesn't have an associated component
|
||||
if (!TryComputeAttributeNames(
|
||||
parent,
|
||||
node,
|
||||
node.AttributeName,
|
||||
out var valueAttributeName,
|
||||
out var changeAttributeName,
|
||||
out var valueAttribute,
|
||||
out var changeAttribute))
|
||||
{
|
||||
// Skip anything we can't understand. It's important that we don't crash, that will bring down
|
||||
// the build.
|
||||
node.Diagnostics.Add(ComponentDiagnosticFactory.CreateBindAttribute_InvalidSyntax(
|
||||
node.Source,
|
||||
node.AttributeName));
|
||||
return new[] { node };
|
||||
}
|
||||
|
||||
var original = GetAttributeContent(node);
|
||||
if (string.IsNullOrEmpty(original.Content))
|
||||
{
|
||||
// This can happen in error cases, the parser will already have flagged this
|
||||
// as an error, so ignore it.
|
||||
return new[] { node };
|
||||
}
|
||||
|
||||
// Look for a matching format node. If we find one then we need to pass the format into the
|
||||
// two nodes we generate.
|
||||
IntermediateToken format = null;
|
||||
if (TryGetFormatNode(
|
||||
parent,
|
||||
node,
|
||||
valueAttributeName,
|
||||
out var formatNode))
|
||||
{
|
||||
// Don't write the format out as its own attribute, just capture it as a string
|
||||
// or expression.
|
||||
parent.Children.Remove(formatNode);
|
||||
format = GetAttributeContent(formatNode);
|
||||
}
|
||||
|
||||
// Now rewrite the content of the value node to look like:
|
||||
//
|
||||
// BindMethods.GetValue(<code>) OR
|
||||
// BindMethods.GetValue(<code>, <format>)
|
||||
var valueExpressionTokens = new List<IntermediateToken>();
|
||||
valueExpressionTokens.Add(new IntermediateToken()
|
||||
{
|
||||
Content = $"{ComponentsApi.BindMethods.GetValue}(",
|
||||
Kind = TokenKind.CSharp
|
||||
});
|
||||
valueExpressionTokens.Add(original);
|
||||
if (!string.IsNullOrEmpty(format?.Content))
|
||||
{
|
||||
valueExpressionTokens.Add(new IntermediateToken()
|
||||
{
|
||||
Content = ", ",
|
||||
Kind = TokenKind.CSharp,
|
||||
});
|
||||
valueExpressionTokens.Add(format);
|
||||
}
|
||||
valueExpressionTokens.Add(new IntermediateToken()
|
||||
{
|
||||
Content = ")",
|
||||
Kind = TokenKind.CSharp,
|
||||
});
|
||||
|
||||
// Now rewrite the content of the change-handler node. There are two cases we care about
|
||||
// here. If it's a component attribute, then don't use the 'BindMethods wrapper. We expect
|
||||
// component attributes to always 'match' on type.
|
||||
//
|
||||
// __value => <code> = __value
|
||||
//
|
||||
// For general DOM attributes, we need to be able to create a delegate that accepts UIEventArgs
|
||||
// so we use BindMethods.SetValueHandler
|
||||
//
|
||||
// BindMethods.SetValueHandler(__value => <code> = __value, <code>) OR
|
||||
// BindMethods.SetValueHandler(__value => <code> = __value, <code>, <format>)
|
||||
//
|
||||
// Note that the linemappings here are applied to the value attribute, not the change attribute.
|
||||
|
||||
string changeExpressionContent = null;
|
||||
if (changeAttribute == null && format == null)
|
||||
{
|
||||
changeExpressionContent = $"{ComponentsApi.BindMethods.SetValueHandler}(__value => {original.Content} = __value, {original.Content})";
|
||||
}
|
||||
else if (changeAttribute == null && format != null)
|
||||
{
|
||||
changeExpressionContent = $"{ComponentsApi.BindMethods.SetValueHandler}(__value => {original.Content} = __value, {original.Content}, {format.Content})";
|
||||
}
|
||||
else
|
||||
{
|
||||
changeExpressionContent = $"__value => {original.Content} = __value";
|
||||
}
|
||||
var changeExpressionTokens = new List<IntermediateToken>()
|
||||
{
|
||||
new IntermediateToken()
|
||||
{
|
||||
Content = changeExpressionContent,
|
||||
Kind = TokenKind.CSharp
|
||||
}
|
||||
};
|
||||
|
||||
if (parent is HtmlElementIntermediateNode)
|
||||
{
|
||||
var valueNode = new HtmlAttributeIntermediateNode()
|
||||
{
|
||||
AttributeName = valueAttributeName,
|
||||
Source = node.Source,
|
||||
|
||||
Prefix = valueAttributeName + "=\"",
|
||||
Suffix = "\"",
|
||||
};
|
||||
|
||||
for (var i = 0; i < node.Diagnostics.Count; i++)
|
||||
{
|
||||
valueNode.Diagnostics.Add(node.Diagnostics[i]);
|
||||
}
|
||||
|
||||
valueNode.Children.Add(new CSharpExpressionAttributeValueIntermediateNode());
|
||||
for (var i = 0; i < valueExpressionTokens.Count; i++)
|
||||
{
|
||||
valueNode.Children[0].Children.Add(valueExpressionTokens[i]);
|
||||
}
|
||||
|
||||
var changeNode = new HtmlAttributeIntermediateNode()
|
||||
{
|
||||
AttributeName = changeAttributeName,
|
||||
Source = node.Source,
|
||||
|
||||
Prefix = changeAttributeName + "=\"",
|
||||
Suffix = "\"",
|
||||
};
|
||||
|
||||
changeNode.Children.Add(new CSharpExpressionAttributeValueIntermediateNode());
|
||||
for (var i = 0; i < changeExpressionTokens.Count; i++)
|
||||
{
|
||||
changeNode.Children[0].Children.Add(changeExpressionTokens[i]);
|
||||
}
|
||||
|
||||
return new[] { valueNode, changeNode };
|
||||
}
|
||||
else
|
||||
{
|
||||
var valueNode = new ComponentAttributeExtensionNode(node)
|
||||
{
|
||||
AttributeName = valueAttributeName,
|
||||
BoundAttribute = valueAttribute, // Might be null if it doesn't match a component attribute
|
||||
PropertyName = valueAttribute?.GetPropertyName(),
|
||||
TagHelper = valueAttribute == null ? null : node.TagHelper,
|
||||
TypeName = valueAttribute?.IsWeaklyTyped() == false ? valueAttribute.TypeName : null,
|
||||
};
|
||||
|
||||
valueNode.Children.Clear();
|
||||
valueNode.Children.Add(new CSharpExpressionIntermediateNode());
|
||||
for (var i = 0; i < valueExpressionTokens.Count; i++)
|
||||
{
|
||||
valueNode.Children[0].Children.Add(valueExpressionTokens[i]);
|
||||
}
|
||||
|
||||
var changeNode = new ComponentAttributeExtensionNode(node)
|
||||
{
|
||||
AttributeName = changeAttributeName,
|
||||
BoundAttribute = changeAttribute, // Might be null if it doesn't match a component attribute
|
||||
PropertyName = changeAttribute?.GetPropertyName(),
|
||||
TagHelper = changeAttribute == null ? null : node.TagHelper,
|
||||
TypeName = changeAttribute?.IsWeaklyTyped() == false ? changeAttribute.TypeName : null,
|
||||
};
|
||||
|
||||
changeNode.Children.Clear();
|
||||
changeNode.Children.Add(new CSharpExpressionIntermediateNode());
|
||||
for (var i = 0; i < changeExpressionTokens.Count; i++)
|
||||
{
|
||||
changeNode.Children[0].Children.Add(changeExpressionTokens[i]);
|
||||
}
|
||||
|
||||
return new[] { valueNode, changeNode };
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryParseBindAttribute(
|
||||
string attributeName,
|
||||
out string valueAttributeName,
|
||||
out string changeAttributeName)
|
||||
{
|
||||
valueAttributeName = null;
|
||||
changeAttributeName = null;
|
||||
|
||||
if (!attributeName.StartsWith("bind"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (attributeName == "bind")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var segments = attributeName.Split('-');
|
||||
for (var i = 0; i < segments.Length; i++)
|
||||
{
|
||||
if (string.IsNullOrEmpty(segments[i]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
switch (segments.Length)
|
||||
{
|
||||
case 2:
|
||||
valueAttributeName = segments[1];
|
||||
return true;
|
||||
|
||||
case 3:
|
||||
changeAttributeName = segments[2];
|
||||
valueAttributeName = segments[1];
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Attempts to compute the attribute names that should be used for an instance of 'bind'.
|
||||
private bool TryComputeAttributeNames(
|
||||
IntermediateNode parent,
|
||||
TagHelperPropertyIntermediateNode node,
|
||||
string attributeName,
|
||||
out string valueAttributeName,
|
||||
out string changeAttributeName,
|
||||
out BoundAttributeDescriptor valueAttribute,
|
||||
out BoundAttributeDescriptor changeAttribute)
|
||||
{
|
||||
valueAttribute = null;
|
||||
changeAttribute = null;
|
||||
|
||||
// Even though some of our 'bind' tag helpers specify the attribute names, they
|
||||
// should still satisfy one of the valid syntaxes.
|
||||
if (!TryParseBindAttribute(attributeName, out valueAttributeName, out changeAttributeName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// The tag helper specifies attribute names, they should win.
|
||||
//
|
||||
// This handles cases like <input type="text" bind="@Foo" /> where the tag helper is
|
||||
// generated to match a specific tag and has metadata that identify the attributes.
|
||||
//
|
||||
// We expect 1 bind tag helper per-node.
|
||||
valueAttributeName = node.TagHelper.GetValueAttributeName() ?? valueAttributeName;
|
||||
changeAttributeName = node.TagHelper.GetChangeAttributeName() ?? changeAttributeName;
|
||||
|
||||
// We expect 0-1 components per-node.
|
||||
var componentTagHelper = (parent as ComponentExtensionNode)?.Component;
|
||||
if (componentTagHelper == null)
|
||||
{
|
||||
// If it's not a component node then there isn't too much else to figure out.
|
||||
return attributeName != null && changeAttributeName != null;
|
||||
}
|
||||
|
||||
// If this is a component, we need an attribute name for the value.
|
||||
if (attributeName == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// If this is a component, then we can infer '<PropertyName>Changed' as the name
|
||||
// of the change event.
|
||||
if (changeAttributeName == null)
|
||||
{
|
||||
changeAttributeName = valueAttributeName + "Changed";
|
||||
}
|
||||
|
||||
for (var i = 0; i < componentTagHelper.BoundAttributes.Count; i++)
|
||||
{
|
||||
var attribute = componentTagHelper.BoundAttributes[i];
|
||||
|
||||
if (string.Equals(valueAttributeName, attribute.Name))
|
||||
{
|
||||
valueAttribute = attribute;
|
||||
}
|
||||
|
||||
if (string.Equals(changeAttributeName, attribute.Name))
|
||||
{
|
||||
changeAttribute = attribute;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryGetFormatNode(
|
||||
IntermediateNode node,
|
||||
TagHelperPropertyIntermediateNode attributeNode,
|
||||
string valueAttributeName,
|
||||
out TagHelperPropertyIntermediateNode formatNode)
|
||||
{
|
||||
for (var i = 0; i < node.Children.Count; i++)
|
||||
{
|
||||
var child = node.Children[i] as TagHelperPropertyIntermediateNode;
|
||||
if (child != null &&
|
||||
child.TagHelper != null &&
|
||||
child.TagHelper == attributeNode.TagHelper &&
|
||||
child.AttributeName == "format-" + valueAttributeName)
|
||||
{
|
||||
formatNode = child;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
formatNode = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static IntermediateToken GetAttributeContent(TagHelperPropertyIntermediateNode node)
|
||||
{
|
||||
var template = node.FindDescendantNodes<TemplateIntermediateNode>().FirstOrDefault();
|
||||
if (template != null)
|
||||
{
|
||||
// See comments in TemplateDiagnosticPass
|
||||
node.Diagnostics.Add(ComponentDiagnosticFactory.Create_TemplateInvalidLocation(template.Source));
|
||||
return new IntermediateToken() { Kind = TokenKind.CSharp, Content = string.Empty, };
|
||||
}
|
||||
|
||||
if (node.Children[0] is HtmlContentIntermediateNode htmlContentNode)
|
||||
{
|
||||
// This case can be hit for a 'string' attribute. We want to turn it into
|
||||
// an expression.
|
||||
var content = "\"" + string.Join(string.Empty, htmlContentNode.Children.OfType<IntermediateToken>().Select(t => t.Content)) + "\"";
|
||||
return new IntermediateToken() { Kind = TokenKind.CSharp, Content = content };
|
||||
}
|
||||
else if (node.Children[0] is CSharpExpressionIntermediateNode cSharpNode)
|
||||
{
|
||||
// This case can be hit when the attribute has an explicit @ inside, which
|
||||
// 'escapes' any special sugar we provide for codegen.
|
||||
return GetToken(cSharpNode);
|
||||
}
|
||||
else
|
||||
{
|
||||
// This is the common case for 'mixed' content
|
||||
return GetToken(node);
|
||||
}
|
||||
|
||||
// In error cases we won't have a single token, but we still want to generate the code.
|
||||
IntermediateToken GetToken(IntermediateNode parent)
|
||||
{
|
||||
return
|
||||
parent.Children.Count == 1 ? (IntermediateToken)parent.Children[0] : new IntermediateToken()
|
||||
{
|
||||
Kind = TokenKind.CSharp,
|
||||
Content = string.Join(string.Empty, parent.Children.OfType<IntermediateToken>().Select(t => t.Content)),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language.Components
|
||||
{
|
||||
internal class BlazorRazorCSharpLoweringPhase : RazorEnginePhaseBase, IRazorCSharpLoweringPhase
|
||||
{
|
||||
protected override void ExecuteCore(RazorCodeDocument codeDocument)
|
||||
{
|
||||
var documentNode = codeDocument.GetDocumentIntermediateNode();
|
||||
ThrowForMissingDocumentDependency(documentNode);
|
||||
#pragma warning disable CS0618
|
||||
var writer = new DocumentWriterWorkaround().Create(documentNode.Target, documentNode.Options);
|
||||
#pragma warning restore CS0618
|
||||
try
|
||||
{
|
||||
var cSharpDocument = writer.WriteDocument(codeDocument, documentNode);
|
||||
codeDocument.SetCSharpDocument(cSharpDocument);
|
||||
}
|
||||
catch (RazorCompilerException ex)
|
||||
{
|
||||
// Currently the Blazor code generation has some 'fatal errors' that can cause code generation
|
||||
// to fail completely. This class is here to make that implementation work gracefully.
|
||||
var cSharpDocument = RazorCSharpDocument.Create("", documentNode.Options, new[] { ex.Diagnostic });
|
||||
codeDocument.SetCSharpDocument(cSharpDocument);
|
||||
}
|
||||
}
|
||||
|
||||
private class DocumentWriterWorkaround : DocumentWriter
|
||||
{
|
||||
public override RazorCSharpDocument WriteDocument(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Directs a <see cref="DocumentWriter"/> to use <see cref="BlazorRuntimeNodeWriter"/>.
|
||||
/// </summary>
|
||||
internal class BlazorCodeTarget : CodeTarget
|
||||
{
|
||||
private readonly RazorCodeGenerationOptions _options;
|
||||
|
||||
public BlazorCodeTarget(RazorCodeGenerationOptions options, IEnumerable<ICodeTargetExtension> extensions)
|
||||
{
|
||||
_options = options;
|
||||
Extensions = extensions.ToArray();
|
||||
}
|
||||
|
||||
public ICodeTargetExtension[] Extensions { get; }
|
||||
|
||||
public override IntermediateNodeWriter CreateNodeWriter()
|
||||
{
|
||||
return _options.DesignTime ? (BlazorNodeWriter)new BlazorDesignTimeNodeWriter() : new BlazorRuntimeNodeWriter();
|
||||
}
|
||||
|
||||
public override TExtension GetExtension<TExtension>()
|
||||
{
|
||||
for (var i = 0; i < Extensions.Length; i++)
|
||||
{
|
||||
var match = Extensions[i] as TExtension;
|
||||
if (match != null)
|
||||
{
|
||||
return match;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public override bool HasExtension<TExtension>()
|
||||
{
|
||||
for (var i = 0; i < Extensions.Length; i++)
|
||||
{
|
||||
var match = Extensions[i] as TExtension;
|
||||
if (match != null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,773 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
|
||||
using Microsoft.AspNetCore.Razor.Language.Extensions;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language.Components
|
||||
{
|
||||
// Based on the DesignTimeNodeWriter from Razor repo.
|
||||
internal class BlazorDesignTimeNodeWriter : BlazorNodeWriter
|
||||
{
|
||||
private readonly ScopeStack _scopeStack = new ScopeStack();
|
||||
|
||||
private static readonly string DesignTimeVariable = "__o";
|
||||
|
||||
public override void WriteHtmlBlock(CodeRenderingContext context, HtmlBlockIntermediateNode node)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (node == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(node));
|
||||
}
|
||||
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
public override void WriteHtmlElement(CodeRenderingContext context, HtmlElementIntermediateNode node)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (node == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(node));
|
||||
}
|
||||
|
||||
context.RenderChildren(node);
|
||||
}
|
||||
|
||||
public override void WriteUsingDirective(CodeRenderingContext context, UsingDirectiveIntermediateNode node)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (node == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(node));
|
||||
}
|
||||
|
||||
if (node.Source.HasValue)
|
||||
{
|
||||
using (context.CodeWriter.BuildLinePragma(node.Source.Value))
|
||||
{
|
||||
context.AddSourceMappingFor(node);
|
||||
context.CodeWriter.WriteUsing(node.Content);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
context.CodeWriter.WriteUsing(node.Content);
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteCSharpExpression(CodeRenderingContext context, CSharpExpressionIntermediateNode node)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (node == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(node));
|
||||
}
|
||||
|
||||
if (node.Children.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.Source != null)
|
||||
{
|
||||
using (context.CodeWriter.BuildLinePragma(node.Source.Value))
|
||||
{
|
||||
var offset = DesignTimeVariable.Length + " = ".Length;
|
||||
context.CodeWriter.WritePadding(offset, node.Source, context);
|
||||
context.CodeWriter.WriteStartAssignment(DesignTimeVariable);
|
||||
|
||||
for (var i = 0; i < node.Children.Count; i++)
|
||||
{
|
||||
if (node.Children[i] is IntermediateToken token && token.IsCSharp)
|
||||
{
|
||||
context.AddSourceMappingFor(token);
|
||||
context.CodeWriter.Write(token.Content);
|
||||
}
|
||||
else
|
||||
{
|
||||
// There may be something else inside the expression like a Template or another extension node.
|
||||
context.RenderNode(node.Children[i]);
|
||||
}
|
||||
}
|
||||
|
||||
context.CodeWriter.WriteLine(";");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
context.CodeWriter.WriteStartAssignment(DesignTimeVariable);
|
||||
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.
|
||||
context.RenderNode(node.Children[i]);
|
||||
}
|
||||
}
|
||||
context.CodeWriter.WriteLine(";");
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteCSharpCode(CodeRenderingContext context, CSharpCodeIntermediateNode node)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (node == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(node));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
IDisposable linePragmaScope = null;
|
||||
if (node.Source != null)
|
||||
{
|
||||
if (!isWhitespaceStatement)
|
||||
{
|
||||
linePragmaScope = context.CodeWriter.BuildLinePragma(node.Source.Value);
|
||||
}
|
||||
|
||||
context.CodeWriter.WritePadding(0, node.Source.Value, context);
|
||||
}
|
||||
else if (isWhitespaceStatement)
|
||||
{
|
||||
// Don't write whitespace if there is no line mapping for it.
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < node.Children.Count; i++)
|
||||
{
|
||||
if (node.Children[i] is IntermediateToken token && token.IsCSharp)
|
||||
{
|
||||
context.AddSourceMappingFor(token);
|
||||
context.CodeWriter.Write(token.Content);
|
||||
}
|
||||
else
|
||||
{
|
||||
// There may be something else inside the statement like an extension node.
|
||||
context.RenderNode(node.Children[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (linePragmaScope != null)
|
||||
{
|
||||
linePragmaScope.Dispose();
|
||||
}
|
||||
else
|
||||
{
|
||||
context.CodeWriter.WriteLine();
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteHtmlAttribute(CodeRenderingContext context, HtmlAttributeIntermediateNode node)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (node == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(node));
|
||||
}
|
||||
|
||||
context.RenderChildren(node);
|
||||
}
|
||||
|
||||
public override void WriteHtmlAttributeValue(CodeRenderingContext context, HtmlAttributeValueIntermediateNode node)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (node == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(node));
|
||||
}
|
||||
|
||||
// Do nothing, this can't contain code.
|
||||
}
|
||||
|
||||
public override void WriteCSharpExpressionAttributeValue(CodeRenderingContext context, CSharpExpressionAttributeValueIntermediateNode node)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (node == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(node));
|
||||
}
|
||||
|
||||
if (node.Children.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var firstChild = node.Children[0];
|
||||
if (firstChild.Source != null)
|
||||
{
|
||||
using (context.CodeWriter.BuildLinePragma(firstChild.Source.Value))
|
||||
{
|
||||
var offset = DesignTimeVariable.Length + " = ".Length;
|
||||
context.CodeWriter.WritePadding(offset, firstChild.Source, context);
|
||||
context.CodeWriter.WriteStartAssignment(DesignTimeVariable);
|
||||
|
||||
for (var i = 0; i < node.Children.Count; i++)
|
||||
{
|
||||
if (node.Children[i] is IntermediateToken token && token.IsCSharp)
|
||||
{
|
||||
context.AddSourceMappingFor(token);
|
||||
context.CodeWriter.Write(token.Content);
|
||||
}
|
||||
else
|
||||
{
|
||||
// There may be something else inside the expression like a Template or another extension node.
|
||||
context.RenderNode(node.Children[i]);
|
||||
}
|
||||
}
|
||||
|
||||
context.CodeWriter.WriteLine(";");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
context.CodeWriter.WriteStartAssignment(DesignTimeVariable);
|
||||
for (var i = 0; i < node.Children.Count; i++)
|
||||
{
|
||||
if (node.Children[i] is IntermediateToken token && token.IsCSharp)
|
||||
{
|
||||
if (token.Source != null)
|
||||
{
|
||||
context.AddSourceMappingFor(token);
|
||||
}
|
||||
|
||||
context.CodeWriter.Write(token.Content);
|
||||
}
|
||||
else
|
||||
{
|
||||
// There may be something else inside the expression like a Template or another extension node.
|
||||
context.RenderNode(node.Children[i]);
|
||||
}
|
||||
}
|
||||
context.CodeWriter.WriteLine(";");
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteHtmlContent(CodeRenderingContext context, HtmlContentIntermediateNode node)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (node == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(node));
|
||||
}
|
||||
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
public override void BeginWriteAttribute(CodeWriter codeWriter, string key)
|
||||
{
|
||||
if (codeWriter == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(codeWriter));
|
||||
}
|
||||
|
||||
if (key == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(key));
|
||||
}
|
||||
|
||||
codeWriter
|
||||
.WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(ComponentsApi.RenderTreeBuilder.AddAttribute)}")
|
||||
.Write("-1")
|
||||
.WriteParameterSeparator()
|
||||
.WriteStringLiteral(key)
|
||||
.WriteParameterSeparator();
|
||||
}
|
||||
|
||||
public override void WriteComponent(CodeRenderingContext context, ComponentExtensionNode node)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (node == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(node));
|
||||
}
|
||||
|
||||
if (node.TypeInferenceNode == null)
|
||||
{
|
||||
// Writes something like:
|
||||
//
|
||||
// builder.OpenComponent<MyComponent>(0);
|
||||
// builder.AddAttribute(1, "Foo", ...);
|
||||
// builder.AddAttribute(2, "ChildContent", ...);
|
||||
// builder.AddElementCapture(3, (__value) => _field = __value);
|
||||
// builder.CloseComponent();
|
||||
foreach (var typeArgument in node.TypeArguments)
|
||||
{
|
||||
context.RenderNode(typeArgument);
|
||||
}
|
||||
|
||||
foreach (var attribute in node.Attributes)
|
||||
{
|
||||
context.RenderNode(attribute);
|
||||
}
|
||||
|
||||
if (node.ChildContents.Any())
|
||||
{
|
||||
foreach (var childContent in node.ChildContents)
|
||||
{
|
||||
context.RenderNode(childContent);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// We eliminate 'empty' child content when building the tree so that usage like
|
||||
// '<MyComponent>\r\n</MyComponent>' doesn't create a child content.
|
||||
//
|
||||
// Consider what would happen if the user's cursor was inside the element. At
|
||||
// design -time we want to render an empty lambda to provide proper scoping
|
||||
// for any code that the user types.
|
||||
context.RenderNode(new ComponentChildContentIntermediateNode()
|
||||
{
|
||||
TypeName = ComponentsApi.RenderFragment.FullTypeName,
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var capture in node.Captures)
|
||||
{
|
||||
context.RenderNode(capture);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// When we're doing type inference, we can't write all of the code inline to initialize
|
||||
// the component on the builder. We generate a method elsewhere, and then pass all of the information
|
||||
// to that method. We pass in all of the attribute values + the sequence numbers.
|
||||
//
|
||||
// __Blazor.MyComponent.TypeInference.CreateMyComponent_0(builder, 0, 1, ..., 2, ..., 3, ....);
|
||||
var attributes = node.Attributes.ToList();
|
||||
var childContents = node.ChildContents.ToList();
|
||||
var captures = node.Captures.ToList();
|
||||
var remaining = attributes.Count + childContents.Count + captures.Count;
|
||||
|
||||
context.CodeWriter.Write(node.TypeInferenceNode.FullTypeName);
|
||||
context.CodeWriter.Write(".");
|
||||
context.CodeWriter.Write(node.TypeInferenceNode.MethodName);
|
||||
context.CodeWriter.Write("(");
|
||||
|
||||
context.CodeWriter.Write(_scopeStack.BuilderVarName);
|
||||
context.CodeWriter.Write(", ");
|
||||
|
||||
context.CodeWriter.Write("-1");
|
||||
context.CodeWriter.Write(", ");
|
||||
|
||||
for (var i = 0; i < attributes.Count; i++)
|
||||
{
|
||||
context.CodeWriter.Write("-1");
|
||||
context.CodeWriter.Write(", ");
|
||||
|
||||
// Don't type check generics, since we can't actually write the type name.
|
||||
// The type checking with happen anyway since we defined a method and we're generating
|
||||
// a call to it.
|
||||
WriteComponentAttributeInnards(context, attributes[i], canTypeCheck: false);
|
||||
|
||||
remaining--;
|
||||
if (remaining > 0)
|
||||
{
|
||||
context.CodeWriter.Write(", ");
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < childContents.Count; i++)
|
||||
{
|
||||
context.CodeWriter.Write("-1");
|
||||
context.CodeWriter.Write(", ");
|
||||
|
||||
WriteComponentChildContentInnards(context, childContents[i]);
|
||||
|
||||
remaining--;
|
||||
if (remaining > 0)
|
||||
{
|
||||
context.CodeWriter.Write(", ");
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < captures.Count; i++)
|
||||
{
|
||||
context.CodeWriter.Write("-1");
|
||||
context.CodeWriter.Write(", ");
|
||||
|
||||
WriteReferenceCaptureInnards(context, captures[i], shouldTypeCheck: false);
|
||||
|
||||
remaining--;
|
||||
if (remaining > 0)
|
||||
{
|
||||
context.CodeWriter.Write(", ");
|
||||
}
|
||||
}
|
||||
|
||||
context.CodeWriter.Write(");");
|
||||
context.CodeWriter.WriteLine();
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteComponentAttribute(CodeRenderingContext context, ComponentAttributeExtensionNode node)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (node == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(node));
|
||||
}
|
||||
|
||||
// Looks like:
|
||||
// __o = 17;
|
||||
context.CodeWriter.Write(DesignTimeVariable);
|
||||
context.CodeWriter.Write(" = ");
|
||||
|
||||
// Following the same design pattern as the runtime codegen
|
||||
WriteComponentAttributeInnards(context, node, canTypeCheck: true);
|
||||
|
||||
context.CodeWriter.Write(";");
|
||||
context.CodeWriter.WriteLine();
|
||||
}
|
||||
|
||||
private void WriteComponentAttributeInnards(CodeRenderingContext context, ComponentAttributeExtensionNode node, bool canTypeCheck)
|
||||
{
|
||||
// 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.
|
||||
//
|
||||
// This roughly follows the design of the runtime writer for simplicity.
|
||||
if (node.AttributeStructure == AttributeStructure.Minimized)
|
||||
{
|
||||
// Minimized attributes always map to 'true'
|
||||
context.CodeWriter.Write("true");
|
||||
}
|
||||
else if (node.Children.Count > 1)
|
||||
{
|
||||
// We don't expect this to happen, we just want to know if it can.
|
||||
throw new InvalidOperationException("Attribute nodes should either be minimized or a single type of content." + string.Join(", ", node.Children));
|
||||
}
|
||||
else if (node.Children.Count == 1 && node.Children[0] is HtmlContentIntermediateNode)
|
||||
{
|
||||
// We don't actually need the content at designtime, an empty string will do.
|
||||
context.CodeWriter.Write("\"\"");
|
||||
}
|
||||
else
|
||||
{
|
||||
// There are a few different forms that could be used to contain all of the tokens, but we don't really care
|
||||
// exactly what it looks like - we just want all of the content.
|
||||
//
|
||||
// This can include an empty list in some cases like the following (sic):
|
||||
// <MyComponent Value="
|
||||
//
|
||||
// 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))
|
||||
{
|
||||
// We always surround the expression with the delegate constructor. This makes type
|
||||
// inference inside lambdas, and method group conversion do the right thing.
|
||||
if (canTypeCheck)
|
||||
{
|
||||
context.CodeWriter.Write("new ");
|
||||
context.CodeWriter.Write(node.TypeName);
|
||||
context.CodeWriter.Write("(");
|
||||
}
|
||||
context.CodeWriter.WriteLine();
|
||||
|
||||
for (var i = 0; i < tokens.Count; i++)
|
||||
{
|
||||
WriteCSharpToken(context, tokens[i]);
|
||||
}
|
||||
|
||||
if (canTypeCheck)
|
||||
{
|
||||
context.CodeWriter.Write(")");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// This is the case when an attribute contains C# code
|
||||
//
|
||||
// If we have a parameter type, then add a type check.
|
||||
if (canTypeCheck && NeedsTypeCheck(node))
|
||||
{
|
||||
context.CodeWriter.Write(ComponentsApi.RuntimeHelpers.TypeCheck);
|
||||
context.CodeWriter.Write("<");
|
||||
context.CodeWriter.Write(node.TypeName);
|
||||
context.CodeWriter.Write(">");
|
||||
context.CodeWriter.Write("(");
|
||||
}
|
||||
|
||||
for (var i = 0; i < tokens.Count; i++)
|
||||
{
|
||||
WriteCSharpToken(context, tokens[i]);
|
||||
}
|
||||
|
||||
if (canTypeCheck && NeedsTypeCheck(node))
|
||||
{
|
||||
context.CodeWriter.Write(")");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool NeedsTypeCheck(ComponentAttributeExtensionNode n)
|
||||
{
|
||||
return n.BoundAttribute != null && !n.BoundAttribute.IsWeaklyTyped();
|
||||
}
|
||||
|
||||
IReadOnlyList<IntermediateToken> GetCSharpTokens(ComponentAttributeExtensionNode attribute)
|
||||
{
|
||||
// We generally expect all children to be CSharp, this is here just in case.
|
||||
return attribute.FindDescendantNodes<IntermediateToken>().Where(t => t.IsCSharp).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteComponentChildContent(CodeRenderingContext context, ComponentChildContentIntermediateNode node)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (node == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(node));
|
||||
}
|
||||
|
||||
// Writes something like:
|
||||
//
|
||||
// builder.AddAttribute(1, "ChildContent", (RenderFragment)((__builder73) => { ... }));
|
||||
// OR
|
||||
// builder.AddAttribute(1, "ChildContent", (RenderFragment<Person>)((person) => (__builder73) => { ... }));
|
||||
BeginWriteAttribute(context.CodeWriter, node.AttributeName);
|
||||
context.CodeWriter.Write($"({node.TypeName})(");
|
||||
|
||||
WriteComponentChildContentInnards(context, node);
|
||||
|
||||
context.CodeWriter.Write(")");
|
||||
context.CodeWriter.WriteEndMethodInvocation();
|
||||
}
|
||||
|
||||
private void WriteComponentChildContentInnards(CodeRenderingContext context, ComponentChildContentIntermediateNode node)
|
||||
{
|
||||
// Writes something like:
|
||||
//
|
||||
// ((__builder73) => { ... })
|
||||
// OR
|
||||
// ((person) => (__builder73) => { })
|
||||
_scopeStack.OpenComponentScope(
|
||||
context,
|
||||
node.AttributeName,
|
||||
node.IsParameterized ? node.ParameterName : null);
|
||||
for (var i = 0; i < node.Children.Count; i++)
|
||||
{
|
||||
context.RenderNode(node.Children[i]);
|
||||
}
|
||||
_scopeStack.CloseScope(context);
|
||||
}
|
||||
|
||||
public override void WriteComponentTypeArgument(CodeRenderingContext context, ComponentTypeArgumentExtensionNode node)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (node == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(node));
|
||||
}
|
||||
|
||||
// At design type we want write the equivalent of:
|
||||
//
|
||||
// __o = typeof(TItem);
|
||||
context.CodeWriter.Write(DesignTimeVariable);
|
||||
context.CodeWriter.Write(" = ");
|
||||
context.CodeWriter.Write("typeof(");
|
||||
|
||||
var tokens = GetCSharpTokens(node);
|
||||
for (var i = 0; i < tokens.Count; i++)
|
||||
{
|
||||
WriteCSharpToken(context, tokens[i]);
|
||||
}
|
||||
|
||||
context.CodeWriter.Write(");");
|
||||
context.CodeWriter.WriteLine();
|
||||
|
||||
IReadOnlyList<IntermediateToken> GetCSharpTokens(ComponentTypeArgumentExtensionNode arg)
|
||||
{
|
||||
// We generally expect all children to be CSharp, this is here just in case.
|
||||
return arg.FindDescendantNodes<IntermediateToken>().Where(t => t.IsCSharp).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteTemplate(CodeRenderingContext context, TemplateIntermediateNode node)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (node == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(node));
|
||||
}
|
||||
|
||||
// Looks like:
|
||||
//
|
||||
// (__builder73) => { ... }
|
||||
_scopeStack.OpenTemplateScope(context);
|
||||
context.RenderChildren(node);
|
||||
_scopeStack.CloseScope(context);
|
||||
}
|
||||
|
||||
public override void WriteReferenceCapture(CodeRenderingContext context, RefExtensionNode node)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (node == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(node));
|
||||
}
|
||||
|
||||
// Looks like:
|
||||
//
|
||||
// __field = default(MyComponent);
|
||||
WriteReferenceCaptureInnards(context, node, shouldTypeCheck: true);
|
||||
}
|
||||
|
||||
protected override void WriteReferenceCaptureInnards(CodeRenderingContext context, RefExtensionNode node, bool shouldTypeCheck)
|
||||
{
|
||||
// We specialize this code based on whether or not we can type check. When we're calling into
|
||||
// a type-inferenced component, we can't do the type check. See the comments in WriteTypeInferenceMethod.
|
||||
if (shouldTypeCheck)
|
||||
{
|
||||
// The runtime node writer moves the call elsewhere. At design time we
|
||||
// just want sufficiently similar code that any unknown-identifier or type
|
||||
// errors will be equivalent
|
||||
var captureTypeName = node.IsComponentCapture
|
||||
? node.ComponentCaptureTypeName
|
||||
: ComponentsApi.ElementRef.FullTypeName;
|
||||
WriteCSharpCode(context, new CSharpCodeIntermediateNode
|
||||
{
|
||||
Source = node.Source,
|
||||
Children =
|
||||
{
|
||||
node.IdentifierToken,
|
||||
new IntermediateToken
|
||||
{
|
||||
Kind = TokenKind.CSharp,
|
||||
Content = $" = default({captureTypeName});"
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// Looks like:
|
||||
//
|
||||
// (__value) = { _field = (MyComponent)__value; }
|
||||
// OR
|
||||
// (__value) = { _field = (ElementRef)__value; }
|
||||
const string refCaptureParamName = "__value";
|
||||
using (var lambdaScope = context.CodeWriter.BuildLambda(refCaptureParamName))
|
||||
{
|
||||
WriteCSharpCode(context, new CSharpCodeIntermediateNode
|
||||
{
|
||||
Source = node.Source,
|
||||
Children =
|
||||
{
|
||||
node.IdentifierToken,
|
||||
new IntermediateToken
|
||||
{
|
||||
Kind = TokenKind.CSharp,
|
||||
Content = $" = {refCaptureParamName};"
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteCSharpToken(CodeRenderingContext context, IntermediateToken token)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(token.Content))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (token.Source?.FilePath == null)
|
||||
{
|
||||
context.CodeWriter.Write(token.Content);
|
||||
return;
|
||||
}
|
||||
|
||||
using (context.CodeWriter.BuildLinePragma(token.Source))
|
||||
{
|
||||
context.CodeWriter.WritePadding(0, token.Source.Value, context);
|
||||
context.AddSourceMappingFor(token);
|
||||
context.CodeWriter.Write(token.Content);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Language.Extensions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes the Blazor extension.
|
||||
/// </summary>
|
||||
public class BlazorExtensionInitializer : RazorExtensionInitializer
|
||||
{
|
||||
/// <summary>
|
||||
/// Specifies the declaration configuration.
|
||||
/// </summary>
|
||||
public static readonly RazorConfiguration DeclarationConfiguration;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the default configuration.
|
||||
/// </summary>
|
||||
public static readonly RazorConfiguration DefaultConfiguration;
|
||||
|
||||
static BlazorExtensionInitializer()
|
||||
{
|
||||
// The configuration names here need to match what we put in the MSBuild configuration
|
||||
DeclarationConfiguration = RazorConfiguration.Create(
|
||||
RazorLanguageVersion.Experimental,
|
||||
"BlazorDeclaration-0.1",
|
||||
Array.Empty<RazorExtension>());
|
||||
|
||||
DefaultConfiguration = RazorConfiguration.Create(
|
||||
RazorLanguageVersion.Experimental,
|
||||
"Blazor-0.1",
|
||||
Array.Empty<RazorExtension>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers the Blazor extension.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="RazorProjectEngineBuilder"/>.</param>
|
||||
public static void Register(RazorProjectEngineBuilder builder)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
FunctionsDirective.Register(builder);
|
||||
ImplementsDirective.Register(builder);
|
||||
InheritsDirective.Register(builder);
|
||||
InjectDirective.Register(builder);
|
||||
LayoutDirective.Register(builder);
|
||||
PageDirective.Register(builder);
|
||||
TypeParamDirective.Register(builder);
|
||||
|
||||
builder.Features.Remove(builder.Features.OfType<IImportProjectFeature>().Single());
|
||||
builder.Features.Add(new BlazorImportProjectFeature());
|
||||
|
||||
var index = builder.Phases.IndexOf(builder.Phases.OfType<IRazorCSharpLoweringPhase>().Single());
|
||||
builder.Phases[index] = new BlazorRazorCSharpLoweringPhase();
|
||||
|
||||
builder.Features.Add(new ConfigureBlazorCodeGenerationOptions());
|
||||
|
||||
builder.AddTargetExtension(new BlazorTemplateTargetExtension());
|
||||
|
||||
var isDeclarationOnlyCompile = builder.Configuration.ConfigurationName == DeclarationConfiguration.ConfigurationName;
|
||||
|
||||
// Blazor-specific passes, in order.
|
||||
if (!isDeclarationOnlyCompile)
|
||||
{
|
||||
// There's no benefit in this optimization during the declaration-only compile
|
||||
builder.Features.Add(new TrimWhitespacePass());
|
||||
}
|
||||
builder.Features.Add(new ComponentDocumentClassifierPass());
|
||||
builder.Features.Add(new ScriptTagPass());
|
||||
builder.Features.Add(new ComplexAttributeContentPass());
|
||||
builder.Features.Add(new ComponentLoweringPass());
|
||||
builder.Features.Add(new EventHandlerLoweringPass());
|
||||
builder.Features.Add(new RefLoweringPass());
|
||||
builder.Features.Add(new BindLoweringPass());
|
||||
builder.Features.Add(new TemplateDiagnosticPass());
|
||||
builder.Features.Add(new GenericComponentPass());
|
||||
builder.Features.Add(new ChildContentDiagnosticPass());
|
||||
builder.Features.Add(new HtmlBlockPass());
|
||||
|
||||
if (isDeclarationOnlyCompile)
|
||||
{
|
||||
// This is for 'declaration only' processing. We don't want to try and emit any method bodies during
|
||||
// the design time build because we can't do it correctly until the set of components is known.
|
||||
builder.Features.Add(new EliminateMethodBodyPass());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the Blazor extension.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="RazorProjectEngineBuilder"/>.</param>
|
||||
public override void Initialize(RazorProjectEngineBuilder builder)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
Register(builder);
|
||||
}
|
||||
|
||||
private class ConfigureBlazorCodeGenerationOptions : IConfigureRazorCodeGenerationOptionsFeature
|
||||
{
|
||||
public int Order => 0;
|
||||
|
||||
public RazorEngine Engine { get; set; }
|
||||
|
||||
public void Configure(RazorCodeGenerationOptionsBuilder options)
|
||||
{
|
||||
if (options == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
// These metadata attributes require a reference to the Razor.Runtime package which we don't
|
||||
// otherwise need.
|
||||
options.SuppressMetadataAttributes = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language.Components
|
||||
{
|
||||
internal class BlazorImportProjectFeature : IImportProjectFeature
|
||||
{
|
||||
private const string ImportsFileName = "_ViewImports.cshtml";
|
||||
|
||||
private static readonly char[] PathSeparators = new char[]{ '/', '\\' };
|
||||
|
||||
// Using explicit newlines here to avoid fooling our baseline tests
|
||||
private readonly static string DefaultUsingImportContent =
|
||||
"\r\n" +
|
||||
"@using System\r\n" +
|
||||
"@using System.Collections.Generic\r\n" +
|
||||
"@using System.Linq\r\n" +
|
||||
"@using System.Threading.Tasks\r\n" +
|
||||
"@using " + ComponentsApi.RenderFragment.Namespace + "\r\n"; // Microsoft.AspNetCore.Components
|
||||
|
||||
public RazorProjectEngine ProjectEngine { get; set; }
|
||||
|
||||
public IReadOnlyList<RazorProjectItem> GetImports(RazorProjectItem projectItem)
|
||||
{
|
||||
if (projectItem == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(projectItem));
|
||||
}
|
||||
|
||||
var imports = new List<RazorProjectItem>()
|
||||
{
|
||||
new VirtualProjectItem(DefaultUsingImportContent),
|
||||
new VirtualProjectItem(@"@addTagHelper ""*, Microsoft.AspNetCore.Components"""),
|
||||
};
|
||||
|
||||
// Try and infer a namespace from the project directory. We don't yet have the ability to pass
|
||||
// the namespace through from the project.
|
||||
if (projectItem.PhysicalPath != null && projectItem.FilePath != null)
|
||||
{
|
||||
// Avoiding the path-specific APIs here, we want to handle all styles of paths
|
||||
// on all platforms
|
||||
var trimLength = projectItem.FilePath.Length + (projectItem.FilePath.StartsWith("/") ? 0 : 1);
|
||||
var baseDirectory = projectItem.PhysicalPath.Substring(0, projectItem.PhysicalPath.Length - trimLength);
|
||||
|
||||
var lastSlash = baseDirectory.LastIndexOfAny(PathSeparators);
|
||||
var baseNamespace = lastSlash == -1 ? baseDirectory : baseDirectory.Substring(lastSlash + 1);
|
||||
if (!string.IsNullOrEmpty(baseNamespace))
|
||||
{
|
||||
imports.Add(new VirtualProjectItem($@"@addTagHelper ""*, {baseNamespace}"""));
|
||||
}
|
||||
}
|
||||
|
||||
// We add hierarchical imports second so any default directive imports can be overridden.
|
||||
imports.AddRange(GetHierarchicalImports(ProjectEngine.FileSystem, projectItem));
|
||||
|
||||
return imports;
|
||||
}
|
||||
|
||||
// Temporary API until we fully convert to RazorProjectEngine
|
||||
public IEnumerable<RazorProjectItem> GetHierarchicalImports(RazorProject project, RazorProjectItem projectItem)
|
||||
{
|
||||
// We want items in descending order. FindHierarchicalItems returns items in ascending order.
|
||||
return project.FindHierarchicalItems(projectItem.FilePath, ImportsFileName).Reverse();
|
||||
}
|
||||
|
||||
private class VirtualProjectItem : RazorProjectItem
|
||||
{
|
||||
private readonly byte[] _bytes;
|
||||
|
||||
public VirtualProjectItem(string content)
|
||||
{
|
||||
var preamble = Encoding.UTF8.GetPreamble();
|
||||
var contentBytes = Encoding.UTF8.GetBytes(content);
|
||||
|
||||
_bytes = new byte[preamble.Length + contentBytes.Length];
|
||||
preamble.CopyTo(_bytes, 0);
|
||||
contentBytes.CopyTo(_bytes, preamble.Length);
|
||||
}
|
||||
|
||||
public override string BasePath => null;
|
||||
|
||||
public override string FilePath => null;
|
||||
|
||||
public override string PhysicalPath => null;
|
||||
|
||||
public override bool Exists => true;
|
||||
|
||||
public override Stream Read() => new MemoryStream(_bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language.Components
|
||||
{
|
||||
// Metadata used for Blazor's interactions with the tag helper system
|
||||
internal static class BlazorMetadata
|
||||
{
|
||||
// There's a bug in the 15.7 preview 1 Razor that prevents 'Kind' from being serialized
|
||||
// this affects both tooling and build. For now our workaround is to ignore 'Kind' and
|
||||
// use our own metadata entry to denote non-Component tag helpers.
|
||||
public static readonly string SpecialKindKey = "Blazor.IsSpecialKind";
|
||||
|
||||
public static class Bind
|
||||
{
|
||||
public static readonly string RuntimeName = "Blazor.None";
|
||||
|
||||
public readonly static string TagHelperKind = "Blazor.Bind";
|
||||
|
||||
public readonly static string FallbackKey = "Blazor.Bind.Fallback";
|
||||
|
||||
public readonly static string TypeAttribute = "Blazor.Bind.TypeAttribute";
|
||||
|
||||
public readonly static string ValueAttribute = "Blazor.Bind.ValueAttribute";
|
||||
|
||||
public readonly static string ChangeAttribute = "Blazor.Bind.ChangeAttribute";
|
||||
}
|
||||
|
||||
public static class ChildContent
|
||||
{
|
||||
public static readonly string RuntimeName = "Blazor.None";
|
||||
|
||||
public static readonly string TagHelperKind = "Blazor.ChildContent";
|
||||
|
||||
public static readonly string ParameterNameBoundAttributeKind = "Blazor.ChildContentParameterName";
|
||||
|
||||
/// <summary>
|
||||
/// The name of the synthesized attribute used to set a child content parameter.
|
||||
/// </summary>
|
||||
public static readonly string ParameterAttributeName = "Context";
|
||||
|
||||
/// <summary>
|
||||
/// The default name of the child content parameter (unless set by a Context attribute).
|
||||
/// </summary>
|
||||
public static readonly string DefaultParameterName = "context";
|
||||
}
|
||||
|
||||
public static class Component
|
||||
{
|
||||
public static readonly string ChildContentKey = "Blazor.ChildContent";
|
||||
|
||||
public static readonly string ChildContentParameterNameKey = "Blazor.ChildContentParameterName";
|
||||
|
||||
public static readonly string DelegateSignatureKey = "Blazor.DelegateSignature";
|
||||
|
||||
public static readonly string WeaklyTypedKey = "Blazor.IsWeaklyTyped";
|
||||
|
||||
public static readonly string RuntimeName = "Blazor.IComponent";
|
||||
|
||||
public readonly static string TagHelperKind = "Blazor.Component";
|
||||
|
||||
public readonly static string GenericTypedKey = "Blazor.GenericTyped";
|
||||
|
||||
public readonly static string TypeParameterKey = "Blazor.TypeParameter";
|
||||
}
|
||||
|
||||
public static class EventHandler
|
||||
{
|
||||
public static readonly string EventArgsType = "Blazor.EventHandler.EventArgs";
|
||||
|
||||
public static readonly string RuntimeName = "Blazor.None";
|
||||
|
||||
public readonly static string TagHelperKind = "Blazor.EventHandler";
|
||||
}
|
||||
|
||||
public static class Ref
|
||||
{
|
||||
public readonly static string TagHelperKind = "Blazor.Ref";
|
||||
|
||||
public static readonly string RuntimeName = "Blazor.None";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,217 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
|
||||
using Microsoft.AspNetCore.Razor.Language.Extensions;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language.Components
|
||||
{
|
||||
internal abstract class BlazorNodeWriter : IntermediateNodeWriter
|
||||
{
|
||||
public abstract void BeginWriteAttribute(CodeWriter codeWriter, string key);
|
||||
|
||||
public abstract void WriteComponent(CodeRenderingContext context, ComponentExtensionNode node);
|
||||
|
||||
public abstract void WriteComponentAttribute(CodeRenderingContext context, ComponentAttributeExtensionNode node);
|
||||
|
||||
public abstract void WriteComponentChildContent(CodeRenderingContext context, ComponentChildContentIntermediateNode node);
|
||||
|
||||
public abstract void WriteComponentTypeArgument(CodeRenderingContext context, ComponentTypeArgumentExtensionNode node);
|
||||
|
||||
public abstract void WriteHtmlElement(CodeRenderingContext context, HtmlElementIntermediateNode node);
|
||||
|
||||
public abstract void WriteHtmlBlock(CodeRenderingContext context, HtmlBlockIntermediateNode node);
|
||||
|
||||
public abstract void WriteReferenceCapture(CodeRenderingContext context, RefExtensionNode node);
|
||||
|
||||
protected abstract void WriteReferenceCaptureInnards(CodeRenderingContext context, RefExtensionNode node, bool shouldTypeCheck);
|
||||
|
||||
public abstract void WriteTemplate(CodeRenderingContext context, TemplateIntermediateNode node);
|
||||
|
||||
public sealed override void BeginWriterScope(CodeRenderingContext context, string writer)
|
||||
{
|
||||
throw new NotImplementedException(nameof(BeginWriterScope));
|
||||
}
|
||||
|
||||
public sealed override void EndWriterScope(CodeRenderingContext context)
|
||||
{
|
||||
throw new NotImplementedException(nameof(EndWriterScope));
|
||||
}
|
||||
|
||||
public sealed override void WriteCSharpCodeAttributeValue(CodeRenderingContext context, CSharpCodeAttributeValueIntermediateNode node)
|
||||
{
|
||||
// We used to support syntaxes like <elem onsomeevent=@{ /* some C# code */ } /> but this is no longer the
|
||||
// case.
|
||||
//
|
||||
// We provide an error for this case just to be friendly.
|
||||
var content = string.Join("", node.Children.OfType<IntermediateToken>().Select(t => t.Content));
|
||||
context.Diagnostics.Add(ComponentDiagnosticFactory.Create_CodeBlockInAttribute(node.Source, content));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Currently the same for design time and runtime
|
||||
public void WriteComponentTypeInferenceMethod(CodeRenderingContext context, ComponentTypeInferenceMethodIntermediateNode node)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (node == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(node));
|
||||
}
|
||||
|
||||
// This is ugly because CodeWriter doesn't allow us to erase, but we need to comma-delimit. So we have to
|
||||
// materizalize something can iterate, or use string.Join. We'll need this multiple times, so materializing
|
||||
// it.
|
||||
var parameters = GetParameterDeclarations();
|
||||
|
||||
// This is really similar to the code in WriteComponentAttribute and WriteComponentChildContent - except simpler because
|
||||
// attributes and child contents look like variables.
|
||||
//
|
||||
// Looks like:
|
||||
//
|
||||
// public static void CreateFoo_0<T1, T2>(RenderTreeBuilder builder, int seq, int __seq0, T1 __arg0, int __seq1, global::System.Collections.Generic.List<T2> __arg1, int __seq2, string __arg2)
|
||||
// {
|
||||
// builder.OpenComponent<Foo<T1, T2>>();
|
||||
// builder.AddAttribute(__seq0, "Attr0", __arg0);
|
||||
// builder.AddAttribute(__seq1, "Attr1", __arg1);
|
||||
// builder.AddAttribute(__seq2, "Attr2", __arg2);
|
||||
// builder.CloseComponent();
|
||||
// }
|
||||
//
|
||||
// As a special case, we need to generate a thunk for captures in this block instead of using
|
||||
// them verbatim.
|
||||
//
|
||||
// The problem is that RenderTreeBuilder wants an Action<object>. The caller can't write the type
|
||||
// name if it contains generics, and we can't write the variable they want to assign to.
|
||||
var writer = context.CodeWriter;
|
||||
|
||||
writer.Write("public static void ");
|
||||
writer.Write(node.MethodName);
|
||||
|
||||
writer.Write("<");
|
||||
writer.Write(string.Join(", ", node.Component.Component.GetTypeParameters().Select(a => a.Name)));
|
||||
writer.Write(">");
|
||||
|
||||
writer.Write("(");
|
||||
writer.Write("global::");
|
||||
writer.Write(ComponentsApi.RenderTreeBuilder.FullTypeName);
|
||||
writer.Write(" builder");
|
||||
writer.Write(", ");
|
||||
writer.Write("int seq");
|
||||
|
||||
if (parameters.Count > 0)
|
||||
{
|
||||
writer.Write(", ");
|
||||
}
|
||||
|
||||
for (var i = 0; i < parameters.Count; i++)
|
||||
{
|
||||
writer.Write("int ");
|
||||
writer.Write(parameters[i].seqName);
|
||||
|
||||
writer.Write(", ");
|
||||
writer.Write(parameters[i].typeName);
|
||||
writer.Write(" ");
|
||||
writer.Write(parameters[i].parameterName);
|
||||
|
||||
if (i < parameters.Count - 1)
|
||||
{
|
||||
writer.Write(", ");
|
||||
}
|
||||
}
|
||||
|
||||
writer.Write(")");
|
||||
writer.WriteLine();
|
||||
|
||||
writer.WriteLine("{");
|
||||
|
||||
// builder.OpenComponent<TComponent>(42);
|
||||
context.CodeWriter.Write("builder");
|
||||
context.CodeWriter.Write(".");
|
||||
context.CodeWriter.Write(ComponentsApi.RenderTreeBuilder.OpenComponent);
|
||||
context.CodeWriter.Write("<");
|
||||
context.CodeWriter.Write(node.Component.TypeName);
|
||||
context.CodeWriter.Write(">(");
|
||||
context.CodeWriter.Write("seq");
|
||||
context.CodeWriter.Write(");");
|
||||
context.CodeWriter.WriteLine();
|
||||
|
||||
var index = 0;
|
||||
foreach (var attribute in node.Component.Attributes)
|
||||
{
|
||||
context.CodeWriter.WriteStartInstanceMethodInvocation("builder", ComponentsApi.RenderTreeBuilder.AddAttribute);
|
||||
context.CodeWriter.Write(parameters[index].seqName);
|
||||
context.CodeWriter.Write(", ");
|
||||
|
||||
context.CodeWriter.Write($"\"{attribute.AttributeName}\"");
|
||||
context.CodeWriter.Write(", ");
|
||||
|
||||
context.CodeWriter.Write(parameters[index].parameterName);
|
||||
context.CodeWriter.WriteEndMethodInvocation();
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
foreach (var childContent in node.Component.ChildContents)
|
||||
{
|
||||
context.CodeWriter.WriteStartInstanceMethodInvocation("builder", ComponentsApi.RenderTreeBuilder.AddAttribute);
|
||||
context.CodeWriter.Write(parameters[index].seqName);
|
||||
context.CodeWriter.Write(", ");
|
||||
|
||||
context.CodeWriter.Write($"\"{childContent.AttributeName}\"");
|
||||
context.CodeWriter.Write(", ");
|
||||
|
||||
context.CodeWriter.Write(parameters[index].parameterName);
|
||||
context.CodeWriter.WriteEndMethodInvocation();
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
foreach (var capture in node.Component.Captures)
|
||||
{
|
||||
context.CodeWriter.WriteStartInstanceMethodInvocation("builder", capture.IsComponentCapture ? ComponentsApi.RenderTreeBuilder.AddComponentReferenceCapture : ComponentsApi.RenderTreeBuilder.AddElementReferenceCapture);
|
||||
context.CodeWriter.Write(parameters[index].seqName);
|
||||
context.CodeWriter.Write(", ");
|
||||
|
||||
var cast = capture.IsComponentCapture ? $"({capture.ComponentCaptureTypeName})" : string.Empty;
|
||||
context.CodeWriter.Write($"(__value) => {{ {parameters[index].parameterName}({cast}__value); }}");
|
||||
context.CodeWriter.WriteEndMethodInvocation();
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
context.CodeWriter.WriteInstanceMethodInvocation("builder", ComponentsApi.RenderTreeBuilder.CloseComponent);
|
||||
|
||||
writer.WriteLine("}");
|
||||
|
||||
List<(string seqName, string typeName, string parameterName)> GetParameterDeclarations()
|
||||
{
|
||||
var p = new List<(string seqName, string typeName, string parameterName)>();
|
||||
foreach (var attribute in node.Component.Attributes)
|
||||
{
|
||||
p.Add(($"__seq{p.Count}", attribute.TypeName, $"__arg{p.Count}"));
|
||||
}
|
||||
|
||||
foreach (var childContent in node.Component.ChildContents)
|
||||
{
|
||||
p.Add(($"__seq{p.Count}", childContent.TypeName, $"__arg{p.Count}"));
|
||||
}
|
||||
|
||||
foreach (var capture in node.Component.Captures)
|
||||
{
|
||||
p.Add(($"__seq{p.Count}", capture.TypeName, $"__arg{p.Count}"));
|
||||
}
|
||||
|
||||
return p;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,764 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
|
||||
using Microsoft.AspNetCore.Razor.Language.Extensions;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates the C# code corresponding to Razor source document contents.
|
||||
/// </summary>
|
||||
internal class BlazorRuntimeNodeWriter : BlazorNodeWriter
|
||||
{
|
||||
private readonly List<IntermediateToken> _currentAttributeValues = new List<IntermediateToken>();
|
||||
private readonly ScopeStack _scopeStack = new ScopeStack();
|
||||
private int _sourceSequence = 0;
|
||||
|
||||
public override void WriteCSharpCode(CodeRenderingContext context, CSharpCodeIntermediateNode node)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (node == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(node));
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
// The runtime and design time code differ in their handling of whitespace-only
|
||||
// statements. At runtime we can discard them completely. At design time we need
|
||||
// to keep them for the editor.
|
||||
return;
|
||||
}
|
||||
|
||||
IDisposable linePragmaScope = null;
|
||||
if (node.Source != null)
|
||||
{
|
||||
linePragmaScope = context.CodeWriter.BuildLinePragma(node.Source.Value);
|
||||
context.CodeWriter.WritePadding(0, node.Source.Value, context);
|
||||
}
|
||||
|
||||
for (var i = 0; i < node.Children.Count; i++)
|
||||
{
|
||||
if (node.Children[i] is IntermediateToken token && token.IsCSharp)
|
||||
{
|
||||
context.AddSourceMappingFor(token);
|
||||
context.CodeWriter.Write(token.Content);
|
||||
}
|
||||
else
|
||||
{
|
||||
// There may be something else inside the statement like an extension node.
|
||||
context.RenderNode(node.Children[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (linePragmaScope != null)
|
||||
{
|
||||
linePragmaScope.Dispose();
|
||||
}
|
||||
else
|
||||
{
|
||||
context.CodeWriter.WriteLine();
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteCSharpExpression(CodeRenderingContext context, CSharpExpressionIntermediateNode node)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (node == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(node));
|
||||
}
|
||||
|
||||
// Since we're not in the middle of writing an element, this must evaluate as some
|
||||
// text to display
|
||||
context.CodeWriter
|
||||
.WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(ComponentsApi.RenderTreeBuilder.AddContent)}")
|
||||
.Write((_sourceSequence++).ToString())
|
||||
.WriteParameterSeparator();
|
||||
|
||||
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.
|
||||
context.RenderNode(node.Children[i]);
|
||||
}
|
||||
}
|
||||
|
||||
context.CodeWriter.WriteEndMethodInvocation();
|
||||
}
|
||||
|
||||
public override void WriteCSharpExpressionAttributeValue(CodeRenderingContext context, CSharpExpressionAttributeValueIntermediateNode node)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (node == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(node));
|
||||
}
|
||||
|
||||
// In cases like "somestring @variable", Razor tokenizes it as:
|
||||
// [0] HtmlContent="somestring"
|
||||
// [1] CsharpContent="variable" Prefix=" "
|
||||
// ... so to avoid losing whitespace, convert the prefix to a further token in the list
|
||||
if (!string.IsNullOrEmpty(node.Prefix))
|
||||
{
|
||||
_currentAttributeValues.Add(new IntermediateToken() { Kind = TokenKind.Html, Content = node.Prefix });
|
||||
}
|
||||
|
||||
for (var i = 0; i < node.Children.Count; i++)
|
||||
{
|
||||
_currentAttributeValues.Add((IntermediateToken)node.Children[i]);
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteHtmlBlock(CodeRenderingContext context, HtmlBlockIntermediateNode node)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (node == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(node));
|
||||
}
|
||||
|
||||
context.CodeWriter
|
||||
.WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(ComponentsApi.RenderTreeBuilder.AddMarkupContent)}")
|
||||
.Write((_sourceSequence++).ToString())
|
||||
.WriteParameterSeparator()
|
||||
.WriteStringLiteral(node.Content)
|
||||
.WriteEndMethodInvocation();
|
||||
}
|
||||
|
||||
public override void WriteHtmlElement(CodeRenderingContext context, HtmlElementIntermediateNode node)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (node == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(node));
|
||||
}
|
||||
|
||||
context.CodeWriter
|
||||
.WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(ComponentsApi.RenderTreeBuilder.OpenElement)}")
|
||||
.Write((_sourceSequence++).ToString())
|
||||
.WriteParameterSeparator()
|
||||
.WriteStringLiteral(node.TagName)
|
||||
.WriteEndMethodInvocation();
|
||||
|
||||
// Render Attributes before creating the scope.
|
||||
foreach (var attribute in node.Attributes)
|
||||
{
|
||||
context.RenderNode(attribute);
|
||||
}
|
||||
|
||||
foreach (var capture in node.Captures)
|
||||
{
|
||||
context.RenderNode(capture);
|
||||
}
|
||||
|
||||
// Render body of the tag inside the scope
|
||||
foreach (var child in node.Body)
|
||||
{
|
||||
context.RenderNode(child);
|
||||
}
|
||||
|
||||
context.CodeWriter
|
||||
.WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{ComponentsApi.RenderTreeBuilder.CloseElement}")
|
||||
.WriteEndMethodInvocation();
|
||||
}
|
||||
|
||||
public override void WriteHtmlAttribute(CodeRenderingContext context, HtmlAttributeIntermediateNode node)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (node == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(node));
|
||||
}
|
||||
|
||||
Debug.Assert(_currentAttributeValues.Count == 0);
|
||||
context.RenderChildren(node);
|
||||
|
||||
WriteAttribute(context.CodeWriter, node.AttributeName, _currentAttributeValues);
|
||||
_currentAttributeValues.Clear();
|
||||
}
|
||||
|
||||
public override void WriteHtmlAttributeValue(CodeRenderingContext context, HtmlAttributeValueIntermediateNode node)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (node == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(node));
|
||||
}
|
||||
|
||||
var stringContent = ((IntermediateToken)node.Children.Single()).Content;
|
||||
_currentAttributeValues.Add(new IntermediateToken() { Kind = TokenKind.Html, Content = node.Prefix + stringContent, });
|
||||
}
|
||||
|
||||
public override void WriteHtmlContent(CodeRenderingContext context, HtmlContentIntermediateNode node)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (node == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(node));
|
||||
}
|
||||
|
||||
// Text node
|
||||
var content = GetHtmlContent(node);
|
||||
context.CodeWriter
|
||||
.WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(ComponentsApi.RenderTreeBuilder.AddContent)}")
|
||||
.Write((_sourceSequence++).ToString())
|
||||
.WriteParameterSeparator()
|
||||
.WriteStringLiteral(content)
|
||||
.WriteEndMethodInvocation();
|
||||
}
|
||||
|
||||
public override void WriteUsingDirective(CodeRenderingContext context, UsingDirectiveIntermediateNode node)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (node == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(node));
|
||||
}
|
||||
|
||||
context.CodeWriter.WriteUsing(node.Content, endLine: true);
|
||||
}
|
||||
|
||||
public override void WriteComponent(CodeRenderingContext context, ComponentExtensionNode node)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (node == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(node));
|
||||
}
|
||||
|
||||
if (node.TypeInferenceNode == null)
|
||||
{
|
||||
// If the component is using not using type inference then we just write an open/close with a series
|
||||
// of add attribute calls in between.
|
||||
//
|
||||
// Writes something like:
|
||||
//
|
||||
// builder.OpenComponent<MyComponent>(0);
|
||||
// builder.AddAttribute(1, "Foo", ...);
|
||||
// builder.AddAttribute(2, "ChildContent", ...);
|
||||
// builder.AddElementCapture(3, (__value) => _field = __value);
|
||||
// builder.CloseComponent();
|
||||
|
||||
// builder.OpenComponent<TComponent>(42);
|
||||
context.CodeWriter.Write(_scopeStack.BuilderVarName);
|
||||
context.CodeWriter.Write(".");
|
||||
context.CodeWriter.Write(ComponentsApi.RenderTreeBuilder.OpenComponent);
|
||||
context.CodeWriter.Write("<");
|
||||
context.CodeWriter.Write(node.TypeName);
|
||||
context.CodeWriter.Write(">(");
|
||||
context.CodeWriter.Write((_sourceSequence++).ToString());
|
||||
context.CodeWriter.Write(");");
|
||||
context.CodeWriter.WriteLine();
|
||||
|
||||
// We can skip type arguments during runtime codegen, they are handled in the
|
||||
// type/parameter declarations.
|
||||
|
||||
foreach (var attribute in node.Attributes)
|
||||
{
|
||||
context.RenderNode(attribute);
|
||||
}
|
||||
|
||||
foreach (var childContent in node.ChildContents)
|
||||
{
|
||||
context.RenderNode(childContent);
|
||||
}
|
||||
|
||||
foreach (var capture in node.Captures)
|
||||
{
|
||||
context.RenderNode(capture);
|
||||
}
|
||||
|
||||
// builder.CloseComponent();
|
||||
context.CodeWriter.Write(_scopeStack.BuilderVarName);
|
||||
context.CodeWriter.Write(".");
|
||||
context.CodeWriter.Write(ComponentsApi.RenderTreeBuilder.CloseComponent);
|
||||
context.CodeWriter.Write("();");
|
||||
context.CodeWriter.WriteLine();
|
||||
}
|
||||
else
|
||||
{
|
||||
// When we're doing type inference, we can't write all of the code inline to initialize
|
||||
// the component on the builder. We generate a method elsewhere, and then pass all of the information
|
||||
// to that method. We pass in all of the attribute values + the sequence numbers.
|
||||
//
|
||||
// __Blazor.MyComponent.TypeInference.CreateMyComponent_0(builder, 0, 1, ..., 2, ..., 3, ...);
|
||||
var attributes = node.Attributes.ToList();
|
||||
var childContents = node.ChildContents.ToList();
|
||||
var captures = node.Captures.ToList();
|
||||
var remaining = attributes.Count + childContents.Count + captures.Count;
|
||||
|
||||
context.CodeWriter.Write(node.TypeInferenceNode.FullTypeName);
|
||||
context.CodeWriter.Write(".");
|
||||
context.CodeWriter.Write(node.TypeInferenceNode.MethodName);
|
||||
context.CodeWriter.Write("(");
|
||||
|
||||
context.CodeWriter.Write(_scopeStack.BuilderVarName);
|
||||
context.CodeWriter.Write(", ");
|
||||
|
||||
context.CodeWriter.Write((_sourceSequence++).ToString());
|
||||
context.CodeWriter.Write(", ");
|
||||
|
||||
for (var i = 0; i < attributes.Count; i++)
|
||||
{
|
||||
context.CodeWriter.Write((_sourceSequence++).ToString());
|
||||
context.CodeWriter.Write(", ");
|
||||
|
||||
// Don't type check generics, since we can't actually write the type name.
|
||||
// The type checking with happen anyway since we defined a method and we're generating
|
||||
// a call to it.
|
||||
WriteComponentAttributeInnards(context, attributes[i], canTypeCheck: false);
|
||||
|
||||
remaining--;
|
||||
if (remaining > 0)
|
||||
{
|
||||
context.CodeWriter.Write(", ");
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < childContents.Count; i++)
|
||||
{
|
||||
context.CodeWriter.Write((_sourceSequence++).ToString());
|
||||
context.CodeWriter.Write(", ");
|
||||
|
||||
WriteComponentChildContentInnards(context, childContents[i]);
|
||||
|
||||
remaining--;
|
||||
if (remaining > 0)
|
||||
{
|
||||
context.CodeWriter.Write(", ");
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < captures.Count; i++)
|
||||
{
|
||||
context.CodeWriter.Write((_sourceSequence++).ToString());
|
||||
context.CodeWriter.Write(", ");
|
||||
|
||||
WriteReferenceCaptureInnards(context, captures[i], shouldTypeCheck: false);
|
||||
|
||||
remaining--;
|
||||
if (remaining > 0)
|
||||
{
|
||||
context.CodeWriter.Write(", ");
|
||||
}
|
||||
}
|
||||
|
||||
context.CodeWriter.Write(");");
|
||||
context.CodeWriter.WriteLine();
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteComponentAttribute(CodeRenderingContext context, ComponentAttributeExtensionNode node)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (node == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(node));
|
||||
}
|
||||
|
||||
// builder.AddAttribute(1, "Foo", 42);
|
||||
context.CodeWriter.Write(_scopeStack.BuilderVarName);
|
||||
context.CodeWriter.Write(".");
|
||||
context.CodeWriter.Write(ComponentsApi.RenderTreeBuilder.AddAttribute);
|
||||
context.CodeWriter.Write("(");
|
||||
context.CodeWriter.Write((_sourceSequence++).ToString());
|
||||
context.CodeWriter.Write(", ");
|
||||
context.CodeWriter.WriteStringLiteral(node.AttributeName);
|
||||
context.CodeWriter.Write(", ");
|
||||
|
||||
WriteComponentAttributeInnards(context, node, canTypeCheck: true);
|
||||
|
||||
context.CodeWriter.Write(");");
|
||||
context.CodeWriter.WriteLine();
|
||||
}
|
||||
|
||||
private void WriteComponentAttributeInnards(CodeRenderingContext context, ComponentAttributeExtensionNode node, bool canTypeCheck)
|
||||
{
|
||||
if (node.AttributeStructure == AttributeStructure.Minimized)
|
||||
{
|
||||
// Minimized attributes always map to 'true'
|
||||
context.CodeWriter.Write("true");
|
||||
}
|
||||
else if (node.Children.Count > 1)
|
||||
{
|
||||
// We don't expect this to happen, we just want to know if it can.
|
||||
throw new InvalidOperationException("Attribute nodes should either be minimized or a single type of content." + string.Join(", ", node.Children));
|
||||
}
|
||||
else if (node.Children.Count == 1 && node.Children[0] is HtmlContentIntermediateNode htmlNode)
|
||||
{
|
||||
// This is how string attributes are lowered by default, a single HTML node with a single HTML token.
|
||||
var content = string.Join(string.Empty, GetHtmlTokens(htmlNode).Select(t => t.Content));
|
||||
context.CodeWriter.WriteStringLiteral(content);
|
||||
}
|
||||
else
|
||||
{
|
||||
// See comments in BlazorDesignTimeNodeWriter for a description of the cases that are possible.
|
||||
var tokens = GetCSharpTokens(node);
|
||||
if ((node.BoundAttribute?.IsDelegateProperty() ?? false) ||
|
||||
(node.BoundAttribute?.IsChildContentProperty() ?? false))
|
||||
{
|
||||
if (canTypeCheck)
|
||||
{
|
||||
context.CodeWriter.Write("new ");
|
||||
context.CodeWriter.Write(node.TypeName);
|
||||
context.CodeWriter.Write("(");
|
||||
}
|
||||
|
||||
for (var i = 0; i < tokens.Count; i++)
|
||||
{
|
||||
context.CodeWriter.Write(tokens[i].Content);
|
||||
}
|
||||
|
||||
if (canTypeCheck)
|
||||
{
|
||||
context.CodeWriter.Write(")");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (canTypeCheck && NeedsTypeCheck(node))
|
||||
{
|
||||
context.CodeWriter.Write(ComponentsApi.RuntimeHelpers.TypeCheck);
|
||||
context.CodeWriter.Write("<");
|
||||
context.CodeWriter.Write(node.TypeName);
|
||||
context.CodeWriter.Write(">");
|
||||
context.CodeWriter.Write("(");
|
||||
}
|
||||
|
||||
for (var i = 0; i < tokens.Count; i++)
|
||||
{
|
||||
context.CodeWriter.Write(tokens[i].Content);
|
||||
}
|
||||
|
||||
if (canTypeCheck && NeedsTypeCheck(node))
|
||||
{
|
||||
context.CodeWriter.Write(")");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IReadOnlyList<IntermediateToken> GetCSharpTokens(ComponentAttributeExtensionNode attribute)
|
||||
{
|
||||
// We generally expect all children to be CSharp, this is here just in case.
|
||||
return attribute.FindDescendantNodes<IntermediateToken>().Where(t => t.IsCSharp).ToArray();
|
||||
}
|
||||
|
||||
IReadOnlyList<IntermediateToken> GetHtmlTokens(HtmlContentIntermediateNode html)
|
||||
{
|
||||
// We generally expect all children to be HTML, this is here just in case.
|
||||
return html.FindDescendantNodes<IntermediateToken>().Where(t => t.IsHtml).ToArray();
|
||||
}
|
||||
|
||||
bool NeedsTypeCheck(ComponentAttributeExtensionNode n)
|
||||
{
|
||||
return node.BoundAttribute != null && !node.BoundAttribute.IsWeaklyTyped();
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteComponentChildContent(CodeRenderingContext context, ComponentChildContentIntermediateNode node)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (node == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(node));
|
||||
}
|
||||
|
||||
// Writes something like:
|
||||
//
|
||||
// builder.AddAttribute(1, "ChildContent", (RenderFragment)((__builder73) => { ... }));
|
||||
// OR
|
||||
// builder.AddAttribute(1, "ChildContent", (RenderFragment<Person>)((person) => (__builder73) => { ... }));
|
||||
BeginWriteAttribute(context.CodeWriter, node.AttributeName);
|
||||
context.CodeWriter.Write($"({node.TypeName})(");
|
||||
|
||||
WriteComponentChildContentInnards(context, node);
|
||||
|
||||
context.CodeWriter.Write(")");
|
||||
context.CodeWriter.WriteEndMethodInvocation();
|
||||
}
|
||||
|
||||
private void WriteComponentChildContentInnards(CodeRenderingContext context, ComponentChildContentIntermediateNode node)
|
||||
{
|
||||
// Writes something like:
|
||||
//
|
||||
// ((__builder73) => { ... })
|
||||
// OR
|
||||
// ((person) => (__builder73) => { })
|
||||
_scopeStack.OpenComponentScope(
|
||||
context,
|
||||
node.AttributeName,
|
||||
node.IsParameterized ? node.ParameterName : null);
|
||||
for (var i = 0; i < node.Children.Count; i++)
|
||||
{
|
||||
context.RenderNode(node.Children[i]);
|
||||
}
|
||||
_scopeStack.CloseScope(context);
|
||||
}
|
||||
|
||||
public override void WriteComponentTypeArgument(CodeRenderingContext context, ComponentTypeArgumentExtensionNode node)
|
||||
{
|
||||
// We can skip type arguments during runtime codegen, they are handled in the
|
||||
// type/parameter declarations.
|
||||
}
|
||||
|
||||
public override void WriteTemplate(CodeRenderingContext context, TemplateIntermediateNode node)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (node == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(node));
|
||||
}
|
||||
|
||||
// Looks like:
|
||||
//
|
||||
// (__builder73) => { ... }
|
||||
_scopeStack.OpenTemplateScope(context);
|
||||
context.RenderChildren(node);
|
||||
_scopeStack.CloseScope(context);
|
||||
}
|
||||
|
||||
public override void WriteReferenceCapture(CodeRenderingContext context, RefExtensionNode node)
|
||||
{
|
||||
// Looks like:
|
||||
//
|
||||
// builder.AddComponentReferenceCapture(2, (__value) = { _field = (MyComponent)__value; });
|
||||
// OR
|
||||
// builder.AddElementReferenceCapture(2, (__value) = { _field = (ElementRef)__value; });
|
||||
var codeWriter = context.CodeWriter;
|
||||
|
||||
var methodName = node.IsComponentCapture
|
||||
? nameof(ComponentsApi.RenderTreeBuilder.AddComponentReferenceCapture)
|
||||
: nameof(ComponentsApi.RenderTreeBuilder.AddElementReferenceCapture);
|
||||
codeWriter
|
||||
.WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{methodName}")
|
||||
.Write((_sourceSequence++).ToString())
|
||||
.WriteParameterSeparator();
|
||||
|
||||
WriteReferenceCaptureInnards(context, node, shouldTypeCheck: true);
|
||||
|
||||
codeWriter.WriteEndMethodInvocation();
|
||||
}
|
||||
|
||||
protected override void WriteReferenceCaptureInnards(CodeRenderingContext context, RefExtensionNode node, bool shouldTypeCheck)
|
||||
{
|
||||
// Looks like:
|
||||
//
|
||||
// (__value) = { _field = (MyComponent)__value; }
|
||||
// OR
|
||||
// (__value) = { _field = (ElementRef)__value; }
|
||||
const string refCaptureParamName = "__value";
|
||||
using (var lambdaScope = context.CodeWriter.BuildLambda(refCaptureParamName))
|
||||
{
|
||||
var typecastIfNeeded = shouldTypeCheck && node.IsComponentCapture ? $"({node.ComponentCaptureTypeName})" : string.Empty;
|
||||
WriteCSharpCode(context, new CSharpCodeIntermediateNode
|
||||
{
|
||||
Source = node.Source,
|
||||
Children =
|
||||
{
|
||||
node.IdentifierToken,
|
||||
new IntermediateToken
|
||||
{
|
||||
Kind = TokenKind.CSharp,
|
||||
Content = $" = {typecastIfNeeded}{refCaptureParamName};"
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteAttribute(CodeWriter codeWriter, string key, IList<IntermediateToken> value)
|
||||
{
|
||||
BeginWriteAttribute(codeWriter, key);
|
||||
WriteAttributeValue(codeWriter, value);
|
||||
codeWriter.WriteEndMethodInvocation();
|
||||
}
|
||||
|
||||
public override void BeginWriteAttribute(CodeWriter codeWriter, string key)
|
||||
{
|
||||
codeWriter
|
||||
.WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(ComponentsApi.RenderTreeBuilder.AddAttribute)}")
|
||||
.Write((_sourceSequence++).ToString())
|
||||
.WriteParameterSeparator()
|
||||
.WriteStringLiteral(key)
|
||||
.WriteParameterSeparator();
|
||||
}
|
||||
|
||||
private static string GetHtmlContent(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();
|
||||
}
|
||||
|
||||
// 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(CodeWriter writer, IList<IntermediateToken> tokens)
|
||||
{
|
||||
if (tokens == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(tokens));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
writer.Write(token.Content);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (insideCSharp)
|
||||
{
|
||||
writer.Write(")");
|
||||
insideCSharp = false;
|
||||
}
|
||||
|
||||
if (i != 0)
|
||||
{
|
||||
writer.Write(" + ");
|
||||
}
|
||||
|
||||
writer.WriteStringLiteral(token.Content);
|
||||
}
|
||||
}
|
||||
|
||||
if (insideCSharp)
|
||||
{
|
||||
writer.Write(")");
|
||||
}
|
||||
}
|
||||
else if (hasCSharp)
|
||||
{
|
||||
writer.Write(string.Join("", tokens.Select(t => t.Content)));
|
||||
}
|
||||
else if (hasHtml)
|
||||
{
|
||||
writer.WriteStringLiteral(string.Join("", tokens.Select(t => t.Content)));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Minimized attributes always map to 'true'
|
||||
writer.Write("true");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
// 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.CodeGeneration;
|
||||
using Microsoft.AspNetCore.Razor.Language.Extensions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language.Components
|
||||
{
|
||||
internal class BlazorTemplateTargetExtension : ITemplateTargetExtension
|
||||
{
|
||||
public void WriteTemplate(CodeRenderingContext context, TemplateIntermediateNode node)
|
||||
{
|
||||
((BlazorNodeWriter)context.NodeWriter).WriteTemplate(context, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language.Components
|
||||
{
|
||||
internal class ChildContentDiagnosticPass : IntermediateNodePassBase, IRazorOptimizationPass
|
||||
{
|
||||
// Runs after components/eventhandlers/ref/bind/templates. We want to validate every component
|
||||
// and it's usage of ChildContent.
|
||||
public override int Order => 160;
|
||||
|
||||
protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
|
||||
{
|
||||
var visitor = new Visitor();
|
||||
visitor.Visit(documentNode);
|
||||
}
|
||||
|
||||
private class Visitor : IntermediateNodeWalker, IExtensionIntermediateNodeVisitor<ComponentExtensionNode>, IExtensionIntermediateNodeVisitor<ComponentChildContentIntermediateNode>
|
||||
{
|
||||
public void VisitExtension(ComponentExtensionNode node)
|
||||
{
|
||||
// Check for properties that are set by both element contents (body) and the attribute itself.
|
||||
foreach (var childContent in node.ChildContents)
|
||||
{
|
||||
foreach (var attribute in node.Attributes)
|
||||
{
|
||||
if (attribute.AttributeName == childContent.AttributeName)
|
||||
{
|
||||
node.Diagnostics.Add(ComponentDiagnosticFactory.Create_ChildContentSetByAttributeAndBody(
|
||||
attribute.Source,
|
||||
attribute.AttributeName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
base.VisitDefault(node);
|
||||
}
|
||||
|
||||
public void VisitExtension(ComponentChildContentIntermediateNode node)
|
||||
{
|
||||
// Check that each child content has a unique parameter name within its scope. This is important
|
||||
// because the parameter name can be implicit, and it doesn't work well when nested.
|
||||
if (node.IsParameterized)
|
||||
{
|
||||
for (var i = 0; i < Ancestors.Count - 1; i++)
|
||||
{
|
||||
var ancestor = Ancestors[i] as ComponentChildContentIntermediateNode;
|
||||
if (ancestor != null &&
|
||||
ancestor.IsParameterized &&
|
||||
string.Equals(node.ParameterName, ancestor.ParameterName, StringComparison.Ordinal))
|
||||
{
|
||||
// Duplicate name. We report an error because this will almost certainly also lead to an error
|
||||
// from the C# compiler that's way less clear.
|
||||
node.Diagnostics.Add(ComponentDiagnosticFactory.Create_ChildContentRepeatedParameterName(
|
||||
node.Source,
|
||||
node,
|
||||
(ComponentExtensionNode)Ancestors[0], // Enclosing component
|
||||
ancestor, // conflicting child content node
|
||||
(ComponentExtensionNode)Ancestors[i + 1])); // Enclosing component of conflicting child content node
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
base.VisitDefault(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.AspNetCore.Razor.Language.Components
|
||||
{
|
||||
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 = autoSpace;
|
||||
_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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language.Components
|
||||
{
|
||||
// We don't support 'complex' content for components (mixed C# and markup) right now.
|
||||
// It's not clear yet if Blazor will have a good scenario to use these constructs.
|
||||
//
|
||||
// This is where a lot of the complexity in the Razor/TagHelpers model creeps in and we
|
||||
// might be able to avoid it if these features aren't needed.
|
||||
internal class ComplexAttributeContentPass : IntermediateNodePassBase, IRazorOptimizationPass
|
||||
{
|
||||
// Run before other Blazor passes
|
||||
public override int Order => -1000;
|
||||
|
||||
protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
|
||||
{
|
||||
var nodes = documentNode.FindDescendantNodes<TagHelperIntermediateNode>();
|
||||
for (var i = 0; i < nodes.Count; i++)
|
||||
{
|
||||
ProcessAttributes(nodes[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessAttributes(TagHelperIntermediateNode node)
|
||||
{
|
||||
for (var i = node.Children.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (node.Children[i] is TagHelperPropertyIntermediateNode propertyNode)
|
||||
{
|
||||
if (TrySimplifyContent(propertyNode) && node.TagHelpers.Any(t => t.IsComponentTagHelper()))
|
||||
{
|
||||
node.Diagnostics.Add(ComponentDiagnosticFactory.Create_UnsupportedComplexContent(
|
||||
propertyNode,
|
||||
propertyNode.AttributeName));
|
||||
node.Children.RemoveAt(i);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (node.Children[i] is TagHelperHtmlAttributeIntermediateNode htmlNode)
|
||||
{
|
||||
if (TrySimplifyContent(htmlNode) && node.TagHelpers.Any(t => t.IsComponentTagHelper()))
|
||||
{
|
||||
node.Diagnostics.Add(ComponentDiagnosticFactory.Create_UnsupportedComplexContent(
|
||||
htmlNode,
|
||||
htmlNode.AttributeName));
|
||||
node.Children.RemoveAt(i);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TrySimplifyContent(IntermediateNode node)
|
||||
{
|
||||
if (node.Children.Count == 1 &&
|
||||
node.Children[0] is HtmlAttributeIntermediateNode htmlNode &&
|
||||
htmlNode.Children.Count > 1)
|
||||
{
|
||||
// This case can be hit for a 'string' attribute
|
||||
return true;
|
||||
}
|
||||
else if (node.Children.Count == 1 &&
|
||||
node.Children[0] is CSharpExpressionIntermediateNode cSharpNode &&
|
||||
cSharpNode.Children.Count > 1)
|
||||
{
|
||||
// This case can be hit when the attribute has an explicit @ inside, which
|
||||
// 'escapes' any special sugar we provide for codegen.
|
||||
//
|
||||
// There's a special case here for explicit expressions. See https://github.com/aspnet/Razor/issues/2203
|
||||
// handling this case as a tactical matter since it's important for lambdas.
|
||||
if (cSharpNode.Children.Count == 3 &&
|
||||
cSharpNode.Children[0] is IntermediateToken token0 &&
|
||||
cSharpNode.Children[2] is IntermediateToken token2 &&
|
||||
token0.Content == "(" &&
|
||||
token2.Content == ")")
|
||||
{
|
||||
cSharpNode.Children.RemoveAt(2);
|
||||
cSharpNode.Children.RemoveAt(0);
|
||||
|
||||
// We were able to simplify it, all good.
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
else if (node.Children.Count == 1 &&
|
||||
node.Children[0] is CSharpCodeIntermediateNode cSharpCodeNode)
|
||||
{
|
||||
// This is the case when an attribute contains a code block @{ ... }
|
||||
// We don't support this.
|
||||
return true;
|
||||
}
|
||||
else if (node.Children.Count > 1)
|
||||
{
|
||||
// This is the common case for 'mixed' content
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language.Components
|
||||
{
|
||||
internal class ComponentAttributeExtensionNode : ExtensionIntermediateNode
|
||||
{
|
||||
public ComponentAttributeExtensionNode()
|
||||
{
|
||||
}
|
||||
|
||||
public ComponentAttributeExtensionNode(TagHelperHtmlAttributeIntermediateNode attributeNode)
|
||||
{
|
||||
if (attributeNode == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(attributeNode));
|
||||
}
|
||||
|
||||
AttributeName = attributeNode.AttributeName;
|
||||
AttributeStructure = attributeNode.AttributeStructure;
|
||||
Source = attributeNode.Source;
|
||||
|
||||
for (var i = 0; i < attributeNode.Children.Count; i++)
|
||||
{
|
||||
Children.Add(attributeNode.Children[i]);
|
||||
}
|
||||
|
||||
for (var i = 0; i < attributeNode.Diagnostics.Count; i++)
|
||||
{
|
||||
Diagnostics.Add(attributeNode.Diagnostics[i]);
|
||||
}
|
||||
}
|
||||
|
||||
public ComponentAttributeExtensionNode(TagHelperPropertyIntermediateNode propertyNode)
|
||||
{
|
||||
if (propertyNode == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(propertyNode));
|
||||
}
|
||||
|
||||
AttributeName = propertyNode.AttributeName;
|
||||
AttributeStructure = propertyNode.AttributeStructure;
|
||||
BoundAttribute = propertyNode.BoundAttribute;
|
||||
PropertyName = propertyNode.BoundAttribute.GetPropertyName();
|
||||
Source = propertyNode.Source;
|
||||
TagHelper = propertyNode.TagHelper;
|
||||
TypeName = propertyNode.BoundAttribute.IsWeaklyTyped() ? null : propertyNode.BoundAttribute.TypeName;
|
||||
|
||||
for (var i = 0; i < propertyNode.Children.Count; i++)
|
||||
{
|
||||
Children.Add(propertyNode.Children[i]);
|
||||
}
|
||||
|
||||
for (var i = 0; i < propertyNode.Diagnostics.Count; i++)
|
||||
{
|
||||
Diagnostics.Add(propertyNode.Diagnostics[i]);
|
||||
}
|
||||
}
|
||||
|
||||
public ComponentAttributeExtensionNode(ComponentAttributeExtensionNode attributeNode)
|
||||
{
|
||||
if (attributeNode == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(attributeNode));
|
||||
}
|
||||
|
||||
AttributeName = attributeNode.AttributeName;
|
||||
AttributeStructure = attributeNode.AttributeStructure;
|
||||
BoundAttribute = attributeNode.BoundAttribute;
|
||||
PropertyName = attributeNode.BoundAttribute.GetPropertyName();
|
||||
Source = attributeNode.Source;
|
||||
TagHelper = attributeNode.TagHelper;
|
||||
TypeName = attributeNode.BoundAttribute.IsWeaklyTyped() ? null : attributeNode.BoundAttribute.TypeName;
|
||||
|
||||
for (var i = 0; i < attributeNode.Children.Count; i++)
|
||||
{
|
||||
Children.Add(attributeNode.Children[i]);
|
||||
}
|
||||
|
||||
for (var i = 0; i < attributeNode.Diagnostics.Count; i++)
|
||||
{
|
||||
Diagnostics.Add(attributeNode.Diagnostics[i]);
|
||||
}
|
||||
}
|
||||
|
||||
public override IntermediateNodeCollection Children { get; } = new IntermediateNodeCollection();
|
||||
|
||||
public string AttributeName { get; set; }
|
||||
|
||||
public AttributeStructure AttributeStructure { get; set; }
|
||||
|
||||
public BoundAttributeDescriptor BoundAttribute { get; set; }
|
||||
|
||||
public string PropertyName { get; set; }
|
||||
|
||||
public TagHelperDescriptor TagHelper { get; set; }
|
||||
|
||||
public string TypeName { get; set; }
|
||||
|
||||
public override void Accept(IntermediateNodeVisitor visitor)
|
||||
{
|
||||
if (visitor == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(visitor));
|
||||
}
|
||||
|
||||
AcceptExtensionNode<ComponentAttributeExtensionNode>(this, visitor);
|
||||
}
|
||||
|
||||
public override void WriteNode(CodeTarget target, CodeRenderingContext context)
|
||||
{
|
||||
if (target == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(target));
|
||||
}
|
||||
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
var writer = (BlazorNodeWriter)context.NodeWriter;
|
||||
writer.WriteComponentAttribute(context, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language.Components
|
||||
{
|
||||
internal class ComponentChildContentIntermediateNode : ExtensionIntermediateNode
|
||||
{
|
||||
public string AttributeName => BoundAttribute?.Name ?? ComponentsApi.RenderTreeBuilder.ChildContent;
|
||||
|
||||
public BoundAttributeDescriptor BoundAttribute { get; set; }
|
||||
|
||||
public override IntermediateNodeCollection Children { get; } = new IntermediateNodeCollection();
|
||||
|
||||
public bool IsParameterized => BoundAttribute?.IsParameterizedChildContentProperty() ?? false;
|
||||
|
||||
public string ParameterName { get; set; }
|
||||
|
||||
public string TypeName { get; set; }
|
||||
|
||||
public override void Accept(IntermediateNodeVisitor visitor)
|
||||
{
|
||||
if (visitor == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(visitor));
|
||||
}
|
||||
|
||||
AcceptExtensionNode<ComponentChildContentIntermediateNode>(this, visitor);
|
||||
}
|
||||
|
||||
public override void WriteNode(CodeTarget target, CodeRenderingContext context)
|
||||
{
|
||||
if (target == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(target));
|
||||
}
|
||||
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
var writer = (BlazorNodeWriter)context.NodeWriter;
|
||||
writer.WriteComponentChildContent(context, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language
|
||||
namespace Microsoft.AspNetCore.Razor.Language.Components
|
||||
{
|
||||
internal static class ComponentDiagnosticFactory
|
||||
{
|
||||
|
|
@ -217,11 +217,11 @@ namespace Microsoft.AspNetCore.Razor.Language
|
|||
"following top-level items: {1}.",
|
||||
RazorDiagnosticSeverity.Error);
|
||||
|
||||
//public static RazorDiagnostic Create_ChildContentMixedWithExplicitChildContent(SourceSpan? source, ComponentExtensionNode component)
|
||||
//{
|
||||
// var supportedElements = string.Join(", ", component.Component.GetChildContentProperties().Select(p => $"'{p.Name}'"));
|
||||
// return RazorDiagnostic.Create(ChildContentMixedWithExplicitChildContent, source ?? SourceSpan.Undefined, component.TagName, supportedElements);
|
||||
//}
|
||||
public static RazorDiagnostic Create_ChildContentMixedWithExplicitChildContent(SourceSpan? source, ComponentExtensionNode component)
|
||||
{
|
||||
var supportedElements = string.Join(", ", component.Component.GetChildContentProperties().Select(p => $"'{p.Name}'"));
|
||||
return RazorDiagnostic.Create(ChildContentMixedWithExplicitChildContent, source ?? SourceSpan.Undefined, component.TagName, supportedElements);
|
||||
}
|
||||
|
||||
public static readonly RazorDiagnosticDescriptor ChildContentHasInvalidAttribute =
|
||||
new RazorDiagnosticDescriptor(
|
||||
|
|
@ -252,26 +252,26 @@ namespace Microsoft.AspNetCore.Razor.Language
|
|||
"element '{3}' of component '{4}'. Specify the parameter name like: '<{0} Context=\"another_name\"> to resolve the ambiguity",
|
||||
RazorDiagnosticSeverity.Error);
|
||||
|
||||
//public static RazorDiagnostic Create_ChildContentRepeatedParameterName(
|
||||
// SourceSpan? source,
|
||||
// ComponentChildContentIntermediateNode childContent1,
|
||||
// ComponentExtensionNode component1,
|
||||
// ComponentChildContentIntermediateNode childContent2,
|
||||
// ComponentExtensionNode component2)
|
||||
//{
|
||||
// Debug.Assert(childContent1.ParameterName == childContent2.ParameterName);
|
||||
// Debug.Assert(childContent1.IsParameterized);
|
||||
// Debug.Assert(childContent2.IsParameterized);
|
||||
public static RazorDiagnostic Create_ChildContentRepeatedParameterName(
|
||||
SourceSpan? source,
|
||||
ComponentChildContentIntermediateNode childContent1,
|
||||
ComponentExtensionNode component1,
|
||||
ComponentChildContentIntermediateNode childContent2,
|
||||
ComponentExtensionNode component2)
|
||||
{
|
||||
Debug.Assert(childContent1.ParameterName == childContent2.ParameterName);
|
||||
Debug.Assert(childContent1.IsParameterized);
|
||||
Debug.Assert(childContent2.IsParameterized);
|
||||
|
||||
// return RazorDiagnostic.Create(
|
||||
// ChildContentRepeatedParameterName,
|
||||
// source ?? SourceSpan.Undefined,
|
||||
// childContent1.AttributeName,
|
||||
// component1.TagName,
|
||||
// childContent1.ParameterName,
|
||||
// childContent2.AttributeName,
|
||||
// component2.TagName);
|
||||
//}
|
||||
return RazorDiagnostic.Create(
|
||||
ChildContentRepeatedParameterName,
|
||||
source ?? SourceSpan.Undefined,
|
||||
childContent1.AttributeName,
|
||||
component1.TagName,
|
||||
childContent1.ParameterName,
|
||||
childContent2.AttributeName,
|
||||
component2.TagName);
|
||||
}
|
||||
|
||||
public static readonly RazorDiagnosticDescriptor GenericComponentMissingTypeArgument =
|
||||
new RazorDiagnosticDescriptor(
|
||||
|
|
@ -279,16 +279,16 @@ namespace Microsoft.AspNetCore.Razor.Language
|
|||
() => "The component '{0}' is missing required type arguments. Specify the missing types using the attributes: {1}.",
|
||||
RazorDiagnosticSeverity.Error);
|
||||
|
||||
//public static RazorDiagnostic Create_GenericComponentMissingTypeArgument(
|
||||
// SourceSpan? source,
|
||||
// ComponentExtensionNode component,
|
||||
// IEnumerable<BoundAttributeDescriptor> attributes)
|
||||
//{
|
||||
// Debug.Assert(component.Component.IsGenericTypedComponent());
|
||||
public static RazorDiagnostic Create_GenericComponentMissingTypeArgument(
|
||||
SourceSpan? source,
|
||||
ComponentExtensionNode component,
|
||||
IEnumerable<BoundAttributeDescriptor> attributes)
|
||||
{
|
||||
Debug.Assert(component.Component.IsGenericTypedComponent());
|
||||
|
||||
// var attributesText = string.Join(", ", attributes.Select(a => $"'{a.Name}'"));
|
||||
// return RazorDiagnostic.Create(GenericComponentMissingTypeArgument, source ?? SourceSpan.Undefined, component.TagName, attributesText);
|
||||
//}
|
||||
var attributesText = string.Join(", ", attributes.Select(a => $"'{a.Name}'"));
|
||||
return RazorDiagnostic.Create(GenericComponentMissingTypeArgument, source ?? SourceSpan.Undefined, component.TagName, attributesText);
|
||||
}
|
||||
|
||||
public static readonly RazorDiagnosticDescriptor GenericComponentTypeInferenceUnderspecified =
|
||||
new RazorDiagnosticDescriptor(
|
||||
|
|
@ -297,16 +297,16 @@ namespace Microsoft.AspNetCore.Razor.Language
|
|||
"directly using the following attributes: {1}.",
|
||||
RazorDiagnosticSeverity.Error);
|
||||
|
||||
//public static RazorDiagnostic Create_GenericComponentTypeInferenceUnderspecified(
|
||||
// SourceSpan? source,
|
||||
// ComponentExtensionNode component,
|
||||
// IEnumerable<BoundAttributeDescriptor> attributes)
|
||||
//{
|
||||
// Debug.Assert(component.Component.IsGenericTypedComponent());
|
||||
public static RazorDiagnostic Create_GenericComponentTypeInferenceUnderspecified(
|
||||
SourceSpan? source,
|
||||
ComponentExtensionNode component,
|
||||
IEnumerable<BoundAttributeDescriptor> attributes)
|
||||
{
|
||||
Debug.Assert(component.Component.IsGenericTypedComponent());
|
||||
|
||||
// var attributesText = string.Join(", ", attributes.Select(a => $"'{a.Name}'"));
|
||||
// return RazorDiagnostic.Create(GenericComponentTypeInferenceUnderspecified, source ?? SourceSpan.Undefined, component.TagName, attributesText);
|
||||
//}
|
||||
var attributesText = string.Join(", ", attributes.Select(a => $"'{a.Name}'"));
|
||||
return RazorDiagnostic.Create(GenericComponentTypeInferenceUnderspecified, source ?? SourceSpan.Undefined, component.TagName, attributesText);
|
||||
}
|
||||
|
||||
public static readonly RazorDiagnosticDescriptor ChildContentHasInvalidParameterOnComponent =
|
||||
new RazorDiagnosticDescriptor(
|
||||
|
|
@ -314,9 +314,9 @@ namespace Microsoft.AspNetCore.Razor.Language
|
|||
() => "Invalid parameter name. The parameter name attribute '{0}' on component '{1}' can only include literal text.",
|
||||
RazorDiagnosticSeverity.Error);
|
||||
|
||||
//public static RazorDiagnostic Create_ChildContentHasInvalidParameterOnComponent(SourceSpan? source, string attribute, string element)
|
||||
//{
|
||||
// return RazorDiagnostic.Create(ChildContentHasInvalidParameterOnComponent, source ?? SourceSpan.Undefined, attribute, element);
|
||||
//}
|
||||
public static RazorDiagnostic Create_ChildContentHasInvalidParameterOnComponent(SourceSpan? source, string attribute, string element)
|
||||
{
|
||||
return RazorDiagnostic.Create(ChildContentHasInvalidParameterOnComponent, source ?? SourceSpan.Undefined, attribute, element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
|
|
@ -135,5 +135,8 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static bool IsBuildRenderTreeBaseCall(CSharpCodeIntermediateNode node)
|
||||
=> node.Annotations[BuildRenderTreeBaseCallAnnotation] != null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language.Components
|
||||
{
|
||||
internal class ComponentExtensionNode : ExtensionIntermediateNode
|
||||
{
|
||||
public IEnumerable<ComponentAttributeExtensionNode> Attributes => Children.OfType<ComponentAttributeExtensionNode>();
|
||||
|
||||
public IEnumerable<RefExtensionNode> Captures => Children.OfType<RefExtensionNode>();
|
||||
|
||||
public IEnumerable<ComponentChildContentIntermediateNode> ChildContents => Children.OfType<ComponentChildContentIntermediateNode>();
|
||||
|
||||
public override IntermediateNodeCollection Children { get; } = new IntermediateNodeCollection();
|
||||
|
||||
public TagHelperDescriptor Component { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the child content parameter name (null if unset) that was applied at the component level.
|
||||
/// </summary>
|
||||
public string ChildContentParameterName { get; set; }
|
||||
|
||||
public IEnumerable<ComponentTypeArgumentExtensionNode> TypeArguments => Children.OfType<ComponentTypeArgumentExtensionNode>();
|
||||
|
||||
public string TagName { get; set; }
|
||||
|
||||
// An optional type inference node. This will be populated (and point to a different part of the tree)
|
||||
// if this component call site requires type inference.
|
||||
public ComponentTypeInferenceMethodIntermediateNode TypeInferenceNode { get; set; }
|
||||
|
||||
public string TypeName { get; set; }
|
||||
|
||||
public override void Accept(IntermediateNodeVisitor visitor)
|
||||
{
|
||||
if (visitor == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(visitor));
|
||||
}
|
||||
|
||||
AcceptExtensionNode<ComponentExtensionNode>(this, visitor);
|
||||
}
|
||||
|
||||
public override void WriteNode(CodeTarget target, CodeRenderingContext context)
|
||||
{
|
||||
if (target == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(target));
|
||||
}
|
||||
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
var writer = (BlazorNodeWriter)context.NodeWriter;
|
||||
writer.WriteComponent(context, this);
|
||||
}
|
||||
|
||||
private string DebuggerDisplay
|
||||
{
|
||||
get
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
builder.Append("Component: ");
|
||||
builder.Append("<");
|
||||
builder.Append(TagName);
|
||||
|
||||
foreach (var attribute in Attributes)
|
||||
{
|
||||
builder.Append(" ");
|
||||
builder.Append(attribute.AttributeName);
|
||||
builder.Append("=\"...\"");
|
||||
}
|
||||
|
||||
foreach (var capture in Captures)
|
||||
{
|
||||
builder.Append(" ");
|
||||
builder.Append("ref");
|
||||
builder.Append("=\"...\"");
|
||||
}
|
||||
|
||||
foreach (var typeArgument in TypeArguments)
|
||||
{
|
||||
builder.Append(" ");
|
||||
builder.Append(typeArgument.TypeParameterName);
|
||||
builder.Append("=\"...\"");
|
||||
}
|
||||
|
||||
builder.Append(">");
|
||||
builder.Append(ChildContents.Any() ? "..." : string.Empty);
|
||||
builder.Append("</");
|
||||
builder.Append(TagName);
|
||||
builder.Append(">");
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,488 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language.Components
|
||||
{
|
||||
internal class ComponentLoweringPass : IntermediateNodePassBase, IRazorOptimizationPass
|
||||
{
|
||||
// This pass runs earlier than our other passes that 'lower' specific kinds of attributes.
|
||||
public override int Order => 0;
|
||||
|
||||
protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
|
||||
{
|
||||
var @namespace = documentNode.FindPrimaryNamespace();
|
||||
var @class = documentNode.FindPrimaryClass();
|
||||
if (@namespace == null || @class == null)
|
||||
{
|
||||
// Nothing to do, bail. We can't function without the standard structure.
|
||||
return;
|
||||
}
|
||||
|
||||
// For each component *usage* we need to rewrite the tag helper node to map to the relevant component
|
||||
// APIs.
|
||||
var references = documentNode.FindDescendantReferences<TagHelperIntermediateNode>();
|
||||
for (var i = 0; i < references.Count; i++)
|
||||
{
|
||||
var reference = references[i];
|
||||
var node = (TagHelperIntermediateNode)reference.Node;
|
||||
|
||||
var count = 0;
|
||||
for (var j = 0; j < node.TagHelpers.Count; j++)
|
||||
{
|
||||
if (node.TagHelpers[j].IsComponentTagHelper())
|
||||
{
|
||||
// Only allow a single component tag helper per element. If there are multiple, we'll just consider
|
||||
// the first one and ignore the others.
|
||||
if (count++ > 1)
|
||||
{
|
||||
node.Diagnostics.Add(ComponentDiagnosticFactory.Create_MultipleComponents(node.Source, node.TagName, node.TagHelpers));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (count >= 1)
|
||||
{
|
||||
reference.Replace(RewriteAsComponent(node, node.TagHelpers.First(t => t.IsComponentTagHelper())));
|
||||
}
|
||||
else if (node.TagHelpers.Any(t => t.IsChildContentTagHelper()))
|
||||
{
|
||||
// Ignore, this will be handled when we rewrite the parent.
|
||||
}
|
||||
else
|
||||
{
|
||||
reference.Replace(RewriteAsElement(node));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ComponentExtensionNode RewriteAsComponent(TagHelperIntermediateNode node, TagHelperDescriptor tagHelper)
|
||||
{
|
||||
var component = new ComponentExtensionNode()
|
||||
{
|
||||
Component = tagHelper,
|
||||
Source = node.Source,
|
||||
TagName = node.TagName,
|
||||
TypeName = tagHelper.GetTypeName(),
|
||||
};
|
||||
|
||||
for (var i = 0; i < node.Diagnostics.Count; i++)
|
||||
{
|
||||
component.Diagnostics.Add(node.Diagnostics[i]);
|
||||
}
|
||||
|
||||
var visitor = new ComponentRewriteVisitor(component);
|
||||
visitor.Visit(node);
|
||||
|
||||
// Fixup the parameter names of child content elements. We can't do this during the rewrite
|
||||
// because we see the nodes in the wrong order.
|
||||
foreach (var childContent in component.ChildContents)
|
||||
{
|
||||
childContent.ParameterName = childContent.ParameterName ?? component.ChildContentParameterName ?? BlazorMetadata.ChildContent.DefaultParameterName;
|
||||
}
|
||||
|
||||
return component;
|
||||
}
|
||||
|
||||
private HtmlElementIntermediateNode RewriteAsElement(TagHelperIntermediateNode node)
|
||||
{
|
||||
var result = new HtmlElementIntermediateNode()
|
||||
{
|
||||
Source = node.Source,
|
||||
TagName = node.TagName,
|
||||
};
|
||||
|
||||
for (var i = 0; i < node.Diagnostics.Count; i++)
|
||||
{
|
||||
result.Diagnostics.Add(node.Diagnostics[i]);
|
||||
}
|
||||
|
||||
var visitor = new ElementRewriteVisitor(result.Children);
|
||||
visitor.Visit(node);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private class ComponentRewriteVisitor : IntermediateNodeWalker
|
||||
{
|
||||
private readonly ComponentExtensionNode _component;
|
||||
private readonly IntermediateNodeCollection _children;
|
||||
|
||||
public ComponentRewriteVisitor(ComponentExtensionNode component)
|
||||
{
|
||||
_component = component;
|
||||
_children = component.Children;
|
||||
}
|
||||
|
||||
public override void VisitTagHelper(TagHelperIntermediateNode node)
|
||||
{
|
||||
// Visit children, we're replacing this node.
|
||||
base.VisitDefault(node);
|
||||
}
|
||||
|
||||
public override void VisitTagHelperBody(TagHelperBodyIntermediateNode node)
|
||||
{
|
||||
// Wrap the component's children in a ChildContent node if we have some significant
|
||||
// content.
|
||||
if (node.Children.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If we get a single HTML content node containing only whitespace,
|
||||
// then this is probably a tag that looks like '<MyComponent> </MyComponent>
|
||||
//
|
||||
// We don't want to create a child content for this case, because it can conflict
|
||||
// with a child content that's set via an attribute. We don't want the formatting
|
||||
// of insignificant whitespace to be annoying when setting attributes directly.
|
||||
if (node.Children.Count == 1 && IsIgnorableWhitespace(node.Children[0]))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// From here we fork and behave differently based on whether the component's child content is
|
||||
// implicit or explicit.
|
||||
//
|
||||
// Explicit child content will look like: <MyComponent><ChildContent><div>...</div></ChildContent></MyComponent>
|
||||
// compared with implicit: <MyComponent><div></div></MyComponent>
|
||||
//
|
||||
// Using implicit child content:
|
||||
// 1. All content is grouped into a single child content lambda, and assigned to the property 'ChildContent'
|
||||
//
|
||||
// Using explicit child content:
|
||||
// 1. All content must be contained within 'child content' elements that are direct children
|
||||
// 2. Whitespace outside of 'child content' elements will be ignored (not an error)
|
||||
// 3. Non-whitespace outside of 'child content' elements will cause an error
|
||||
// 4. All 'child content' elements must match parameters on the component (exception for ChildContent,
|
||||
// which is always allowed.
|
||||
// 5. Each 'child content' element will generate its own lambda, and be assigned to the property
|
||||
// that matches the element name.
|
||||
if (!node.Children.OfType<TagHelperIntermediateNode>().Any(t => t.TagHelpers.Any(th => th.IsChildContentTagHelper())))
|
||||
{
|
||||
// This node has implicit child content. It may or may not have an attribute that matches.
|
||||
var attribute = _component.Component.BoundAttributes
|
||||
.Where(a => string.Equals(a.Name, ComponentsApi.RenderTreeBuilder.ChildContent, StringComparison.Ordinal))
|
||||
.FirstOrDefault();
|
||||
_children.Add(RewriteChildContent(attribute, node.Source, node.Children));
|
||||
return;
|
||||
}
|
||||
|
||||
// OK this node has explicit child content, we can rewrite it by visiting each node
|
||||
// in sequence, since we:
|
||||
// a) need to rewrite each child content element
|
||||
// b) any significant content outside of a child content is an error
|
||||
for (var i = 0; i < node.Children.Count; i++)
|
||||
{
|
||||
var child = node.Children[i];
|
||||
if (IsIgnorableWhitespace(child))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (child is TagHelperIntermediateNode tagHelperNode &&
|
||||
tagHelperNode.TagHelpers.Any(th => th.IsChildContentTagHelper()))
|
||||
{
|
||||
// This is a child content element
|
||||
var attribute = _component.Component.BoundAttributes
|
||||
.Where(a => string.Equals(a.Name, tagHelperNode.TagName, StringComparison.Ordinal))
|
||||
.FirstOrDefault();
|
||||
_children.Add(RewriteChildContent(attribute, child.Source, child.Children));
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we get here then this is significant content inside a component with explicit child content.
|
||||
child.Diagnostics.Add(ComponentDiagnosticFactory.Create_ChildContentMixedWithExplicitChildContent(child.Source, _component));
|
||||
_children.Add(child);
|
||||
}
|
||||
|
||||
bool IsIgnorableWhitespace(IntermediateNode n)
|
||||
{
|
||||
if (n is HtmlContentIntermediateNode html &&
|
||||
html.Children.Count == 1 &&
|
||||
html.Children[0] is IntermediateToken token &&
|
||||
string.IsNullOrWhiteSpace(token.Content))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private ComponentChildContentIntermediateNode RewriteChildContent(BoundAttributeDescriptor attribute, SourceSpan? source, IntermediateNodeCollection children)
|
||||
{
|
||||
var childContent = new ComponentChildContentIntermediateNode()
|
||||
{
|
||||
BoundAttribute = attribute,
|
||||
Source = source,
|
||||
TypeName = attribute?.TypeName ?? ComponentsApi.RenderFragment.FullTypeName,
|
||||
};
|
||||
|
||||
// There are two cases here:
|
||||
// 1. Implicit child content - the children will be non-taghelper nodes, just accept them
|
||||
// 2. Explicit child content - the children will be various tag helper nodes, that need special processing.
|
||||
for (var i = 0; i < children.Count; i++)
|
||||
{
|
||||
var child = children[i];
|
||||
if (child is TagHelperBodyIntermediateNode body)
|
||||
{
|
||||
// The body is all of the content we want to render, the rest of the children will
|
||||
// be the attributes.
|
||||
for (var j = 0; j < body.Children.Count; j++)
|
||||
{
|
||||
childContent.Children.Add(body.Children[j]);
|
||||
}
|
||||
}
|
||||
else if (child is TagHelperPropertyIntermediateNode property)
|
||||
{
|
||||
if (property.BoundAttribute.IsChildContentParameterNameProperty())
|
||||
{
|
||||
// Check for each child content with a parameter name, that the parameter name is specified
|
||||
// with literal text. For instance, the following is not allowed and should generate a diagnostic.
|
||||
//
|
||||
// <MyComponent><ChildContent Context="@Foo()">...</ChildContent></MyComponent>
|
||||
if (TryGetAttributeStringContent(property, out var parameterName))
|
||||
{
|
||||
childContent.ParameterName = parameterName;
|
||||
continue;
|
||||
}
|
||||
|
||||
// The parameter name is invalid.
|
||||
childContent.Diagnostics.Add(ComponentDiagnosticFactory.Create_ChildContentHasInvalidParameter(property.Source, property.AttributeName, attribute.Name));
|
||||
continue;
|
||||
}
|
||||
|
||||
// This is an unrecognized attribute, this is possible if you try to do something like put 'ref' on a child content.
|
||||
childContent.Diagnostics.Add(ComponentDiagnosticFactory.Create_ChildContentHasInvalidAttribute(property.Source, property.AttributeName, attribute.Name));
|
||||
}
|
||||
else if (child is TagHelperHtmlAttributeIntermediateNode a)
|
||||
{
|
||||
// This is an HTML attribute on a child content.
|
||||
childContent.Diagnostics.Add(ComponentDiagnosticFactory.Create_ChildContentHasInvalidAttribute(a.Source, a.AttributeName, attribute.Name));
|
||||
}
|
||||
else
|
||||
{
|
||||
// This is some other kind of node (likely an implicit child content)
|
||||
childContent.Children.Add(child);
|
||||
}
|
||||
}
|
||||
|
||||
return childContent;
|
||||
}
|
||||
|
||||
private bool TryGetAttributeStringContent(TagHelperPropertyIntermediateNode property, out string content)
|
||||
{
|
||||
// The success path looks like - a single HTML Attribute Value node with tokens
|
||||
if (property.Children.Count == 1 &&
|
||||
property.Children[0] is HtmlContentIntermediateNode html)
|
||||
{
|
||||
content = string.Join(string.Empty, html.Children.OfType<IntermediateToken>().Select(n => n.Content));
|
||||
return true;
|
||||
}
|
||||
|
||||
content = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public override void VisitTagHelperHtmlAttribute(TagHelperHtmlAttributeIntermediateNode node)
|
||||
{
|
||||
var attribute = new ComponentAttributeExtensionNode(node);
|
||||
_children.Add(attribute);
|
||||
|
||||
// Since we don't support complex content, we can rewrite the inside of this
|
||||
// node to the rather simpler form that property nodes usually have.
|
||||
for (var i = 0; i < attribute.Children.Count; i++)
|
||||
{
|
||||
if (attribute.Children[i] is HtmlAttributeValueIntermediateNode htmlValue)
|
||||
{
|
||||
attribute.Children[i] = new HtmlContentIntermediateNode()
|
||||
{
|
||||
Children =
|
||||
{
|
||||
htmlValue.Children.Single(),
|
||||
},
|
||||
Source = htmlValue.Source,
|
||||
};
|
||||
}
|
||||
else if (attribute.Children[i] is CSharpExpressionAttributeValueIntermediateNode expressionValue)
|
||||
{
|
||||
attribute.Children[i] = new CSharpExpressionIntermediateNode()
|
||||
{
|
||||
Children =
|
||||
{
|
||||
expressionValue.Children.Single(),
|
||||
},
|
||||
Source = expressionValue.Source,
|
||||
};
|
||||
}
|
||||
else if (attribute.Children[i] is CSharpCodeAttributeValueIntermediateNode codeValue)
|
||||
{
|
||||
attribute.Children[i] = new CSharpExpressionIntermediateNode()
|
||||
{
|
||||
Children =
|
||||
{
|
||||
codeValue.Children.Single(),
|
||||
},
|
||||
Source = codeValue.Source,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void VisitTagHelperProperty(TagHelperPropertyIntermediateNode node)
|
||||
{
|
||||
// Each 'tag helper property' belongs to a specific tag helper. We want to handle
|
||||
// the cases for components, but leave others alone. This allows our other passes
|
||||
// to handle those cases.
|
||||
if (!node.TagHelper.IsComponentTagHelper())
|
||||
{
|
||||
_children.Add(node);
|
||||
return;
|
||||
}
|
||||
|
||||
// Another special case here - this might be a type argument. These don't represent 'real' parameters
|
||||
// that get passed to the component, it needs special code generation support.
|
||||
if (node.TagHelper.IsGenericTypedComponent() && node.BoundAttribute.IsTypeParameterProperty())
|
||||
{
|
||||
_children.Add(new ComponentTypeArgumentExtensionNode(node));
|
||||
return;
|
||||
}
|
||||
|
||||
// Another special case here -- this might be a 'Context' parameter, which specifies the name
|
||||
// for lambda parameter for parameterized child content
|
||||
if (node.BoundAttribute.IsChildContentParameterNameProperty())
|
||||
{
|
||||
// Check for each child content with a parameter name, that the parameter name is specified
|
||||
// with literal text. For instance, the following is not allowed and should generate a diagnostic.
|
||||
//
|
||||
// <MyComponent Context="@Foo()">...</MyComponent>
|
||||
if (TryGetAttributeStringContent(node, out var parameterName))
|
||||
{
|
||||
_component.ChildContentParameterName = parameterName;
|
||||
return;
|
||||
}
|
||||
|
||||
// The parameter name is invalid.
|
||||
_component.Diagnostics.Add(ComponentDiagnosticFactory.Create_ChildContentHasInvalidParameterOnComponent(node.Source, node.AttributeName, _component.TagName));
|
||||
return;
|
||||
}
|
||||
|
||||
_children.Add(new ComponentAttributeExtensionNode(node));
|
||||
}
|
||||
|
||||
public override void VisitDefault(IntermediateNode node)
|
||||
{
|
||||
_children.Add(node);
|
||||
}
|
||||
}
|
||||
|
||||
private class ElementRewriteVisitor : IntermediateNodeWalker
|
||||
{
|
||||
private readonly IntermediateNodeCollection _children;
|
||||
|
||||
public ElementRewriteVisitor(IntermediateNodeCollection children)
|
||||
{
|
||||
_children = children;
|
||||
}
|
||||
|
||||
public override void VisitTagHelper(TagHelperIntermediateNode node)
|
||||
{
|
||||
// Visit children, we're replacing this node.
|
||||
for (var i = 0; i < node.Children.Count; i++)
|
||||
{
|
||||
Visit(node.Children[i]);
|
||||
}
|
||||
}
|
||||
|
||||
public override void VisitTagHelperBody(TagHelperBodyIntermediateNode node)
|
||||
{
|
||||
for (var i = 0; i < node.Children.Count; i++)
|
||||
{
|
||||
_children.Add(node.Children[i]);
|
||||
}
|
||||
}
|
||||
|
||||
public override void VisitTagHelperHtmlAttribute(TagHelperHtmlAttributeIntermediateNode node)
|
||||
{
|
||||
var attribute = new HtmlAttributeIntermediateNode()
|
||||
{
|
||||
AttributeName = node.AttributeName,
|
||||
Source = node.Source,
|
||||
};
|
||||
_children.Add(attribute);
|
||||
|
||||
for (var i = 0; i < node.Diagnostics.Count; i++)
|
||||
{
|
||||
attribute.Diagnostics.Add(node.Diagnostics[i]);
|
||||
}
|
||||
|
||||
switch (node.AttributeStructure)
|
||||
{
|
||||
case AttributeStructure.Minimized:
|
||||
|
||||
attribute.Prefix = node.AttributeName;
|
||||
attribute.Suffix = string.Empty;
|
||||
break;
|
||||
|
||||
case AttributeStructure.NoQuotes:
|
||||
case AttributeStructure.SingleQuotes:
|
||||
case AttributeStructure.DoubleQuotes:
|
||||
|
||||
// We're ignoring attribute structure here for simplicity, it doesn't effect us.
|
||||
attribute.Prefix = node.AttributeName + "=\"";
|
||||
attribute.Suffix = "\"";
|
||||
|
||||
for (var i = 0; i < node.Children.Count; i++)
|
||||
{
|
||||
attribute.Children.Add(RewriteAttributeContent(node.Children[i]));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
IntermediateNode RewriteAttributeContent(IntermediateNode content)
|
||||
{
|
||||
if (content is HtmlContentIntermediateNode html)
|
||||
{
|
||||
var value = new HtmlAttributeValueIntermediateNode()
|
||||
{
|
||||
Source = content.Source,
|
||||
};
|
||||
|
||||
for (var i = 0; i < html.Children.Count; i++)
|
||||
{
|
||||
value.Children.Add(html.Children[i]);
|
||||
}
|
||||
|
||||
for (var i = 0; i < html.Diagnostics.Count; i++)
|
||||
{
|
||||
value.Diagnostics.Add(html.Diagnostics[i]);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
public override void VisitTagHelperProperty(TagHelperPropertyIntermediateNode node)
|
||||
{
|
||||
// Each 'tag helper property' belongs to a specific tag helper. We want to handle
|
||||
// the cases for components, but leave others alone. This allows our other passes
|
||||
// to handle those cases.
|
||||
_children.Add(node.TagHelper.IsComponentTagHelper() ? (IntermediateNode)new ComponentAttributeExtensionNode(node) : node);
|
||||
}
|
||||
|
||||
public override void VisitDefault(IntermediateNode node)
|
||||
{
|
||||
_children.Add(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language.Components
|
||||
{
|
||||
internal class ComponentTypeArgumentExtensionNode : ExtensionIntermediateNode
|
||||
{
|
||||
public ComponentTypeArgumentExtensionNode(TagHelperPropertyIntermediateNode propertyNode)
|
||||
{
|
||||
if (propertyNode == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(propertyNode));
|
||||
}
|
||||
|
||||
BoundAttribute = propertyNode.BoundAttribute;
|
||||
Source = propertyNode.Source;
|
||||
TagHelper = propertyNode.TagHelper;
|
||||
|
||||
for (var i = 0; i < propertyNode.Children.Count; i++)
|
||||
{
|
||||
Children.Add(propertyNode.Children[i]);
|
||||
}
|
||||
|
||||
for (var i = 0; i < propertyNode.Diagnostics.Count; i++)
|
||||
{
|
||||
Diagnostics.Add(propertyNode.Diagnostics[i]);
|
||||
}
|
||||
}
|
||||
|
||||
public override IntermediateNodeCollection Children { get; } = new IntermediateNodeCollection();
|
||||
|
||||
public BoundAttributeDescriptor BoundAttribute { get; set; }
|
||||
|
||||
public string TypeParameterName => BoundAttribute.Name;
|
||||
|
||||
public TagHelperDescriptor TagHelper { get; set; }
|
||||
|
||||
public override void Accept(IntermediateNodeVisitor visitor)
|
||||
{
|
||||
if (visitor == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(visitor));
|
||||
}
|
||||
|
||||
AcceptExtensionNode<ComponentTypeArgumentExtensionNode>(this, visitor);
|
||||
}
|
||||
|
||||
public override void WriteNode(CodeTarget target, CodeRenderingContext context)
|
||||
{
|
||||
if (target == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(target));
|
||||
}
|
||||
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
var writer = (BlazorNodeWriter)context.NodeWriter;
|
||||
writer.WriteComponentTypeArgument(context, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a type-inference thunk that is used by the generated component code.
|
||||
/// </summary>
|
||||
internal class ComponentTypeInferenceMethodIntermediateNode : ExtensionIntermediateNode
|
||||
{
|
||||
public override IntermediateNodeCollection Children => IntermediateNodeCollection.ReadOnly;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the component usage linked to this type inference method.
|
||||
/// </summary>
|
||||
public ComponentExtensionNode Component { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the full type name of the generated class containing this method.
|
||||
/// </summary>
|
||||
public string FullTypeName { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the generated method.
|
||||
/// </summary>
|
||||
public string MethodName { get; set; }
|
||||
|
||||
public override void Accept(IntermediateNodeVisitor visitor)
|
||||
{
|
||||
if (visitor == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(visitor));
|
||||
}
|
||||
|
||||
AcceptExtensionNode<ComponentTypeInferenceMethodIntermediateNode>(this, visitor);
|
||||
}
|
||||
|
||||
public override void WriteNode(CodeTarget target, CodeRenderingContext context)
|
||||
{
|
||||
if (target == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(target));
|
||||
}
|
||||
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
var writer = (BlazorNodeWriter)context.NodeWriter;
|
||||
writer.WriteComponentTypeInferenceMethod(context, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language.Components
|
||||
{
|
||||
// Constants for method names used in code-generation
|
||||
// Keep these in sync with the actual definitions
|
||||
internal static class ComponentsApi
|
||||
{
|
||||
public static readonly string AssemblyName = "Microsoft.AspNetCore.Components";
|
||||
|
||||
public static class ComponentBase
|
||||
{
|
||||
public static readonly string Namespace = "Microsoft.AspNetCore.Components";
|
||||
public static readonly string FullTypeName = Namespace + ".ComponentBase";
|
||||
public static readonly string MetadataName = FullTypeName;
|
||||
|
||||
public static readonly string BuildRenderTree = nameof(BuildRenderTree);
|
||||
}
|
||||
|
||||
public static class ParameterAttribute
|
||||
{
|
||||
public static readonly string FullTypeName = "Microsoft.AspNetCore.Components.ParameterAttribute";
|
||||
public static readonly string MetadataName = FullTypeName;
|
||||
}
|
||||
|
||||
public static class LayoutAttribute
|
||||
{
|
||||
public static readonly string FullTypeName = "Microsoft.AspNetCore.Components.Layouts.LayoutAttribute";
|
||||
}
|
||||
|
||||
public static class InjectAttribute
|
||||
{
|
||||
public static readonly string FullTypeName = "Microsoft.AspNetCore.Components.InjectAttribute";
|
||||
}
|
||||
|
||||
public static class IComponent
|
||||
{
|
||||
public static readonly string FullTypeName = "Microsoft.AspNetCore.Components.IComponent";
|
||||
|
||||
public static readonly string MetadataName = FullTypeName;
|
||||
}
|
||||
|
||||
public static class IDictionary
|
||||
{
|
||||
public static readonly string MetadataName = "System.Collection.IDictionary`2";
|
||||
}
|
||||
|
||||
public static class RenderFragment
|
||||
{
|
||||
public static readonly string Namespace = "Microsoft.AspNetCore.Components";
|
||||
public static readonly string FullTypeName = Namespace + ".RenderFragment";
|
||||
public static readonly string MetadataName = FullTypeName;
|
||||
}
|
||||
|
||||
public static class RenderFragmentOfT
|
||||
{
|
||||
public static readonly string Namespace = "Microsoft.AspNetCore.Components";
|
||||
public static readonly string FullTypeName = Namespace + ".RenderFragment<>";
|
||||
public static readonly string MetadataName = Namespace + ".RenderFragment`1";
|
||||
}
|
||||
|
||||
public static class RenderTreeBuilder
|
||||
{
|
||||
public static readonly string FullTypeName = "Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder";
|
||||
|
||||
public static readonly string OpenElement = nameof(OpenElement);
|
||||
|
||||
public static readonly string CloseElement = nameof(CloseElement);
|
||||
|
||||
public static readonly string OpenComponent = nameof(OpenComponent);
|
||||
|
||||
public static readonly string CloseComponent = nameof(CloseComponent);
|
||||
|
||||
public static readonly string AddMarkupContent = nameof(AddMarkupContent);
|
||||
|
||||
public static readonly string AddContent = nameof(AddContent);
|
||||
|
||||
public static readonly string AddAttribute = nameof(AddAttribute);
|
||||
|
||||
public static readonly string AddElementReferenceCapture = nameof(AddElementReferenceCapture);
|
||||
|
||||
public static readonly string AddComponentReferenceCapture = nameof(AddComponentReferenceCapture);
|
||||
|
||||
public static readonly string Clear = nameof(Clear);
|
||||
|
||||
public static readonly string GetFrames = nameof(GetFrames);
|
||||
|
||||
public static readonly string ChildContent = nameof(ChildContent);
|
||||
}
|
||||
|
||||
public static class RuntimeHelpers
|
||||
{
|
||||
public static readonly string TypeCheck = "Microsoft.AspNetCore.Components.RuntimeHelpers.TypeCheck";
|
||||
}
|
||||
|
||||
public static class RouteAttribute
|
||||
{
|
||||
public static readonly string FullTypeName = "Microsoft.AspNetCore.Components.RouteAttribute";
|
||||
}
|
||||
|
||||
public static class BindElementAttribute
|
||||
{
|
||||
public static readonly string FullTypeName = "Microsoft.AspNetCore.Components.BindElementAttribute";
|
||||
}
|
||||
|
||||
public static class BindInputElementAttribute
|
||||
{
|
||||
public static readonly string FullTypeName = "Microsoft.AspNetCore.Components.BindInputElementAttribute";
|
||||
}
|
||||
|
||||
public static class BindMethods
|
||||
{
|
||||
public static readonly string FullTypeName = "Microsoft.AspNetCore.Components.BindMethods";
|
||||
|
||||
public static readonly string GetValue = "Microsoft.AspNetCore.Components.BindMethods.GetValue";
|
||||
|
||||
public static readonly string GetEventHandlerValue = "Microsoft.AspNetCore.Components.BindMethods.GetEventHandlerValue";
|
||||
|
||||
public static readonly string SetValueHandler = "Microsoft.AspNetCore.Components.BindMethods.SetValueHandler";
|
||||
}
|
||||
|
||||
public static class EventHandlerAttribute
|
||||
{
|
||||
public static readonly string FullTypeName = "Microsoft.AspNetCore.Components.EventHandlerAttribute";
|
||||
}
|
||||
|
||||
public static class ElementRef
|
||||
{
|
||||
public static readonly string FullTypeName = "Microsoft.AspNetCore.Components.ElementRef";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Razor.Language.Extensions;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language.Components
|
||||
{
|
||||
internal class EventHandlerLoweringPass : IntermediateNodePassBase, IRazorOptimizationPass
|
||||
{
|
||||
public override int Order => 50;
|
||||
|
||||
protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
|
||||
{
|
||||
var @namespace = documentNode.FindPrimaryNamespace();
|
||||
var @class = documentNode.FindPrimaryClass();
|
||||
if (@namespace == null || @class == null)
|
||||
{
|
||||
// Nothing to do, bail. We can't function without the standard structure.
|
||||
return;
|
||||
}
|
||||
|
||||
// For each event handler *usage* we need to rewrite the tag helper node to map to basic constructs.
|
||||
// Each usage will be represented by a tag helper property that is a descendant of either
|
||||
// a component or element.
|
||||
var references = documentNode.FindDescendantReferences<TagHelperPropertyIntermediateNode>();
|
||||
|
||||
var parents = new HashSet<IntermediateNode>();
|
||||
for (var i = 0; i < references.Count; i++)
|
||||
{
|
||||
parents.Add(references[i].Parent);
|
||||
}
|
||||
|
||||
foreach (var parent in parents)
|
||||
{
|
||||
ProcessDuplicates(parent);
|
||||
}
|
||||
|
||||
for (var i = 0; i < references.Count; i++)
|
||||
{
|
||||
var reference = references[i];
|
||||
var node = (TagHelperPropertyIntermediateNode)reference.Node;
|
||||
|
||||
if (!reference.Parent.Children.Contains(node))
|
||||
{
|
||||
// This node was removed as a duplicate, skip it.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (node.TagHelper.IsEventHandlerTagHelper())
|
||||
{
|
||||
reference.Replace(RewriteUsage(reference.Parent, node));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessDuplicates(IntermediateNode parent)
|
||||
{
|
||||
// Reverse order because we will remove nodes.
|
||||
//
|
||||
// Each 'property' node could be duplicated if there are multiple tag helpers that match that
|
||||
// particular attribute. This is likely to happen when a component also defines something like
|
||||
// OnClick. We want to remove the 'onclick' and let it fall back to be handled by the component.
|
||||
for (var i = parent.Children.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var eventHandler = parent.Children[i] as TagHelperPropertyIntermediateNode;
|
||||
if (eventHandler != null &&
|
||||
eventHandler.TagHelper != null &&
|
||||
eventHandler.TagHelper.IsEventHandlerTagHelper())
|
||||
{
|
||||
for (var j = 0; j < parent.Children.Count; j++)
|
||||
{
|
||||
var componentAttribute = parent.Children[j] as ComponentAttributeExtensionNode;
|
||||
if (componentAttribute != null &&
|
||||
componentAttribute.TagHelper != null &&
|
||||
componentAttribute.TagHelper.IsComponentTagHelper() &&
|
||||
componentAttribute.AttributeName == eventHandler.AttributeName)
|
||||
{
|
||||
// Found a duplicate - remove the 'fallback' in favor of the component's own handling.
|
||||
parent.Children.RemoveAt(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we still have duplicates at this point then they are genuine conflicts.
|
||||
var duplicates = parent.Children
|
||||
.OfType<TagHelperPropertyIntermediateNode>()
|
||||
.Where(p => p.TagHelper?.IsEventHandlerTagHelper() ?? false)
|
||||
.GroupBy(p => p.AttributeName)
|
||||
.Where(g => g.Count() > 1);
|
||||
|
||||
foreach (var duplicate in duplicates)
|
||||
{
|
||||
parent.Diagnostics.Add(ComponentDiagnosticFactory.CreateEventHandler_Duplicates(
|
||||
parent.Source,
|
||||
duplicate.Key,
|
||||
duplicate.ToArray()));
|
||||
foreach (var property in duplicate)
|
||||
{
|
||||
parent.Children.Remove(property);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IntermediateNode RewriteUsage(IntermediateNode parent, TagHelperPropertyIntermediateNode node)
|
||||
{
|
||||
var original = GetAttributeContent(node);
|
||||
if (original.Count == 0)
|
||||
{
|
||||
// This can happen in error cases, the parser will already have flagged this
|
||||
// as an error, so ignore it.
|
||||
return node;
|
||||
}
|
||||
|
||||
// Now rewrite the content of the value node to look like:
|
||||
//
|
||||
// BindMethods.GetEventHandlerValue<TDelegate>(<code>)
|
||||
//
|
||||
// This method is overloaded on string and TDelegate, which means that it will put the code in the
|
||||
// correct context for intellisense when typing in the attribute.
|
||||
var eventArgsType = node.TagHelper.GetEventArgsType();
|
||||
var tokens = new List<IntermediateToken>()
|
||||
{
|
||||
new IntermediateToken()
|
||||
{
|
||||
Content = $"{ComponentsApi.BindMethods.GetEventHandlerValue}<{eventArgsType}>(",
|
||||
Kind = TokenKind.CSharp
|
||||
},
|
||||
new IntermediateToken()
|
||||
{
|
||||
Content = $")",
|
||||
Kind = TokenKind.CSharp
|
||||
}
|
||||
};
|
||||
|
||||
for (var i = 0; i < original.Count; i++)
|
||||
{
|
||||
tokens.Insert(i + 1, original[i]);
|
||||
}
|
||||
|
||||
if (parent is HtmlElementIntermediateNode)
|
||||
{
|
||||
var result = new HtmlAttributeIntermediateNode()
|
||||
{
|
||||
AttributeName = node.AttributeName,
|
||||
Source = node.Source,
|
||||
|
||||
Prefix = node.AttributeName + "=\"",
|
||||
Suffix = "\"",
|
||||
};
|
||||
|
||||
for (var i = 0; i < node.Diagnostics.Count; i++)
|
||||
{
|
||||
result.Diagnostics.Add(node.Diagnostics[i]);
|
||||
}
|
||||
|
||||
result.Children.Add(new CSharpExpressionAttributeValueIntermediateNode());
|
||||
for (var i = 0; i < tokens.Count; i++)
|
||||
{
|
||||
result.Children[0].Children.Add(tokens[i]);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
var result = new ComponentAttributeExtensionNode(node);
|
||||
|
||||
result.Children.Clear();
|
||||
result.Children.Add(new CSharpExpressionIntermediateNode());
|
||||
for (var i = 0; i < tokens.Count; i++)
|
||||
{
|
||||
result.Children[0].Children.Add(tokens[i]);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private static IReadOnlyList<IntermediateToken> GetAttributeContent(TagHelperPropertyIntermediateNode node)
|
||||
{
|
||||
var template = node.FindDescendantNodes<TemplateIntermediateNode>().FirstOrDefault();
|
||||
if (template != null)
|
||||
{
|
||||
// See comments in TemplateDiagnosticPass
|
||||
node.Diagnostics.Add(ComponentDiagnosticFactory.Create_TemplateInvalidLocation(template.Source));
|
||||
return new[] { new IntermediateToken() { Kind = TokenKind.CSharp, Content = string.Empty, }, };
|
||||
}
|
||||
|
||||
if (node.Children.Count == 1 && node.Children[0] is HtmlContentIntermediateNode htmlContentNode)
|
||||
{
|
||||
// This case can be hit for a 'string' attribute. We want to turn it into
|
||||
// an expression.
|
||||
var tokens = htmlContentNode.FindDescendantNodes<IntermediateToken>();
|
||||
|
||||
var content = "\"" + string.Join(string.Empty, tokens.Select(t => t.Content.Replace("\"", "\\\""))) + "\"";
|
||||
return new[] { new IntermediateToken() { Content = content, Kind = TokenKind.CSharp, } };
|
||||
}
|
||||
else
|
||||
{
|
||||
return node.FindDescendantNodes<IntermediateToken>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,314 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language.Components
|
||||
{
|
||||
// This pass:
|
||||
// 1. Adds diagnostics for missing generic type arguments
|
||||
// 2. Rewrites the type name of the component to substitute generic type arguments
|
||||
// 3. Rewrites the type names of parameters/child content to substitute generic type arguments
|
||||
internal class GenericComponentPass : IntermediateNodePassBase, IRazorOptimizationPass
|
||||
{
|
||||
private TypeNameFeature _typeNameFeature;
|
||||
|
||||
// Runs after components/eventhandlers/ref/bind/templates. We want to validate every component
|
||||
// and it's usage of ChildContent.
|
||||
public override int Order => 160;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
_typeNameFeature = GetRequiredFeature<TypeNameFeature>();
|
||||
}
|
||||
|
||||
protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
|
||||
{
|
||||
var visitor = new Visitor(_typeNameFeature);
|
||||
visitor.Visit(documentNode);
|
||||
}
|
||||
|
||||
private class Visitor : IntermediateNodeWalker, IExtensionIntermediateNodeVisitor<ComponentExtensionNode>
|
||||
{
|
||||
private readonly TypeNameFeature _typeNameFeature;
|
||||
|
||||
// Incrementing ID for type inference method names
|
||||
private int _id;
|
||||
|
||||
public Visitor(TypeNameFeature typeNameFeature)
|
||||
{
|
||||
_typeNameFeature = typeNameFeature;
|
||||
}
|
||||
|
||||
public void VisitExtension(ComponentExtensionNode node)
|
||||
{
|
||||
if (node.Component.IsGenericTypedComponent())
|
||||
{
|
||||
// Not generic, ignore.
|
||||
Process(node);
|
||||
}
|
||||
|
||||
base.VisitDefault(node);
|
||||
}
|
||||
|
||||
private void Process(ComponentExtensionNode node)
|
||||
{
|
||||
// First collect all of the information we have about each type parameter
|
||||
//
|
||||
// Listing all type parameters that exist
|
||||
var bindings = new Dictionary<string, Binding>();
|
||||
foreach (var attribute in node.Component.GetTypeParameters())
|
||||
{
|
||||
bindings.Add(attribute.Name, new Binding() { Attribute = attribute, });
|
||||
}
|
||||
|
||||
// Listing all type arguments that have been specified.
|
||||
var hasTypeArgumentSpecified = false;
|
||||
foreach (var typeArgumentNode in node.TypeArguments)
|
||||
{
|
||||
hasTypeArgumentSpecified = true;
|
||||
|
||||
var binding = bindings[typeArgumentNode.TypeParameterName];
|
||||
binding.Node = typeArgumentNode;
|
||||
binding.Content = GetContent(typeArgumentNode);
|
||||
}
|
||||
|
||||
if (hasTypeArgumentSpecified)
|
||||
{
|
||||
// OK this means that the developer has specified at least one type parameter.
|
||||
// Either they specified everything and its OK to rewrite, or its an error.
|
||||
if (ValidateTypeArguments(node, bindings))
|
||||
{
|
||||
var mappings = bindings.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Content);
|
||||
RewriteTypeNames(_typeNameFeature.CreateGenericTypeRewriter(mappings), node);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// OK if we get here that means that no type arguments were specified, so we will try to infer
|
||||
// the type.
|
||||
//
|
||||
// The actual inference is done by the C# compiler, we just emit an a method that represents the
|
||||
// use of this component.
|
||||
|
||||
// Since we're generating code in a different namespace, we need to 'global qualify' all of the types
|
||||
// to avoid clashes with our generated code.
|
||||
RewriteTypeNames(_typeNameFeature.CreateGlobalQualifiedTypeNameRewriter(bindings.Keys), node);
|
||||
|
||||
//
|
||||
// We need to verify that an argument was provided that 'covers' each type parameter.
|
||||
//
|
||||
// For example, consider a repeater where the generic type is the 'item' type, but the developer has
|
||||
// not set the items. We won't be able to do type inference on this and so it will just be nonsense.
|
||||
var attributes = node.Attributes.Select(a => a.BoundAttribute).Concat(node.ChildContents.Select(c => c.BoundAttribute));
|
||||
foreach (var attribute in attributes)
|
||||
{
|
||||
if (attribute == null)
|
||||
{
|
||||
// Will be null for attributes set on the component that don't match a declared component parameter
|
||||
continue;
|
||||
}
|
||||
|
||||
// Now we need to parse the type name and extract the generic parameters.
|
||||
//
|
||||
// Two cases;
|
||||
// 1. name is a simple identifier like TItem
|
||||
// 2. name contains type parameters like Dictionary<string, TItem>
|
||||
if (!attribute.IsGenericTypedProperty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var typeParameters = _typeNameFeature.ParseTypeParameters(attribute.TypeName);
|
||||
if (typeParameters.Count == 0)
|
||||
{
|
||||
bindings.Remove(attribute.TypeName);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (var i = 0; i < typeParameters.Count; i++)
|
||||
{
|
||||
var typeParameter = typeParameters[i];
|
||||
bindings.Remove(typeParameter.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If any bindings remain then this means we would never be able to infer the arguments of this
|
||||
// component usage because the user hasn't set properties that include all of the types.
|
||||
if (bindings.Count > 0)
|
||||
{
|
||||
// However we still want to generate 'type inference' code because we want the errors to be as
|
||||
// helpful as possible. So let's substitute 'object' for all of those type parameters, and add
|
||||
// an error.
|
||||
var mappings = bindings.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Content);
|
||||
RewriteTypeNames(_typeNameFeature.CreateGenericTypeRewriter(mappings), node);
|
||||
|
||||
node.Diagnostics.Add(ComponentDiagnosticFactory.Create_GenericComponentTypeInferenceUnderspecified(node.Source, node, node.Component.GetTypeParameters()));
|
||||
}
|
||||
|
||||
// Next we need to generate a type inference 'method' node. This represents a method that we will codegen that
|
||||
// contains all of the operations on the render tree building. Calling a method to operate on the builder
|
||||
// will allow the C# compiler to perform type inference.
|
||||
var documentNode = (DocumentIntermediateNode)Ancestors[Ancestors.Count - 1];
|
||||
CreateTypeInferenceMethod(documentNode, node);
|
||||
}
|
||||
|
||||
private string GetContent(ComponentTypeArgumentExtensionNode node)
|
||||
{
|
||||
return string.Join(string.Empty, node.FindDescendantNodes<IntermediateToken>().Where(t => t.IsCSharp).Select(t => t.Content));
|
||||
}
|
||||
|
||||
private static bool ValidateTypeArguments(ComponentExtensionNode node, Dictionary<string, Binding> bindings)
|
||||
{
|
||||
var missing = new List<BoundAttributeDescriptor>();
|
||||
foreach (var binding in bindings)
|
||||
{
|
||||
if (binding.Value.Node == null || string.IsNullOrWhiteSpace(binding.Value.Content))
|
||||
{
|
||||
missing.Add(binding.Value.Attribute);
|
||||
}
|
||||
}
|
||||
|
||||
if (missing.Count > 0)
|
||||
{
|
||||
// We add our own error for this because its likely the user will see other errors due
|
||||
// to incorrect codegen without the types. Our errors message will pretty clearly indicate
|
||||
// what to do, whereas the other errors might be confusing.
|
||||
node.Diagnostics.Add(ComponentDiagnosticFactory.Create_GenericComponentMissingTypeArgument(node.Source, node, missing));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void RewriteTypeNames(TypeNameRewriter rewriter, ComponentExtensionNode node)
|
||||
{
|
||||
// Rewrite the component type name
|
||||
node.TypeName = rewriter.Rewrite(node.TypeName);
|
||||
|
||||
foreach (var attribute in node.Attributes)
|
||||
{
|
||||
if (attribute.BoundAttribute?.IsGenericTypedProperty() ?? false && attribute.TypeName != null)
|
||||
{
|
||||
// If we know the type name, then replace any generic type parameter inside it with
|
||||
// the known types.
|
||||
attribute.TypeName = rewriter.Rewrite(attribute.TypeName);
|
||||
}
|
||||
else if (attribute.TypeName == null && (attribute.BoundAttribute?.IsDelegateProperty() ?? false))
|
||||
{
|
||||
// This is a weakly typed delegate, treat it as Action<object>
|
||||
attribute.TypeName = "System.Action<System.Object>";
|
||||
}
|
||||
else if (attribute.TypeName == null)
|
||||
{
|
||||
// This is a weakly typed attribute, treat it as System.Object
|
||||
attribute.TypeName = "System.Object";
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var capture in node.Captures)
|
||||
{
|
||||
if (capture.IsComponentCapture && capture.ComponentCaptureTypeName != null)
|
||||
{
|
||||
capture.ComponentCaptureTypeName = rewriter.Rewrite(capture.ComponentCaptureTypeName);
|
||||
}
|
||||
else if (capture.IsComponentCapture)
|
||||
{
|
||||
capture.ComponentCaptureTypeName = "System.Object";
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var childContent in node.ChildContents)
|
||||
{
|
||||
if (childContent.BoundAttribute?.IsGenericTypedProperty() ?? false && childContent.TypeName != null)
|
||||
{
|
||||
// If we know the type name, then replace any generic type parameter inside it with
|
||||
// the known types.
|
||||
childContent.TypeName = rewriter.Rewrite(childContent.TypeName);
|
||||
}
|
||||
else if (childContent.IsParameterized)
|
||||
{
|
||||
// This is a weakly typed parameterized child content, treat it as RenderFragment<object>
|
||||
childContent.TypeName = ComponentsApi.RenderFragment.FullTypeName + "<System.Object>";
|
||||
}
|
||||
else
|
||||
{
|
||||
// This is a weakly typed child content, treat it as RenderFragment
|
||||
childContent.TypeName = ComponentsApi.RenderFragment.FullTypeName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateTypeInferenceMethod(DocumentIntermediateNode documentNode, ComponentExtensionNode node)
|
||||
{
|
||||
var @namespace = documentNode.FindPrimaryNamespace().Content;
|
||||
@namespace = string.IsNullOrEmpty(@namespace) ? "__Blazor" : "__Blazor." + @namespace;
|
||||
@namespace += "." + documentNode.FindPrimaryClass().ClassName;
|
||||
|
||||
var typeInferenceNode = new ComponentTypeInferenceMethodIntermediateNode()
|
||||
{
|
||||
Component = node,
|
||||
|
||||
// Method name is generated and guaranteed not to collide, since it's unique for each
|
||||
// component call site.
|
||||
MethodName = $"Create{node.TagName}_{_id++}",
|
||||
FullTypeName = @namespace + ".TypeInference",
|
||||
};
|
||||
|
||||
node.TypeInferenceNode = typeInferenceNode;
|
||||
|
||||
// Now we need to insert the type inference node into the tree.
|
||||
var namespaceNode = documentNode.Children
|
||||
.OfType<NamespaceDeclarationIntermediateNode>()
|
||||
.Where(n => n.Annotations.Contains(new KeyValuePair<object, object>(BlazorMetadata.Component.GenericTypedKey, bool.TrueString)))
|
||||
.FirstOrDefault();
|
||||
if (namespaceNode == null)
|
||||
{
|
||||
namespaceNode = new NamespaceDeclarationIntermediateNode()
|
||||
{
|
||||
Annotations =
|
||||
{
|
||||
{ BlazorMetadata.Component.GenericTypedKey, bool.TrueString },
|
||||
},
|
||||
Content = @namespace,
|
||||
};
|
||||
|
||||
documentNode.Children.Add(namespaceNode);
|
||||
}
|
||||
|
||||
var classNode = namespaceNode.Children
|
||||
.OfType<ClassDeclarationIntermediateNode>()
|
||||
.Where(n => n.ClassName == "TypeInference")
|
||||
.FirstOrDefault();
|
||||
if (classNode == null)
|
||||
{
|
||||
classNode = new ClassDeclarationIntermediateNode()
|
||||
{
|
||||
ClassName = "TypeInference",
|
||||
Modifiers =
|
||||
{
|
||||
"internal",
|
||||
"static",
|
||||
},
|
||||
};
|
||||
namespaceNode.Children.Add(classNode);
|
||||
}
|
||||
|
||||
classNode.Children.Add(typeInferenceNode);
|
||||
}
|
||||
}
|
||||
|
||||
private class Binding
|
||||
{
|
||||
public BoundAttributeDescriptor Attribute { get; set; }
|
||||
|
||||
public string Content { get; set; }
|
||||
|
||||
public ComponentTypeArgumentExtensionNode Node { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language.Components
|
||||
{
|
||||
[DebuggerDisplay("{DebuggerDisplay,nq}")]
|
||||
internal class HtmlBlockIntermediateNode : ExtensionIntermediateNode
|
||||
{
|
||||
public override IntermediateNodeCollection Children { get; } = new IntermediateNodeCollection();
|
||||
|
||||
public string Content { get; set; }
|
||||
|
||||
public override void Accept(IntermediateNodeVisitor visitor)
|
||||
{
|
||||
if (visitor == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(visitor));
|
||||
}
|
||||
|
||||
AcceptExtensionNode<HtmlBlockIntermediateNode>(this, visitor);
|
||||
}
|
||||
|
||||
public override void WriteNode(CodeTarget target, CodeRenderingContext context)
|
||||
{
|
||||
if (target == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(target));
|
||||
}
|
||||
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
var writer = (BlazorNodeWriter)context.NodeWriter;
|
||||
writer.WriteHtmlBlock(context, this);
|
||||
}
|
||||
|
||||
private string DebuggerDisplay => Content;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,331 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language.Components
|
||||
{
|
||||
// Rewrites contiguous subtrees of HTML into a special node type to reduce the
|
||||
// size of the Render tree.
|
||||
//
|
||||
// Does not preserve insignificant details of the HTML, like tag closing style
|
||||
// or quote style.
|
||||
internal class HtmlBlockPass : IntermediateNodePassBase, IRazorOptimizationPass
|
||||
{
|
||||
// Runs LATE because we want to destroy structure.
|
||||
public override int Order => 10000;
|
||||
|
||||
protected override void ExecuteCore(
|
||||
RazorCodeDocument codeDocument,
|
||||
DocumentIntermediateNode documentNode)
|
||||
{
|
||||
if (documentNode.Options.DesignTime)
|
||||
{
|
||||
// Nothing to do during design time.
|
||||
return;
|
||||
}
|
||||
|
||||
var findVisitor = new FindHtmlTreeVisitor();
|
||||
findVisitor.Visit(documentNode);
|
||||
|
||||
var trees = findVisitor.Trees;
|
||||
var rewriteVisitor = new RewriteVisitor(trees);
|
||||
while (trees.Count > 0)
|
||||
{
|
||||
// Walk backwards since we did a postorder traversal.
|
||||
var reference = trees[trees.Count - 1];
|
||||
|
||||
// Forcibly remove a node to prevent infinite loops.
|
||||
trees.RemoveAt(trees.Count - 1);
|
||||
|
||||
// We want to fold together siblings where possible. To do this, first we find
|
||||
// the index of the node we're looking at now - then we need to walk backwards
|
||||
// and identify a set of contiguous nodes we can merge.
|
||||
var start = reference.Parent.Children.Count - 1;
|
||||
for (; start >= 0; start--)
|
||||
{
|
||||
if (ReferenceEquals(reference.Node, reference.Parent.Children[start]))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// This is the current node. Check if the left sibling is always a candidate
|
||||
// for rewriting. Due to the order we processed the nodes, we know that the
|
||||
// left sibling is next in the list to process if it's a candidate.
|
||||
var end = start;
|
||||
while (start - 1 >= 0)
|
||||
{
|
||||
var candidate = reference.Parent.Children[start - 1];
|
||||
if (trees.Count == 0 || !ReferenceEquals(trees[trees.Count - 1].Node, candidate))
|
||||
{
|
||||
// This means the we're out of nodes, or the left sibling is not in the list.
|
||||
break;
|
||||
}
|
||||
|
||||
// This means that the left sibling is valid to merge.
|
||||
start--;
|
||||
|
||||
// Remove this since we're combining it.
|
||||
trees.RemoveAt(trees.Count - 1);
|
||||
}
|
||||
|
||||
// As a degenerate case, don't bother rewriting an single HtmlContent node
|
||||
// It doesn't add any value.
|
||||
if (end - start == 0 && reference.Node is HtmlContentIntermediateNode)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Now we know the range of nodes to rewrite (end is inclusive)
|
||||
var length = end + 1 - start;
|
||||
while (length > 0)
|
||||
{
|
||||
// Keep using start since we're removing nodes.
|
||||
var node = reference.Parent.Children[start];
|
||||
reference.Parent.Children.RemoveAt(start);
|
||||
|
||||
rewriteVisitor.Visit(node);
|
||||
|
||||
length--;
|
||||
}
|
||||
|
||||
reference.Parent.Children.Insert(start, new HtmlBlockIntermediateNode()
|
||||
{
|
||||
Content = rewriteVisitor.Builder.ToString(),
|
||||
});
|
||||
|
||||
rewriteVisitor.Builder.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Finds HTML-blocks using a postorder traversal. We store nodes in an
|
||||
// ordered list so we can avoid redundant rewrites.
|
||||
//
|
||||
// Consider a case like:
|
||||
// <div>
|
||||
// <a href="...">click me</a>
|
||||
// </div>
|
||||
//
|
||||
// We would store both the div and a tag in a list, but make sure to visit
|
||||
// the div first. Then when we process the div (recursively), we would remove
|
||||
// the a from the list.
|
||||
private class FindHtmlTreeVisitor :
|
||||
IntermediateNodeWalker,
|
||||
IExtensionIntermediateNodeVisitor<HtmlElementIntermediateNode>
|
||||
{
|
||||
private bool _foundNonHtml;
|
||||
|
||||
public List<IntermediateNodeReference> Trees { get; } = new List<IntermediateNodeReference>();
|
||||
|
||||
public override void VisitDefault(IntermediateNode node)
|
||||
{
|
||||
// If we get here, we found a non-HTML node. Keep traversing.
|
||||
_foundNonHtml = true;
|
||||
base.VisitDefault(node);
|
||||
}
|
||||
|
||||
public void VisitExtension(HtmlElementIntermediateNode node)
|
||||
{
|
||||
// We need to restore the state after processing this node.
|
||||
// We might have found a leaf-block of HTML, but that shouldn't
|
||||
// affect our parent's state.
|
||||
var originalState = _foundNonHtml;
|
||||
|
||||
_foundNonHtml = false;
|
||||
|
||||
if (node.HasDiagnostics)
|
||||
{
|
||||
// Treat node with errors as non-HTML - don't let the parent rewrite this either.
|
||||
_foundNonHtml = true;
|
||||
}
|
||||
|
||||
if (string.Equals("script", node.TagName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Treat script tags as non-HTML - we trigger errors for script tags
|
||||
// later.
|
||||
_foundNonHtml = true;
|
||||
}
|
||||
|
||||
base.VisitDefault(node);
|
||||
|
||||
if (!_foundNonHtml)
|
||||
{
|
||||
Trees.Add(new IntermediateNodeReference(Parent, node));
|
||||
}
|
||||
|
||||
_foundNonHtml = originalState |= _foundNonHtml;
|
||||
}
|
||||
|
||||
public override void VisitHtmlAttribute(HtmlAttributeIntermediateNode node)
|
||||
{
|
||||
if (node.HasDiagnostics)
|
||||
{
|
||||
// Treat node with errors as non-HTML
|
||||
_foundNonHtml = true;
|
||||
}
|
||||
|
||||
// Visit Children
|
||||
base.VisitDefault(node);
|
||||
}
|
||||
|
||||
public override void VisitHtmlAttributeValue(HtmlAttributeValueIntermediateNode node)
|
||||
{
|
||||
if (node.HasDiagnostics)
|
||||
{
|
||||
// Treat node with errors as non-HTML
|
||||
_foundNonHtml = true;
|
||||
}
|
||||
|
||||
// Visit Children
|
||||
base.VisitDefault(node);
|
||||
}
|
||||
|
||||
public override void VisitHtml(HtmlContentIntermediateNode node)
|
||||
{
|
||||
// We need to restore the state after processing this node.
|
||||
// We might have found a leaf-block of HTML, but that shouldn't
|
||||
// affect our parent's state.
|
||||
var originalState = _foundNonHtml;
|
||||
|
||||
_foundNonHtml = false;
|
||||
|
||||
if (node.HasDiagnostics)
|
||||
{
|
||||
// Treat node with errors as non-HTML
|
||||
_foundNonHtml = true;
|
||||
}
|
||||
|
||||
// Visit Children
|
||||
base.VisitDefault(node);
|
||||
|
||||
if (!_foundNonHtml)
|
||||
{
|
||||
Trees.Add(new IntermediateNodeReference(Parent, node));
|
||||
}
|
||||
|
||||
_foundNonHtml = originalState |= _foundNonHtml;
|
||||
}
|
||||
|
||||
public override void VisitToken(IntermediateToken node)
|
||||
{
|
||||
if (node.HasDiagnostics)
|
||||
{
|
||||
// Treat node with errors as non-HTML
|
||||
_foundNonHtml = true;
|
||||
}
|
||||
|
||||
if (node.IsCSharp)
|
||||
{
|
||||
_foundNonHtml = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class RewriteVisitor :
|
||||
IntermediateNodeWalker,
|
||||
IExtensionIntermediateNodeVisitor<HtmlElementIntermediateNode>
|
||||
{
|
||||
private readonly StringBuilder _encodingBuilder;
|
||||
|
||||
private readonly List<IntermediateNodeReference> _trees;
|
||||
|
||||
public RewriteVisitor(List<IntermediateNodeReference> trees)
|
||||
{
|
||||
_trees = trees;
|
||||
|
||||
_encodingBuilder = new StringBuilder();
|
||||
}
|
||||
|
||||
public StringBuilder Builder { get; } = new StringBuilder();
|
||||
|
||||
public void VisitExtension(HtmlElementIntermediateNode node)
|
||||
{
|
||||
for (var i = 0; i < _trees.Count; i++)
|
||||
{
|
||||
// Remove this node if it's in the list. This ensures that we don't
|
||||
// do redundant operations.
|
||||
if (ReferenceEquals(_trees[i].Node, node))
|
||||
{
|
||||
_trees.RemoveAt(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var isVoid = Legacy.ParserHelpers.VoidElements.Contains(node.TagName);
|
||||
var hasBodyContent = node.Body.Any();
|
||||
|
||||
Builder.Append("<");
|
||||
Builder.Append(node.TagName);
|
||||
|
||||
foreach (var attribute in node.Attributes)
|
||||
{
|
||||
Visit(attribute);
|
||||
}
|
||||
|
||||
// If for some reason a void element contains body, then treat it as a
|
||||
// start/end tag.
|
||||
if (!hasBodyContent && isVoid)
|
||||
{
|
||||
// void
|
||||
Builder.Append(">");
|
||||
return;
|
||||
}
|
||||
else if (!hasBodyContent)
|
||||
{
|
||||
// In HTML5, we can't have self-closing non-void elements, so explicitly
|
||||
// add a close tag
|
||||
Builder.Append("></");
|
||||
Builder.Append(node.TagName);
|
||||
Builder.Append(">");
|
||||
return;
|
||||
}
|
||||
|
||||
// start/end tag with body.
|
||||
Builder.Append(">");
|
||||
|
||||
foreach (var item in node.Body)
|
||||
{
|
||||
Visit(item);
|
||||
}
|
||||
|
||||
Builder.Append("</");
|
||||
Builder.Append(node.TagName);
|
||||
Builder.Append(">");
|
||||
}
|
||||
|
||||
public override void VisitHtmlAttribute(HtmlAttributeIntermediateNode node)
|
||||
{
|
||||
Builder.Append(" ");
|
||||
Builder.Append(node.AttributeName);
|
||||
|
||||
if (node.Children.Count == 0)
|
||||
{
|
||||
// Minimized attribute
|
||||
return;
|
||||
}
|
||||
|
||||
Builder.Append("=\"");
|
||||
|
||||
// Visit Children
|
||||
base.VisitDefault(node);
|
||||
|
||||
Builder.Append("\"");
|
||||
}
|
||||
|
||||
public override void VisitHtmlAttributeValue(HtmlAttributeValueIntermediateNode node)
|
||||
{
|
||||
Builder.Append(node.Children);
|
||||
}
|
||||
|
||||
public override void VisitHtml(HtmlContentIntermediateNode node)
|
||||
{
|
||||
Builder.Append(node.Children);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language.Components
|
||||
{
|
||||
[DebuggerDisplay("{DebuggerDisplay,nq}")]
|
||||
internal class HtmlElementIntermediateNode : ExtensionIntermediateNode
|
||||
{
|
||||
public IEnumerable<HtmlAttributeIntermediateNode> Attributes => Children.OfType<HtmlAttributeIntermediateNode>();
|
||||
|
||||
public IEnumerable<RefExtensionNode> Captures => Children.OfType<RefExtensionNode>();
|
||||
|
||||
public IEnumerable<IntermediateNode> Body => Children.Where(c =>
|
||||
{
|
||||
return
|
||||
c as HtmlAttributeIntermediateNode == null &&
|
||||
c as RefExtensionNode == null;
|
||||
});
|
||||
|
||||
public override IntermediateNodeCollection Children { get; } = new IntermediateNodeCollection();
|
||||
|
||||
public string TagName { get; set; }
|
||||
|
||||
public override void Accept(IntermediateNodeVisitor visitor)
|
||||
{
|
||||
if (visitor == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(visitor));
|
||||
}
|
||||
|
||||
AcceptExtensionNode<HtmlElementIntermediateNode>(this, visitor);
|
||||
}
|
||||
|
||||
public override void WriteNode(CodeTarget target, CodeRenderingContext context)
|
||||
{
|
||||
if (target == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(target));
|
||||
}
|
||||
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
var writer = (BlazorNodeWriter)context.NodeWriter;
|
||||
writer.WriteHtmlElement(context, this);
|
||||
}
|
||||
|
||||
private string DebuggerDisplay
|
||||
{
|
||||
get
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
builder.Append("Element: ");
|
||||
builder.Append("<");
|
||||
builder.Append(TagName);
|
||||
|
||||
foreach (var attribute in Attributes)
|
||||
{
|
||||
builder.Append(" ");
|
||||
builder.Append(attribute.AttributeName);
|
||||
builder.Append("=\"...\"");
|
||||
}
|
||||
|
||||
foreach (var capture in Captures)
|
||||
{
|
||||
builder.Append(" ");
|
||||
builder.Append("ref");
|
||||
builder.Append("=\"...\"");
|
||||
}
|
||||
|
||||
builder.Append(">");
|
||||
builder.Append(Body.Any() ? "..." : string.Empty);
|
||||
builder.Append("</");
|
||||
builder.Append(TagName);
|
||||
builder.Append(">");
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language.Components
|
||||
{
|
||||
internal static class ImplementsDirective
|
||||
{
|
||||
public static readonly DirectiveDescriptor Directive = DirectiveDescriptor.CreateDirective(
|
||||
"implements",
|
||||
DirectiveKind.SingleLine,
|
||||
builder =>
|
||||
{
|
||||
builder.AddTypeToken(ComponentResources.ImplementsDirective_TypeToken_Name, ComponentResources.ImplementsDirective_TypeToken_Description);
|
||||
builder.Usage = DirectiveUsage.FileScopedMultipleOccurring;
|
||||
builder.Description = ComponentResources.ImplementsDirective_Description;
|
||||
});
|
||||
|
||||
public static void Register(RazorProjectEngineBuilder builder)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
builder.AddDirective(Directive);
|
||||
builder.Features.Add(new ImplementsDirectivePass());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Language.Extensions;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language.Components
|
||||
{
|
||||
internal class ImplementsDirectivePass : IntermediateNodePassBase, IRazorDirectiveClassifierPass
|
||||
{
|
||||
protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
|
||||
{
|
||||
var @class = documentNode.FindPrimaryClass();
|
||||
if (@class == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (@class.Interfaces == null)
|
||||
{
|
||||
@class.Interfaces = new List<string>();
|
||||
}
|
||||
|
||||
foreach (var implements in documentNode.FindDirectiveReferences(ImplementsDirective.Directive))
|
||||
{
|
||||
var token = ((DirectiveIntermediateNode)implements.Node).Tokens.FirstOrDefault();
|
||||
if (token != null)
|
||||
{
|
||||
@class.Interfaces.Add(token.Content);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language.Components
|
||||
{
|
||||
// Much of the following is equivalent to Microsoft.AspNetCore.Mvc.Razor.Extensions's InjectDirective,
|
||||
// but this one outputs properties annotated for Blazor's property injector, plus it doesn't need to
|
||||
// support multiple CodeTargets.
|
||||
|
||||
internal class InjectDirective
|
||||
{
|
||||
public static readonly DirectiveDescriptor Directive = DirectiveDescriptor.CreateDirective(
|
||||
"inject",
|
||||
DirectiveKind.SingleLine,
|
||||
builder =>
|
||||
{
|
||||
builder.AddTypeToken("TypeName", "The type of the service to inject.");
|
||||
builder.AddMemberToken("PropertyName", "The name of the property.");
|
||||
builder.Usage = DirectiveUsage.FileScopedMultipleOccurring;
|
||||
builder.Description = "Inject a service from the application's service container into a property.";
|
||||
});
|
||||
|
||||
public static void Register(RazorProjectEngineBuilder builder)
|
||||
{
|
||||
builder.AddDirective(Directive);
|
||||
builder.Features.Add(new Pass());
|
||||
}
|
||||
|
||||
private class Pass : IntermediateNodePassBase, IRazorDirectiveClassifierPass
|
||||
{
|
||||
protected override void ExecuteCore(
|
||||
RazorCodeDocument codeDocument,
|
||||
DocumentIntermediateNode documentNode)
|
||||
{
|
||||
var visitor = new Visitor();
|
||||
visitor.Visit(documentNode);
|
||||
|
||||
var properties = new HashSet<string>(StringComparer.Ordinal);
|
||||
var classNode = documentNode.FindPrimaryClass();
|
||||
|
||||
for (var i = visitor.Directives.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var directive = visitor.Directives[i];
|
||||
var tokens = directive.Tokens.ToArray();
|
||||
if (tokens.Length < 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var typeName = tokens[0].Content;
|
||||
var memberName = tokens[1].Content;
|
||||
|
||||
if (!properties.Add(memberName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
classNode.Children.Add(new InjectIntermediateNode(typeName, memberName));
|
||||
}
|
||||
}
|
||||
|
||||
private class Visitor : IntermediateNodeWalker
|
||||
{
|
||||
public IList<DirectiveIntermediateNode> Directives { get; }
|
||||
= new List<DirectiveIntermediateNode>();
|
||||
|
||||
public override void VisitDirective(DirectiveIntermediateNode node)
|
||||
{
|
||||
if (node.Directive == Directive)
|
||||
{
|
||||
Directives.Add(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class InjectIntermediateNode : ExtensionIntermediateNode
|
||||
{
|
||||
private static readonly IList<string> _injectedPropertyModifiers = new[]
|
||||
{
|
||||
$"[global::{ComponentsApi.InjectAttribute.FullTypeName}]",
|
||||
"private" // Encapsulation is the default
|
||||
};
|
||||
|
||||
public string TypeName { get; }
|
||||
public string MemberName { get; }
|
||||
public override IntermediateNodeCollection Children => IntermediateNodeCollection.ReadOnly;
|
||||
|
||||
public InjectIntermediateNode(string typeName, string memberName)
|
||||
{
|
||||
TypeName = typeName;
|
||||
MemberName = memberName;
|
||||
}
|
||||
|
||||
public override void Accept(IntermediateNodeVisitor visitor)
|
||||
=> AcceptExtensionNode(this, visitor);
|
||||
|
||||
public override void WriteNode(CodeTarget target, CodeRenderingContext context)
|
||||
=> context.CodeWriter.WriteAutoPropertyDeclaration(
|
||||
_injectedPropertyModifiers,
|
||||
TypeName,
|
||||
MemberName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language.Components
|
||||
{
|
||||
internal static class LayoutDirective
|
||||
{
|
||||
public static readonly DirectiveDescriptor Directive = DirectiveDescriptor.CreateDirective(
|
||||
"layout",
|
||||
DirectiveKind.SingleLine,
|
||||
builder =>
|
||||
{
|
||||
builder.AddTypeToken(ComponentResources.LayoutDirective_TypeToken_Name, ComponentResources.LayoutDirective_TypeToken_Description);
|
||||
builder.Usage = DirectiveUsage.FileScopedSinglyOccurring;
|
||||
builder.Description = ComponentResources.LayoutDirective_Description;
|
||||
});
|
||||
|
||||
public static void Register(RazorProjectEngineBuilder builder)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
builder.AddDirective(Directive);
|
||||
builder.Features.Add(new LayoutDirectivePass());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language.Components
|
||||
{
|
||||
internal class LayoutDirectivePass : IntermediateNodePassBase, IRazorDirectiveClassifierPass
|
||||
{
|
||||
protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
|
||||
{
|
||||
var @namespace = documentNode.FindPrimaryNamespace();
|
||||
var @class = documentNode.FindPrimaryClass();
|
||||
if (@namespace == null || @class == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var directives = documentNode.FindDirectiveReferences(LayoutDirective.Directive);
|
||||
if (directives.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var token = ((DirectiveIntermediateNode)directives[0].Node).Tokens.FirstOrDefault();
|
||||
if (token == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var attributeNode = new CSharpCodeIntermediateNode();
|
||||
attributeNode.Children.Add(new IntermediateToken()
|
||||
{
|
||||
Kind = TokenKind.CSharp,
|
||||
Content = $"[{ComponentsApi.LayoutAttribute.FullTypeName}(typeof({token.Content}))]" + Environment.NewLine,
|
||||
});
|
||||
|
||||
// Insert the new attribute on top of the class
|
||||
for (var i = 0; i < @namespace.Children.Count; i++)
|
||||
{
|
||||
if (object.ReferenceEquals(@namespace.Children[i], @class))
|
||||
{
|
||||
@namespace.Children.Insert(i, attributeNode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0</TargetFrameworks>
|
||||
<TargetFrameworks Condition="'$(OS)' == 'Windows_NT'">$(TargetFrameworks);net461</TargetFrameworks>
|
||||
<RootNamespace>Microsoft.AspNetCore.Components.Razor</RootNamespace>
|
||||
<Description>Extensions to the Razor compiler to support building Razor Components.</Description>
|
||||
<IsProductPackage>true</IsProductPackage>
|
||||
|
||||
<!-- Copy package references to output, needed so the build project can find them -->
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AngleSharp" Version="0.9.9.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Razor.Language" Version="$(RazorPackageVersion)" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Razor" Version="$(RazorPackageVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Resources.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Resources.resx">
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\shared\ComponentsApi.cs" Link="shared\%(Filename)%(Extension)" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language.Components
|
||||
{
|
||||
internal class PageDirective
|
||||
{
|
||||
public static readonly DirectiveDescriptor Directive = DirectiveDescriptor.CreateDirective(
|
||||
"page",
|
||||
DirectiveKind.SingleLine,
|
||||
builder =>
|
||||
{
|
||||
builder.AddStringToken(ComponentResources.PageDirective_RouteToken_Name, ComponentResources.PageDirective_RouteToken_Description);
|
||||
builder.Usage = DirectiveUsage.FileScopedMultipleOccurring;
|
||||
builder.Description = ComponentResources.PageDirective_Description;
|
||||
});
|
||||
|
||||
private PageDirective(string routeTemplate, IntermediateNode directiveNode)
|
||||
{
|
||||
RouteTemplate = routeTemplate;
|
||||
DirectiveNode = directiveNode;
|
||||
}
|
||||
|
||||
public string RouteTemplate { get; }
|
||||
|
||||
public IntermediateNode DirectiveNode { get; }
|
||||
|
||||
public static RazorProjectEngineBuilder Register(RazorProjectEngineBuilder builder)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
builder.AddDirective(Directive);
|
||||
builder.Features.Add(new PageDirectivePass());
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language.Components
|
||||
{
|
||||
internal class PageDirectivePass : IntermediateNodePassBase, IRazorDirectiveClassifierPass
|
||||
{
|
||||
protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
|
||||
{
|
||||
if (codeDocument == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(codeDocument));
|
||||
}
|
||||
|
||||
if (documentNode == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(documentNode));
|
||||
}
|
||||
|
||||
var @namespace = documentNode.FindPrimaryNamespace();
|
||||
var @class = documentNode.FindPrimaryClass();
|
||||
if (@namespace == null || @class == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var directives = documentNode.FindDirectiveReferences(PageDirective.Directive);
|
||||
if (directives.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// We don't allow @page directives in imports
|
||||
for (var i = 0; i < directives.Count; i++)
|
||||
{
|
||||
var directive = directives[i];
|
||||
if (directive.Node.IsImported())
|
||||
{
|
||||
directive.Node.Diagnostics.Add(ComponentDiagnosticFactory.CreatePageDirective_CannotBeImported(directive.Node.Source.Value));
|
||||
}
|
||||
}
|
||||
|
||||
// Insert the attributes 'on-top' of the class declaration, since classes don't directly support attributes.
|
||||
var index = 0;
|
||||
for (; index < @namespace.Children.Count; index++)
|
||||
{
|
||||
if (object.ReferenceEquals(@class, @namespace.Children[index]))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < directives.Count; i++)
|
||||
{
|
||||
var pageDirective = (DirectiveIntermediateNode)directives[i].Node;
|
||||
|
||||
// The parser also adds errors for invalid syntax, we just need to not crash.
|
||||
var routeToken = pageDirective.Tokens.FirstOrDefault();
|
||||
|
||||
if (routeToken != null &&
|
||||
routeToken.Content.Length >= 3 &&
|
||||
routeToken.Content[0] == '\"' &&
|
||||
routeToken.Content[1] == '/' &&
|
||||
routeToken.Content[routeToken.Content.Length - 1] == '\"')
|
||||
{
|
||||
var template = routeToken.Content.Substring(1, routeToken.Content.Length - 2);
|
||||
@namespace.Children.Insert(index++, new RouteAttributeExtensionNode(template));
|
||||
}
|
||||
else
|
||||
{
|
||||
pageDirective.Diagnostics.Add(ComponentDiagnosticFactory.CreatePageDirective_MustSpecifyRoute(pageDirective.Source));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
// 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 System;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a fatal error during the transformation of a Blazor component from
|
||||
/// Razor source code to C# source code.
|
||||
/// </summary>
|
||||
public class RazorCompilerException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructs an instance of <see cref="RazorCompilerException"/>.
|
||||
/// </summary>
|
||||
/// <param name="diagnostic"></param>
|
||||
public RazorCompilerException(RazorDiagnostic diagnostic)
|
||||
{
|
||||
Diagnostic = diagnostic;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the diagnostic value.
|
||||
/// </summary>
|
||||
public RazorDiagnostic Diagnostic { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language.Components
|
||||
{
|
||||
internal class RefExtensionNode : ExtensionIntermediateNode
|
||||
{
|
||||
public RefExtensionNode(IntermediateToken identifierToken)
|
||||
{
|
||||
IdentifierToken = identifierToken ?? throw new ArgumentNullException(nameof(identifierToken));
|
||||
Source = IdentifierToken.Source;
|
||||
}
|
||||
|
||||
public RefExtensionNode(IntermediateToken identifierToken, string componentCaptureTypeName)
|
||||
: this(identifierToken)
|
||||
{
|
||||
if (string.IsNullOrEmpty(componentCaptureTypeName))
|
||||
{
|
||||
throw new ArgumentException("Cannot be null or empty", nameof(componentCaptureTypeName));
|
||||
}
|
||||
|
||||
IsComponentCapture = true;
|
||||
ComponentCaptureTypeName = componentCaptureTypeName;
|
||||
}
|
||||
|
||||
public override IntermediateNodeCollection Children => IntermediateNodeCollection.ReadOnly;
|
||||
|
||||
public IntermediateToken IdentifierToken { get; }
|
||||
|
||||
public bool IsComponentCapture { get; }
|
||||
|
||||
public string ComponentCaptureTypeName { get; set; }
|
||||
|
||||
public string TypeName => $"global::System.Action<{(IsComponentCapture ? ComponentCaptureTypeName : "global::" + ComponentsApi.ElementRef.FullTypeName)}>";
|
||||
|
||||
public override void Accept(IntermediateNodeVisitor visitor)
|
||||
{
|
||||
if (visitor == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(visitor));
|
||||
}
|
||||
|
||||
AcceptExtensionNode<RefExtensionNode>(this, visitor);
|
||||
}
|
||||
|
||||
public override void WriteNode(CodeTarget target, CodeRenderingContext context)
|
||||
{
|
||||
if (target == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(target));
|
||||
}
|
||||
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
var writer = (BlazorNodeWriter)context.NodeWriter;
|
||||
writer.WriteReferenceCapture(context, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
// 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.Intermediate;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language.Components
|
||||
{
|
||||
internal class RefLoweringPass : IntermediateNodePassBase, IRazorOptimizationPass
|
||||
{
|
||||
// Run after component lowering pass
|
||||
public override int Order => 50;
|
||||
|
||||
protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
|
||||
{
|
||||
var @namespace = documentNode.FindPrimaryNamespace();
|
||||
var @class = documentNode.FindPrimaryClass();
|
||||
if (@namespace == null || @class == null)
|
||||
{
|
||||
// Nothing to do, bail. We can't function without the standard structure.
|
||||
return;
|
||||
}
|
||||
|
||||
var references = documentNode.FindDescendantReferences<TagHelperPropertyIntermediateNode>();
|
||||
for (var i = 0; i < references.Count; i++)
|
||||
{
|
||||
var reference = references[i];
|
||||
var node = (TagHelperPropertyIntermediateNode)reference.Node;
|
||||
|
||||
if (node.TagHelper.IsRefTagHelper())
|
||||
{
|
||||
reference.Replace(RewriteUsage(@class, reference.Parent, node));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IntermediateNode RewriteUsage(ClassDeclarationIntermediateNode classNode, IntermediateNode parent, TagHelperPropertyIntermediateNode node)
|
||||
{
|
||||
// If we can't get a nonempty attribute name, do nothing because there will
|
||||
// already be a diagnostic for empty values
|
||||
var identifierToken = DetermineIdentifierToken(node);
|
||||
if (identifierToken == null)
|
||||
{
|
||||
return node;
|
||||
}
|
||||
|
||||
// Determine whether this is an element capture or a component capture, and
|
||||
// if applicable the type name that will appear in the resulting capture code
|
||||
var componentTagHelper = (parent as ComponentExtensionNode)?.Component;
|
||||
if (componentTagHelper != null)
|
||||
{
|
||||
return new RefExtensionNode(identifierToken, componentTagHelper.GetTypeName());
|
||||
}
|
||||
else
|
||||
{
|
||||
return new RefExtensionNode(identifierToken);
|
||||
}
|
||||
}
|
||||
|
||||
private IntermediateToken DetermineIdentifierToken(TagHelperPropertyIntermediateNode attributeNode)
|
||||
{
|
||||
IntermediateToken foundToken = null;
|
||||
|
||||
if (attributeNode.Children.Count == 1)
|
||||
{
|
||||
if (attributeNode.Children[0] is IntermediateToken token)
|
||||
{
|
||||
foundToken = token;
|
||||
}
|
||||
else if (attributeNode.Children[0] is CSharpExpressionIntermediateNode csharpNode)
|
||||
{
|
||||
if (csharpNode.Children.Count == 1)
|
||||
{
|
||||
foundToken = csharpNode.Children[0] as IntermediateToken;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return !string.IsNullOrWhiteSpace(foundToken?.Content) ? foundToken : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
// 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.CodeGeneration;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language.Components
|
||||
{
|
||||
internal class RouteAttributeExtensionNode : ExtensionIntermediateNode
|
||||
{
|
||||
public RouteAttributeExtensionNode(string template)
|
||||
{
|
||||
Template = template;
|
||||
}
|
||||
|
||||
public string Template { get; }
|
||||
|
||||
public override IntermediateNodeCollection Children => IntermediateNodeCollection.ReadOnly;
|
||||
|
||||
public override void Accept(IntermediateNodeVisitor visitor) => AcceptExtensionNode(this, visitor);
|
||||
|
||||
public override void WriteNode(CodeTarget target, CodeRenderingContext context)
|
||||
{
|
||||
context.CodeWriter.Write("[");
|
||||
context.CodeWriter.Write(ComponentsApi.RouteAttribute.FullTypeName);
|
||||
context.CodeWriter.Write("(\"");
|
||||
context.CodeWriter.Write(Template);
|
||||
context.CodeWriter.Write("\")");
|
||||
context.CodeWriter.Write("]");
|
||||
context.CodeWriter.WriteLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Keeps track of the nesting of elements/containers while writing out the C# source code
|
||||
/// for a component. This allows us to detect mismatched start/end tags, as well as inject
|
||||
/// additional C# source to capture component descendants in a lambda.
|
||||
/// </summary>
|
||||
internal class ScopeStack
|
||||
{
|
||||
private readonly Stack<ScopeEntry> _stack = new Stack<ScopeEntry>();
|
||||
private int _builderVarNumber = 1;
|
||||
|
||||
public string BuilderVarName { get; private set; } = "builder";
|
||||
|
||||
public void OpenComponentScope(CodeRenderingContext context, string name, string parameterName)
|
||||
{
|
||||
var scope = new ScopeEntry(name, ScopeKind.Component);
|
||||
_stack.Push(scope);
|
||||
|
||||
OffsetBuilderVarNumber(1);
|
||||
|
||||
// Writes code that looks like:
|
||||
//
|
||||
// ((__builder) => { ... })
|
||||
// OR
|
||||
// ((context) => (__builder) => { ... })
|
||||
|
||||
if (parameterName != null)
|
||||
{
|
||||
context.CodeWriter.Write($"({parameterName}) => ");
|
||||
}
|
||||
|
||||
scope.LambdaScope = context.CodeWriter.BuildLambda(BuilderVarName);
|
||||
}
|
||||
|
||||
public void OpenTemplateScope(CodeRenderingContext context)
|
||||
{
|
||||
var currentScope = new ScopeEntry("__template", ScopeKind.Template);
|
||||
_stack.Push(currentScope);
|
||||
|
||||
// Templates always get a lambda scope, because they are defined as a lambda.
|
||||
OffsetBuilderVarNumber(1);
|
||||
currentScope.LambdaScope = context.CodeWriter.BuildLambda(BuilderVarName);
|
||||
}
|
||||
|
||||
public void CloseScope(CodeRenderingContext context)
|
||||
{
|
||||
var currentScope = _stack.Pop();
|
||||
currentScope.LambdaScope.Dispose();
|
||||
OffsetBuilderVarNumber(-1);
|
||||
}
|
||||
|
||||
private void OffsetBuilderVarNumber(int delta)
|
||||
{
|
||||
_builderVarNumber += delta;
|
||||
BuilderVarName = _builderVarNumber == 1
|
||||
? "builder"
|
||||
: $"builder{_builderVarNumber}";
|
||||
}
|
||||
|
||||
private class ScopeEntry
|
||||
{
|
||||
public readonly string Name;
|
||||
public ScopeKind Kind;
|
||||
public int ChildCount;
|
||||
public IDisposable LambdaScope;
|
||||
|
||||
public ScopeEntry(string name, ScopeKind kind)
|
||||
{
|
||||
Name = name;
|
||||
Kind = kind;
|
||||
ChildCount = 0;
|
||||
}
|
||||
|
||||
public override string ToString() => $"<{Name}> ({Kind})";
|
||||
}
|
||||
|
||||
private enum ScopeKind
|
||||
{
|
||||
Component,
|
||||
Template,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language.Components
|
||||
{
|
||||
internal class ScriptTagPass : IntermediateNodePassBase, IRazorDocumentClassifierPass
|
||||
{
|
||||
// Run as soon as possible after the Component rewrite pass
|
||||
public override int Order => ComponentDocumentClassifierPass.DefaultFeatureOrder + 2;
|
||||
|
||||
protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
|
||||
{
|
||||
if (documentNode.DocumentKind != ComponentDocumentClassifierPass.ComponentDocumentKind)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var visitor = new Visitor();
|
||||
visitor.Visit(documentNode);
|
||||
}
|
||||
|
||||
private class Visitor : IntermediateNodeWalker, IExtensionIntermediateNodeVisitor<HtmlElementIntermediateNode>
|
||||
{
|
||||
public void VisitExtension(HtmlElementIntermediateNode node)
|
||||
{
|
||||
// Disallow <script> in components as per #552
|
||||
if (string.Equals(node.TagName, "script", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
for (var i = 0; i < node.Children.Count; i++)
|
||||
{
|
||||
// We allow you to suppress this error like:
|
||||
// <script suppress-error="BL9992" />
|
||||
var attribute = node.Children[i] as HtmlAttributeIntermediateNode;
|
||||
if (attribute != null &&
|
||||
attribute.AttributeName == "suppress-error" &&
|
||||
attribute.Children.Count == 1 &&
|
||||
attribute.Children[0] is HtmlAttributeValueIntermediateNode value &&
|
||||
value.Children.Count == 1 &&
|
||||
value.Children[0] is IntermediateToken token &&
|
||||
token.IsHtml &&
|
||||
string.Equals(token.Content, "BL9992", StringComparison.Ordinal))
|
||||
{
|
||||
node.Children.RemoveAt(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var diagnostic = ComponentDiagnosticFactory.Create_DisallowedScriptTag(node.Source);
|
||||
node.Diagnostics.Add(diagnostic);
|
||||
}
|
||||
|
||||
base.VisitDefault(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language.Components
|
||||
{
|
||||
internal static class TagHelperBoundAttributeDescriptorExtensions
|
||||
{
|
||||
public static bool IsDelegateProperty(this BoundAttributeDescriptor attribute)
|
||||
{
|
||||
if (attribute == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(attribute));
|
||||
}
|
||||
|
||||
var key = BlazorMetadata.Component.DelegateSignatureKey;
|
||||
return
|
||||
attribute.Metadata.TryGetValue(key, out var value) &&
|
||||
string.Equals(value, bool.TrueString);
|
||||
}
|
||||
|
||||
public static bool IsGenericTypedProperty(this BoundAttributeDescriptor attribute)
|
||||
{
|
||||
if (attribute == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(attribute));
|
||||
}
|
||||
|
||||
return
|
||||
attribute.Metadata.TryGetValue(BlazorMetadata.Component.GenericTypedKey, out var value) &&
|
||||
string.Equals(value, bool.TrueString);
|
||||
}
|
||||
|
||||
public static bool IsTypeParameterProperty(this BoundAttributeDescriptor attribute)
|
||||
{
|
||||
if (attribute == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(attribute));
|
||||
}
|
||||
|
||||
return
|
||||
attribute.Metadata.TryGetValue(BlazorMetadata.Component.TypeParameterKey, out var value) &&
|
||||
string.Equals(value, bool.TrueString);
|
||||
}
|
||||
|
||||
public static bool IsWeaklyTyped(this BoundAttributeDescriptor attribute)
|
||||
{
|
||||
if (attribute == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(attribute));
|
||||
}
|
||||
|
||||
var key = BlazorMetadata.Component.WeaklyTypedKey;
|
||||
return
|
||||
attribute.Metadata.TryGetValue(key, out var value) &&
|
||||
string.Equals(value, bool.TrueString);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value that indicates whether the property is a child content property. Properties are
|
||||
/// considered child content if they have the type <c>RenderFragment</c> or <c>RenderFragment{T}</c>.
|
||||
/// </summary>
|
||||
/// <param name="attribute">The <see cref="BoundAttributeDescriptor"/>.</param>
|
||||
/// <returns>Returns <c>true</c> if the property is child content, otherwise <c>false</c>.</returns>
|
||||
public static bool IsChildContentProperty(this BoundAttributeDescriptor attribute)
|
||||
{
|
||||
if (attribute == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(attribute));
|
||||
}
|
||||
|
||||
var key = BlazorMetadata.Component.ChildContentKey;
|
||||
return
|
||||
attribute.Metadata.TryGetValue(key, out var value) &&
|
||||
string.Equals(value, bool.TrueString);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value that indicates whether the property is a child content property. Properties are
|
||||
/// considered child content if they have the type <c>RenderFragment</c> or <c>RenderFragment{T}</c>.
|
||||
/// </summary>
|
||||
/// <param name="attribute">The <see cref="BoundAttributeDescriptorBuilder"/>.</param>
|
||||
/// <returns>Returns <c>true</c> if the property is child content, otherwise <c>false</c>.</returns>
|
||||
public static bool IsChildContentProperty(this BoundAttributeDescriptorBuilder attribute)
|
||||
{
|
||||
if (attribute == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(attribute));
|
||||
}
|
||||
|
||||
var key = BlazorMetadata.Component.ChildContentKey;
|
||||
return
|
||||
attribute.Metadata.TryGetValue(key, out var value) &&
|
||||
string.Equals(value, bool.TrueString);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value that indicates whether the property is a parameterized child content property. Properties are
|
||||
/// considered parameterized child content if they have the type <c>RenderFragment{T}</c> (for some T).
|
||||
/// </summary>
|
||||
/// <param name="attribute">The <see cref="BoundAttributeDescriptor"/>.</param>
|
||||
/// <returns>Returns <c>true</c> if the property is parameterized child content, otherwise <c>false</c>.</returns>
|
||||
public static bool IsParameterizedChildContentProperty(this BoundAttributeDescriptor attribute)
|
||||
{
|
||||
if (attribute == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(attribute));
|
||||
}
|
||||
|
||||
return attribute.IsChildContentProperty() &&
|
||||
!string.Equals(attribute.TypeName, ComponentsApi.RenderFragment.FullTypeName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value that indicates whether the property is a parameterized child content property. Properties are
|
||||
/// considered parameterized child content if they have the type <c>RenderFragment{T}</c> (for some T).
|
||||
/// </summary>
|
||||
/// <param name="attribute">The <see cref="BoundAttributeDescriptor"/>.</param>
|
||||
/// <returns>Returns <c>true</c> if the property is parameterized child content, otherwise <c>false</c>.</returns>
|
||||
public static bool IsParameterizedChildContentProperty(this BoundAttributeDescriptorBuilder attribute)
|
||||
{
|
||||
if (attribute == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(attribute));
|
||||
}
|
||||
|
||||
return attribute.IsChildContentProperty() &&
|
||||
!string.Equals(attribute.TypeName, ComponentsApi.RenderFragment.FullTypeName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value that indicates whether the property is used to specify the name of the parameter
|
||||
/// for a parameterized child content property.
|
||||
/// </summary>
|
||||
/// <param name="attribute">The <see cref="BoundAttributeDescriptor"/>.</param>
|
||||
/// <returns>
|
||||
/// Returns <c>true</c> if the property specifies the name of a parameter for a parameterized child content,
|
||||
/// otherwise <c>false</c>.
|
||||
/// </returns>
|
||||
public static bool IsChildContentParameterNameProperty(this BoundAttributeDescriptor attribute)
|
||||
{
|
||||
if (attribute == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(attribute));
|
||||
}
|
||||
|
||||
var key = BlazorMetadata.Component.ChildContentParameterNameKey;
|
||||
return
|
||||
attribute.Metadata.TryGetValue(key, out var value) &&
|
||||
string.Equals(value, bool.TrueString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,199 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language.Components
|
||||
{
|
||||
internal static class TagHelperDescriptorExtensions
|
||||
{
|
||||
public static bool IsBindTagHelper(this TagHelperDescriptor tagHelper)
|
||||
{
|
||||
if (tagHelper == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(tagHelper));
|
||||
}
|
||||
|
||||
return
|
||||
tagHelper.Metadata.TryGetValue(BlazorMetadata.SpecialKindKey, out var kind) &&
|
||||
string.Equals(BlazorMetadata.Bind.TagHelperKind, kind);
|
||||
}
|
||||
|
||||
public static bool IsFallbackBindTagHelper(this TagHelperDescriptor tagHelper)
|
||||
{
|
||||
if (tagHelper == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(tagHelper));
|
||||
}
|
||||
|
||||
return
|
||||
tagHelper.IsBindTagHelper() &&
|
||||
tagHelper.Metadata.TryGetValue(BlazorMetadata.Bind.FallbackKey, out var fallback) &&
|
||||
string.Equals(bool.TrueString, fallback);
|
||||
}
|
||||
|
||||
public static bool IsGenericTypedComponent(this TagHelperDescriptor tagHelper)
|
||||
{
|
||||
if (tagHelper == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(tagHelper));
|
||||
}
|
||||
|
||||
return
|
||||
IsComponentTagHelper(tagHelper) &&
|
||||
tagHelper.Metadata.TryGetValue(BlazorMetadata.Component.GenericTypedKey, out var value) &&
|
||||
string.Equals(bool.TrueString, value);
|
||||
}
|
||||
|
||||
public static bool IsInputElementBindTagHelper(this TagHelperDescriptor tagHelper)
|
||||
{
|
||||
if (tagHelper == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(tagHelper));
|
||||
}
|
||||
|
||||
return
|
||||
tagHelper.IsBindTagHelper() &&
|
||||
tagHelper.TagMatchingRules.Count == 1 &&
|
||||
string.Equals("input", tagHelper.TagMatchingRules[0].TagName);
|
||||
}
|
||||
|
||||
public static bool IsInputElementFallbackBindTagHelper(this TagHelperDescriptor tagHelper)
|
||||
{
|
||||
if (tagHelper == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(tagHelper));
|
||||
}
|
||||
|
||||
return
|
||||
tagHelper.IsInputElementBindTagHelper() &&
|
||||
!tagHelper.Metadata.ContainsKey(BlazorMetadata.Bind.TypeAttribute);
|
||||
}
|
||||
|
||||
public static string GetValueAttributeName(this TagHelperDescriptor tagHelper)
|
||||
{
|
||||
if (tagHelper == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(tagHelper));
|
||||
}
|
||||
|
||||
tagHelper.Metadata.TryGetValue(BlazorMetadata.Bind.ValueAttribute, out var result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static string GetChangeAttributeName(this TagHelperDescriptor tagHelper)
|
||||
{
|
||||
if (tagHelper == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(tagHelper));
|
||||
}
|
||||
|
||||
tagHelper.Metadata.TryGetValue(BlazorMetadata.Bind.ChangeAttribute, out var result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static bool IsChildContentTagHelper(this TagHelperDescriptor tagHelper)
|
||||
{
|
||||
if (tagHelper == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(tagHelper));
|
||||
}
|
||||
|
||||
return
|
||||
tagHelper.Metadata.TryGetValue(BlazorMetadata.SpecialKindKey, out var value) &&
|
||||
string.Equals(value, BlazorMetadata.ChildContent.TagHelperKind, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
public static bool IsComponentTagHelper(this TagHelperDescriptor tagHelper)
|
||||
{
|
||||
if (tagHelper == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(tagHelper));
|
||||
}
|
||||
|
||||
return !tagHelper.Metadata.ContainsKey(BlazorMetadata.SpecialKindKey);
|
||||
}
|
||||
|
||||
public static bool IsEventHandlerTagHelper(this TagHelperDescriptor tagHelper)
|
||||
{
|
||||
if (tagHelper == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(tagHelper));
|
||||
}
|
||||
|
||||
return
|
||||
tagHelper.Metadata.TryGetValue(BlazorMetadata.SpecialKindKey, out var kind) &&
|
||||
string.Equals(BlazorMetadata.EventHandler.TagHelperKind, kind);
|
||||
}
|
||||
|
||||
public static bool IsRefTagHelper(this TagHelperDescriptor tagHelper)
|
||||
{
|
||||
if (tagHelper == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(tagHelper));
|
||||
}
|
||||
|
||||
return
|
||||
tagHelper.Metadata.TryGetValue(BlazorMetadata.SpecialKindKey, out var kind) &&
|
||||
string.Equals(BlazorMetadata.Ref.TagHelperKind, kind);
|
||||
}
|
||||
|
||||
public static string GetEventArgsType(this TagHelperDescriptor tagHelper)
|
||||
{
|
||||
if (tagHelper == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(tagHelper));
|
||||
}
|
||||
|
||||
tagHelper.Metadata.TryGetValue(BlazorMetadata.EventHandler.EventArgsType, out var result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the set of component attributes that can accept child content (<c>RenderFragment</c> or <c>RenderFragment{T}</c>).
|
||||
/// </summary>
|
||||
/// <param name="tagHelper">The <see cref="TagHelperDescriptor"/>.</param>
|
||||
/// <returns>The child content attributes</returns>
|
||||
public static IEnumerable<BoundAttributeDescriptor> GetChildContentProperties(this TagHelperDescriptor tagHelper)
|
||||
{
|
||||
if (tagHelper == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(tagHelper));
|
||||
}
|
||||
|
||||
for (var i = 0; i < tagHelper.BoundAttributes.Count; i++)
|
||||
{
|
||||
var attribute = tagHelper.BoundAttributes[i];
|
||||
if (attribute.IsChildContentProperty())
|
||||
{
|
||||
yield return attribute;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the set of component attributes that represent generic type parameters of the component type.
|
||||
/// </summary>
|
||||
/// <param name="tagHelper">The <see cref="TagHelperDescriptor"/>.</param>
|
||||
/// <returns>The type parameter attributes</returns>
|
||||
public static IEnumerable<BoundAttributeDescriptor> GetTypeParameters(this TagHelperDescriptor tagHelper)
|
||||
{
|
||||
if (tagHelper == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(tagHelper));
|
||||
}
|
||||
|
||||
for (var i = 0; i < tagHelper.BoundAttributes.Count; i++)
|
||||
{
|
||||
var attribute = tagHelper.BoundAttributes[i];
|
||||
if (attribute.IsTypeParameterProperty())
|
||||
{
|
||||
yield return attribute;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
// 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.Extensions;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language.Components
|
||||
{
|
||||
internal class TemplateDiagnosticPass : IntermediateNodePassBase, IRazorOptimizationPass
|
||||
{
|
||||
// Runs after components/eventhandlers/ref/bind. We need to check for templates in all of those
|
||||
// places.
|
||||
public override int Order => 150;
|
||||
|
||||
protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
|
||||
{
|
||||
var visitor = new Visitor();
|
||||
visitor.Visit(documentNode);
|
||||
|
||||
for (var i = 0; i < visitor.Candidates.Count; i++)
|
||||
{
|
||||
var candidate = visitor.Candidates[i];
|
||||
candidate.Parent.Diagnostics.Add(ComponentDiagnosticFactory.Create_TemplateInvalidLocation(candidate.Node.Source));
|
||||
|
||||
// Remove the offending node since we don't know how to render it. This means that the user won't get C#
|
||||
// completion at this location, which is fine because it's inside an HTML attribute.
|
||||
candidate.Remove();
|
||||
}
|
||||
}
|
||||
|
||||
private class Visitor : IntermediateNodeWalker, IExtensionIntermediateNodeVisitor<TemplateIntermediateNode>
|
||||
{
|
||||
public List<IntermediateNodeReference> Candidates { get; } = new List<IntermediateNodeReference>();
|
||||
|
||||
public void VisitExtension(TemplateIntermediateNode node)
|
||||
{
|
||||
// We found a template, let's check where it's located.
|
||||
for (var i = 0; i < Ancestors.Count; i++)
|
||||
{
|
||||
var ancestor = Ancestors[i];
|
||||
|
||||
if (
|
||||
// Inside markup attribute
|
||||
ancestor is HtmlAttributeIntermediateNode ||
|
||||
|
||||
// Inside component attribute
|
||||
ancestor is ComponentAttributeExtensionNode ||
|
||||
|
||||
// Inside malformed ref attribute
|
||||
ancestor is TagHelperPropertyIntermediateNode)
|
||||
{
|
||||
Candidates.Add(new IntermediateNodeReference(Parent, node));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
// 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.Intermediate;
|
||||
using System;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language.Components
|
||||
{
|
||||
internal class TrimWhitespacePass : IntermediateNodePassBase, IRazorDirectiveClassifierPass
|
||||
{
|
||||
protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
|
||||
{
|
||||
if (codeDocument == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(codeDocument));
|
||||
}
|
||||
|
||||
if (documentNode == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(documentNode));
|
||||
}
|
||||
|
||||
// There's no benefit running the whitespace trimmer during design-time builds
|
||||
if (!documentNode.Options.DesignTime)
|
||||
{
|
||||
var method = documentNode.FindPrimaryMethod();
|
||||
if (method != null)
|
||||
{
|
||||
RemoveContiguousWhitespace(method.Children, TraversalDirection.Forwards);
|
||||
RemoveContiguousWhitespace(method.Children, TraversalDirection.Backwards);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void RemoveContiguousWhitespace(IntermediateNodeCollection nodes, TraversalDirection direction)
|
||||
{
|
||||
var position = direction == TraversalDirection.Forwards ? 0 : nodes.Count - 1;
|
||||
while (position >= 0 && position < nodes.Count)
|
||||
{
|
||||
var node = nodes[position];
|
||||
bool shouldRemoveNode;
|
||||
bool shouldContinueIteration;
|
||||
|
||||
switch (node)
|
||||
{
|
||||
case IntermediateToken intermediateToken:
|
||||
shouldRemoveNode = string.IsNullOrWhiteSpace(intermediateToken.Content);
|
||||
shouldContinueIteration = shouldRemoveNode;
|
||||
break;
|
||||
|
||||
case HtmlContentIntermediateNode htmlContentIntermediateNode:
|
||||
RemoveContiguousWhitespace(htmlContentIntermediateNode.Children, direction);
|
||||
shouldRemoveNode = htmlContentIntermediateNode.Children.Count == 0;
|
||||
shouldContinueIteration = shouldRemoveNode;
|
||||
break;
|
||||
|
||||
case HtmlElementIntermediateNode _:
|
||||
case CSharpExpressionIntermediateNode _:
|
||||
case TagHelperIntermediateNode _:
|
||||
// These node types may produce non-whitespace output at runtime
|
||||
shouldRemoveNode = false;
|
||||
shouldContinueIteration = false;
|
||||
break;
|
||||
|
||||
case CSharpCodeIntermediateNode codeIntermediateNode:
|
||||
shouldRemoveNode = false;
|
||||
shouldContinueIteration = ComponentDocumentClassifierPass.IsBuildRenderTreeBaseCall(codeIntermediateNode);
|
||||
break;
|
||||
|
||||
default:
|
||||
shouldRemoveNode = false;
|
||||
shouldContinueIteration = true; // Because other types of nodes don't produce output
|
||||
break;
|
||||
}
|
||||
|
||||
if (shouldRemoveNode)
|
||||
{
|
||||
nodes.RemoveAt(position);
|
||||
if (direction == TraversalDirection.Forwards)
|
||||
{
|
||||
position--;
|
||||
}
|
||||
}
|
||||
|
||||
position += direction == TraversalDirection.Forwards ? 1 : -1;
|
||||
|
||||
if (!shouldContinueIteration)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum TraversalDirection
|
||||
{
|
||||
Forwards,
|
||||
Backwards
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language.Components
|
||||
{
|
||||
internal class TypeParamDirective
|
||||
{
|
||||
public static readonly DirectiveDescriptor Directive = DirectiveDescriptor.CreateDirective(
|
||||
"typeparam",
|
||||
DirectiveKind.SingleLine,
|
||||
builder =>
|
||||
{
|
||||
builder.AddMemberToken(ComponentResources.TypeParamDirective_Token_Name, ComponentResources.TypeParamDirective_Token_Description);
|
||||
builder.Usage = DirectiveUsage.FileScopedMultipleOccurring;
|
||||
builder.Description = ComponentResources.TypeParamDirective_Description;
|
||||
});
|
||||
|
||||
public static RazorProjectEngineBuilder Register(RazorProjectEngineBuilder builder)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
builder.AddDirective(Directive);
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -54,6 +54,19 @@ namespace Microsoft.AspNetCore.Razor.Language.Intermediate
|
|||
visitor.Visit(node);
|
||||
return visitor.Directives;
|
||||
}
|
||||
|
||||
public static IReadOnlyList<IntermediateNodeReference> FindDescendantReferences<TNode>(this DocumentIntermediateNode document)
|
||||
where TNode : IntermediateNode
|
||||
{
|
||||
if (document == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(document));
|
||||
}
|
||||
|
||||
var visitor = new ReferenceVisitor<TNode>();
|
||||
visitor.Visit(document);
|
||||
return visitor.References;
|
||||
}
|
||||
|
||||
private static T FindWithAnnotation<T>(IntermediateNode node, object annotation) where T : IntermediateNode
|
||||
{
|
||||
|
|
@ -95,5 +108,25 @@ namespace Microsoft.AspNetCore.Razor.Language.Intermediate
|
|||
base.VisitDirective(node);
|
||||
}
|
||||
}
|
||||
|
||||
private class ReferenceVisitor<TNode> : IntermediateNodeWalker
|
||||
where TNode : IntermediateNode
|
||||
{
|
||||
public List<IntermediateNodeReference> References = new List<IntermediateNodeReference>();
|
||||
|
||||
public override void VisitDefault(IntermediateNode node)
|
||||
{
|
||||
base.VisitDefault(node);
|
||||
|
||||
// Use a post-order traversal because references are used to replace nodes, and thus
|
||||
// change the parent nodes.
|
||||
//
|
||||
// This ensures that we always operate on the leaf nodes first.
|
||||
if (node is TNode)
|
||||
{
|
||||
References.Add(new IntermediateNodeReference(Parent, node));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language
|
||||
{
|
||||
// Razor.Language doesn't reference Microsoft.CodeAnalysis.CSharp so we
|
||||
// need some indirection.
|
||||
internal abstract class TypeNameFeature : RazorEngineFeatureBase
|
||||
{
|
||||
public abstract IReadOnlyList<string> ParseTypeParameters(string typeName);
|
||||
|
||||
public abstract TypeNameRewriter CreateGenericTypeRewriter(Dictionary<string, string> bindings);
|
||||
|
||||
public abstract TypeNameRewriter CreateGlobalQualifiedTypeNameRewriter(ICollection<string> ignore);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language
|
||||
{
|
||||
// Razor.Language doesn't reference Microsoft.CodeAnalysis.CSharp so we
|
||||
// need some indirection.
|
||||
internal abstract class TypeNameRewriter
|
||||
{
|
||||
public abstract string Rewrite(string typeName);
|
||||
}
|
||||
}
|
||||
|
|
@ -5,10 +5,8 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
using Microsoft.Extensions.CommandLineUtils;
|
||||
using Microsoft.VisualStudio.LanguageServices.Razor.Serialization;
|
||||
using Newtonsoft.Json;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,490 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Language.Components;
|
||||
|
||||
namespace Microsoft.CodeAnalysis.Razor
|
||||
{
|
||||
internal class BindTagHelperDescriptorProvider : ITagHelperDescriptorProvider
|
||||
{
|
||||
// Run after the component tag helper provider, because we need to see the results.
|
||||
public int Order { get; set; } = 1000;
|
||||
|
||||
public RazorEngine Engine { get; set; }
|
||||
|
||||
public void Execute(TagHelperDescriptorProviderContext context)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
// This provider returns tag helper information for 'bind' which doesn't necessarily
|
||||
// map to any real component. Bind behaviors more like a macro, which can map a single LValue to
|
||||
// both a 'value' attribute and a 'value changed' attribute.
|
||||
//
|
||||
// User types:
|
||||
// <input type="text" bind="@FirstName"/>
|
||||
//
|
||||
// We generate:
|
||||
// <input type="text"
|
||||
// value="@BindMethods.GetValue(FirstName)"
|
||||
// onchange="@BindMethods.SetValue(__value => FirstName = __value, FirstName)"/>
|
||||
//
|
||||
// This isn't very different from code the user could write themselves - thus the pronouncement
|
||||
// that bind is very much like a macro.
|
||||
//
|
||||
// A lot of the value that provide in this case is that the associations between the
|
||||
// elements, and the attributes aren't straightforward.
|
||||
//
|
||||
// For instance on <input type="text" /> we need to listen to 'value' and 'onchange',
|
||||
// but on <input type="checked" we need to listen to 'checked' and 'onchange'.
|
||||
//
|
||||
// We handle a few different cases here:
|
||||
//
|
||||
// 1. When given an attribute like **anywhere**'bind-value-onchange="@FirstName"' we will
|
||||
// generate the 'value' attribute and 'onchange' attribute.
|
||||
//
|
||||
// We don't do any transformation or inference for this case, because the developer has
|
||||
// told us exactly what to do. This is the *full* form of bind, and should support any
|
||||
// combination of element, component, and attributes.
|
||||
//
|
||||
// This is the most general case, and is implemented with a built-in tag helper that applies
|
||||
// to everything, and binds to a dictionary of attributes that start with bind-.
|
||||
//
|
||||
// 2. We also support cases like 'bind-value="@FirstName"' where we will generate the 'value'
|
||||
// attribute and another attribute based for a changed handler based on the metadata.
|
||||
//
|
||||
// These mappings are provided by attributes that tell us what attributes, suffixes, and
|
||||
// elements to map.
|
||||
//
|
||||
// 3. When given an attribute like 'bind="@FirstName"' we will generate a value and change
|
||||
// attribute solely based on the context. We need the context of an HTML tag to know
|
||||
// what attributes to generate.
|
||||
//
|
||||
// Similar to case #2, this should 'just work' from the users point of view. We expect
|
||||
// using this syntax most frequently with input elements.
|
||||
//
|
||||
// These mappings are also provided by attributes. Primarily these are used by <input />
|
||||
// and so we have a special case for input elements and their type attributes.
|
||||
//
|
||||
// 4. For components, we have a bit of a special case. We can infer a syntax that matches
|
||||
// case #2 based on property names. So if a component provides both 'Value' and 'ValueChanged'
|
||||
// we will turn that into an instance of bind.
|
||||
//
|
||||
// So case #1 here is the most general case. Case #2 and #3 are data-driven based on attribute data
|
||||
// we have. Case #4 is data-driven based on component definitions.
|
||||
//
|
||||
// We provide a good set of attributes that map to the HTML dom. This set is user extensible.
|
||||
var compilation = context.GetCompilation();
|
||||
if (compilation == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var bindMethods = compilation.GetTypeByMetadataName(ComponentsApi.BindMethods.FullTypeName);
|
||||
if (bindMethods == null)
|
||||
{
|
||||
// If we can't find BindMethods, then just bail. We won't be able to compile the
|
||||
// generated code anyway.
|
||||
return;
|
||||
}
|
||||
|
||||
// Tag Helper defintion for case #1. This is the most general case.
|
||||
context.Results.Add(CreateFallbackBindTagHelper());
|
||||
|
||||
// For case #2 & #3 we have a whole bunch of attribute entries on BindMethods that we can use
|
||||
// to data-drive the definitions of these tag helpers.
|
||||
var elementBindData = GetElementBindData(compilation);
|
||||
|
||||
// Case #2 & #3
|
||||
foreach (var tagHelper in CreateElementBindTagHelpers(elementBindData))
|
||||
{
|
||||
context.Results.Add(tagHelper);
|
||||
}
|
||||
|
||||
// For case #4 we look at the tag helpers that were already created corresponding to components
|
||||
// and pattern match on properties.
|
||||
foreach (var tagHelper in CreateComponentBindTagHelpers(context.Results))
|
||||
{
|
||||
context.Results.Add(tagHelper);
|
||||
}
|
||||
}
|
||||
|
||||
private TagHelperDescriptor CreateFallbackBindTagHelper()
|
||||
{
|
||||
var builder = TagHelperDescriptorBuilder.Create(BlazorMetadata.Bind.TagHelperKind, "Bind", ComponentsApi.AssemblyName);
|
||||
builder.Documentation = ComponentResources.BindTagHelper_Fallback_Documentation;
|
||||
|
||||
builder.Metadata.Add(BlazorMetadata.SpecialKindKey, BlazorMetadata.Bind.TagHelperKind);
|
||||
builder.Metadata[TagHelperMetadata.Runtime.Name] = BlazorMetadata.Bind.RuntimeName;
|
||||
builder.Metadata[BlazorMetadata.Bind.FallbackKey] = bool.TrueString;
|
||||
|
||||
// WTE has a bug in 15.7p1 where a Tag Helper without a display-name that looks like
|
||||
// a C# property will crash trying to create the toolips.
|
||||
builder.SetTypeName("Microsoft.AspNetCore.Components.Bind");
|
||||
|
||||
builder.TagMatchingRule(rule =>
|
||||
{
|
||||
rule.TagName = "*";
|
||||
rule.Attribute(attribute =>
|
||||
{
|
||||
attribute.Name = "bind-";
|
||||
attribute.NameComparisonMode = RequiredAttributeDescriptor.NameComparisonMode.PrefixMatch;
|
||||
});
|
||||
});
|
||||
|
||||
builder.BindAttribute(attribute =>
|
||||
{
|
||||
attribute.Documentation = ComponentResources.BindTagHelper_Fallback_Documentation;
|
||||
|
||||
attribute.Name = "bind-...";
|
||||
attribute.AsDictionary("bind-", typeof(object).FullName);
|
||||
|
||||
// WTE has a bug 15.7p1 where a Tag Helper without a display-name that looks like
|
||||
// a C# property will crash trying to create the toolips.
|
||||
attribute.SetPropertyName("Bind");
|
||||
attribute.TypeName = "System.Collections.Generic.Dictionary<string, object>";
|
||||
});
|
||||
|
||||
builder.BindAttribute(attribute =>
|
||||
{
|
||||
attribute.Documentation = ComponentResources.BindTagHelper_Fallback_Format_Documentation;
|
||||
|
||||
attribute.Name = "format-...";
|
||||
attribute.AsDictionary("format-", typeof(string).FullName);
|
||||
|
||||
// WTE has a bug 15.7p1 where a Tag Helper without a display-name that looks like
|
||||
// a C# property will crash trying to create the toolips.
|
||||
attribute.SetPropertyName("Format");
|
||||
attribute.TypeName = "System.Collections.Generic.Dictionary<string, string>";
|
||||
});
|
||||
|
||||
return builder.Build();
|
||||
}
|
||||
|
||||
private List<ElementBindData> GetElementBindData(Compilation compilation)
|
||||
{
|
||||
var bindElement = compilation.GetTypeByMetadataName(ComponentsApi.BindElementAttribute.FullTypeName);
|
||||
var bindInputElement = compilation.GetTypeByMetadataName(ComponentsApi.BindInputElementAttribute.FullTypeName);
|
||||
|
||||
if (bindElement == null || bindInputElement == null)
|
||||
{
|
||||
// This won't likely happen, but just in case.
|
||||
return new List<ElementBindData>();
|
||||
}
|
||||
|
||||
var types = new List<INamedTypeSymbol>();
|
||||
var visitor = new BindElementDataVisitor(types);
|
||||
|
||||
// Visit the primary output of this compilation, as well as all references.
|
||||
visitor.Visit(compilation.Assembly);
|
||||
foreach (var reference in compilation.References)
|
||||
{
|
||||
// We ignore .netmodules here - there really isn't a case where they are used by user code
|
||||
// even though the Roslyn APIs all support them.
|
||||
if (compilation.GetAssemblyOrModuleSymbol(reference) is IAssemblySymbol assembly)
|
||||
{
|
||||
visitor.Visit(assembly);
|
||||
}
|
||||
}
|
||||
|
||||
var results = new List<ElementBindData>();
|
||||
|
||||
for (var i = 0; i < types.Count; i++)
|
||||
{
|
||||
var type = types[i];
|
||||
var attributes = type.GetAttributes();
|
||||
|
||||
// Not handling duplicates here for now since we're the primary ones extending this.
|
||||
// If we see users adding to the set of 'bind' constructs we will want to add deduplication
|
||||
// and potentially diagnostics.
|
||||
for (var j = 0; j < attributes.Length; j++)
|
||||
{
|
||||
var attribute = attributes[j];
|
||||
|
||||
if (attribute.AttributeClass == bindElement)
|
||||
{
|
||||
results.Add(new ElementBindData(
|
||||
type.ContainingAssembly.Name,
|
||||
type.ToDisplayString(),
|
||||
(string)attribute.ConstructorArguments[0].Value,
|
||||
null,
|
||||
(string)attribute.ConstructorArguments[1].Value,
|
||||
(string)attribute.ConstructorArguments[2].Value,
|
||||
(string)attribute.ConstructorArguments[3].Value));
|
||||
}
|
||||
else if (attribute.AttributeClass == bindInputElement)
|
||||
{
|
||||
results.Add(new ElementBindData(
|
||||
type.ContainingAssembly.Name,
|
||||
type.ToDisplayString(),
|
||||
"input",
|
||||
(string)attribute.ConstructorArguments[0].Value,
|
||||
(string)attribute.ConstructorArguments[1].Value,
|
||||
(string)attribute.ConstructorArguments[2].Value,
|
||||
(string)attribute.ConstructorArguments[3].Value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
private List<TagHelperDescriptor> CreateElementBindTagHelpers(List<ElementBindData> data)
|
||||
{
|
||||
var results = new List<TagHelperDescriptor>();
|
||||
|
||||
for (var i = 0; i < data.Count; i++)
|
||||
{
|
||||
var entry = data[i];
|
||||
|
||||
var name = entry.Suffix == null ? "Bind" : "Bind_" + entry.Suffix;
|
||||
var attributeName = entry.Suffix == null ? "bind" : "bind-" + entry.Suffix;
|
||||
|
||||
var formatName = entry.Suffix == null ? "Format_" + entry.ValueAttribute : "Format_" + entry.Suffix;
|
||||
var formatAttributeName = entry.Suffix == null ? "format-" + entry.ValueAttribute : "format-" + entry.Suffix;
|
||||
|
||||
var builder = TagHelperDescriptorBuilder.Create(BlazorMetadata.Bind.TagHelperKind, name, entry.Assembly);
|
||||
builder.Documentation = string.Format(
|
||||
ComponentResources.BindTagHelper_Element_Documentation,
|
||||
entry.ValueAttribute,
|
||||
entry.ChangeAttribute);
|
||||
|
||||
builder.Metadata.Add(BlazorMetadata.SpecialKindKey, BlazorMetadata.Bind.TagHelperKind);
|
||||
builder.Metadata[TagHelperMetadata.Runtime.Name] = BlazorMetadata.Bind.RuntimeName;
|
||||
builder.Metadata[BlazorMetadata.Bind.ValueAttribute] = entry.ValueAttribute;
|
||||
builder.Metadata[BlazorMetadata.Bind.ChangeAttribute] = entry.ChangeAttribute;
|
||||
|
||||
if (entry.TypeAttribute != null)
|
||||
{
|
||||
// For entries that map to the <input /> element, we need to be able to know
|
||||
// the difference between <input /> and <input type="text" .../> for which we
|
||||
// want to use the same attributes.
|
||||
//
|
||||
// We provide a tag helper for <input /> that should match all input elements,
|
||||
// but we only want it to be used when a more specific one is used.
|
||||
//
|
||||
// Therefore we use this metadata to know which one is more specific when two
|
||||
// tag helpers match.
|
||||
builder.Metadata[BlazorMetadata.Bind.TypeAttribute] = entry.TypeAttribute;
|
||||
}
|
||||
|
||||
// WTE has a bug in 15.7p1 where a Tag Helper without a display-name that looks like
|
||||
// a C# property will crash trying to create the toolips.
|
||||
builder.SetTypeName(entry.TypeName);
|
||||
|
||||
builder.TagMatchingRule(rule =>
|
||||
{
|
||||
rule.TagName = entry.Element;
|
||||
if (entry.TypeAttribute != null)
|
||||
{
|
||||
rule.Attribute(a =>
|
||||
{
|
||||
a.Name = "type";
|
||||
a.NameComparisonMode = RequiredAttributeDescriptor.NameComparisonMode.FullMatch;
|
||||
a.Value = entry.TypeAttribute;
|
||||
a.ValueComparisonMode = RequiredAttributeDescriptor.ValueComparisonMode.FullMatch;
|
||||
});
|
||||
}
|
||||
|
||||
rule.Attribute(a =>
|
||||
{
|
||||
a.Name = attributeName;
|
||||
a.NameComparisonMode = RequiredAttributeDescriptor.NameComparisonMode.FullMatch;
|
||||
});
|
||||
});
|
||||
|
||||
builder.BindAttribute(a =>
|
||||
{
|
||||
a.Documentation = string.Format(
|
||||
ComponentResources.BindTagHelper_Element_Documentation,
|
||||
entry.ValueAttribute,
|
||||
entry.ChangeAttribute);
|
||||
|
||||
a.Name = attributeName;
|
||||
a.TypeName = typeof(object).FullName;
|
||||
|
||||
// WTE has a bug 15.7p1 where a Tag Helper without a display-name that looks like
|
||||
// a C# property will crash trying to create the toolips.
|
||||
a.SetPropertyName(name);
|
||||
});
|
||||
|
||||
builder.BindAttribute(attribute =>
|
||||
{
|
||||
attribute.Documentation = string.Format(ComponentResources.BindTagHelper_Element_Format_Documentation, attributeName);
|
||||
|
||||
attribute.Name = formatAttributeName;
|
||||
attribute.TypeName = "System.String";
|
||||
|
||||
// WTE has a bug 15.7p1 where a Tag Helper without a display-name that looks like
|
||||
// a C# property will crash trying to create the toolips.
|
||||
attribute.SetPropertyName(formatName);
|
||||
});
|
||||
|
||||
results.Add(builder.Build());
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private List<TagHelperDescriptor> CreateComponentBindTagHelpers(ICollection<TagHelperDescriptor> tagHelpers)
|
||||
{
|
||||
var results = new List<TagHelperDescriptor>();
|
||||
|
||||
foreach (var tagHelper in tagHelpers)
|
||||
{
|
||||
if (!tagHelper.IsComponentTagHelper())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// We want to create a 'bind' tag helper everywhere we see a pair of properties like `Foo`, `FooChanged`
|
||||
// where `FooChanged` is a delegate and `Foo` is not.
|
||||
//
|
||||
// The easiest way to figure this out without a lot of backtracking is to look for `FooChanged` and then
|
||||
// try to find a matching "Foo".
|
||||
for (var i = 0; i < tagHelper.BoundAttributes.Count; i++)
|
||||
{
|
||||
var changeAttribute = tagHelper.BoundAttributes[i];
|
||||
if (!changeAttribute.Name.EndsWith("Changed") || !changeAttribute.IsDelegateProperty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
BoundAttributeDescriptor valueAttribute = null;
|
||||
var valueAttributeName = changeAttribute.Name.Substring(0, changeAttribute.Name.Length - "Changed".Length);
|
||||
for (var j = 0; j < tagHelper.BoundAttributes.Count; j++)
|
||||
{
|
||||
if (tagHelper.BoundAttributes[j].Name == valueAttributeName && !tagHelper.BoundAttributes[j].IsDelegateProperty())
|
||||
{
|
||||
valueAttribute = tagHelper.BoundAttributes[j];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (valueAttribute == null)
|
||||
{
|
||||
// No matching attribute found.
|
||||
continue;
|
||||
}
|
||||
|
||||
var builder = TagHelperDescriptorBuilder.Create(BlazorMetadata.Bind.TagHelperKind, tagHelper.Name, tagHelper.AssemblyName);
|
||||
builder.DisplayName = tagHelper.DisplayName;
|
||||
builder.Documentation = string.Format(
|
||||
ComponentResources.BindTagHelper_Component_Documentation,
|
||||
valueAttribute.Name,
|
||||
changeAttribute.Name);
|
||||
|
||||
builder.Metadata.Add(BlazorMetadata.SpecialKindKey, BlazorMetadata.Bind.TagHelperKind);
|
||||
builder.Metadata[TagHelperMetadata.Runtime.Name] = BlazorMetadata.Bind.RuntimeName;
|
||||
builder.Metadata[BlazorMetadata.Bind.ValueAttribute] = valueAttribute.Name;
|
||||
builder.Metadata[BlazorMetadata.Bind.ChangeAttribute] = changeAttribute.Name;
|
||||
|
||||
// WTE has a bug 15.7p1 where a Tag Helper without a display-name that looks like
|
||||
// a C# property will crash trying to create the toolips.
|
||||
builder.SetTypeName(tagHelper.GetTypeName());
|
||||
|
||||
// Match the component and attribute name
|
||||
builder.TagMatchingRule(rule =>
|
||||
{
|
||||
rule.TagName = tagHelper.TagMatchingRules.Single().TagName;
|
||||
rule.Attribute(attribute =>
|
||||
{
|
||||
attribute.Name = "bind-" + valueAttribute.Name;
|
||||
attribute.NameComparisonMode = RequiredAttributeDescriptor.NameComparisonMode.FullMatch;
|
||||
});
|
||||
});
|
||||
|
||||
builder.BindAttribute(attribute =>
|
||||
{
|
||||
attribute.Documentation = string.Format(
|
||||
ComponentResources.BindTagHelper_Component_Documentation,
|
||||
valueAttribute.Name,
|
||||
changeAttribute.Name);
|
||||
|
||||
attribute.Name = "bind-" + valueAttribute.Name;
|
||||
attribute.TypeName = valueAttribute.TypeName;
|
||||
attribute.IsEnum = valueAttribute.IsEnum;
|
||||
|
||||
// WTE has a bug 15.7p1 where a Tag Helper without a display-name that looks like
|
||||
// a C# property will crash trying to create the toolips.
|
||||
attribute.SetPropertyName(valueAttribute.GetPropertyName());
|
||||
});
|
||||
|
||||
results.Add(builder.Build());
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private struct ElementBindData
|
||||
{
|
||||
public ElementBindData(
|
||||
string assembly,
|
||||
string typeName,
|
||||
string element,
|
||||
string typeAttribute,
|
||||
string suffix,
|
||||
string valueAttribute,
|
||||
string changeAttribute)
|
||||
{
|
||||
Assembly = assembly;
|
||||
TypeName = typeName;
|
||||
Element = element;
|
||||
TypeAttribute = typeAttribute;
|
||||
Suffix = suffix;
|
||||
ValueAttribute = valueAttribute;
|
||||
ChangeAttribute = changeAttribute;
|
||||
}
|
||||
|
||||
public string Assembly { get; }
|
||||
public string TypeName { get; }
|
||||
public string Element { get; }
|
||||
public string TypeAttribute { get; }
|
||||
public string Suffix { get; }
|
||||
public string ValueAttribute { get; }
|
||||
public string ChangeAttribute { get; }
|
||||
}
|
||||
|
||||
private class BindElementDataVisitor : SymbolVisitor
|
||||
{
|
||||
private List<INamedTypeSymbol> _results;
|
||||
|
||||
public BindElementDataVisitor(List<INamedTypeSymbol> results)
|
||||
{
|
||||
_results = results;
|
||||
}
|
||||
|
||||
public override void VisitNamedType(INamedTypeSymbol symbol)
|
||||
{
|
||||
if (symbol.Name == "BindAttributes" && symbol.DeclaredAccessibility == Accessibility.Public)
|
||||
{
|
||||
_results.Add(symbol);
|
||||
}
|
||||
}
|
||||
|
||||
public override void VisitNamespace(INamespaceSymbol symbol)
|
||||
{
|
||||
foreach (var member in symbol.GetMembers())
|
||||
{
|
||||
Visit(member);
|
||||
}
|
||||
}
|
||||
|
||||
public override void VisitAssembly(IAssemblySymbol symbol)
|
||||
{
|
||||
// This as a simple yet high-value optimization that excludes the vast majority of
|
||||
// assemblies that (by definition) can't contain a component.
|
||||
if (symbol.Name != null && !symbol.Name.StartsWith("System.", StringComparison.Ordinal))
|
||||
{
|
||||
Visit(symbol.GlobalNamespace);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,29 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Language.Components;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
|
||||
namespace Microsoft.CodeAnalysis.Razor
|
||||
{
|
||||
public class ComponentTagHelperDescriptorProvider : RazorEngineFeatureBase, ITagHelperDescriptorProvider
|
||||
internal class ComponentTagHelperDescriptorProvider : RazorEngineFeatureBase, ITagHelperDescriptorProvider
|
||||
{
|
||||
private static readonly SymbolDisplayFormat FullNameTypeDisplayFormat =
|
||||
SymbolDisplayFormat.FullyQualifiedFormat
|
||||
.WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted)
|
||||
.WithMiscellaneousOptions(SymbolDisplayFormat.FullyQualifiedFormat.MiscellaneousOptions & (~SymbolDisplayMiscellaneousOptions.UseSpecialTypes));
|
||||
|
||||
private static MethodInfo WithMetadataImportOptionsMethodInfo =
|
||||
typeof(CSharpCompilationOptions)
|
||||
.GetMethod("WithMetadataImportOptions", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
public bool IncludeDocumentation { get; set; }
|
||||
|
||||
public int Order { get; set; }
|
||||
|
||||
public void Execute(TagHelperDescriptorProviderContext context)
|
||||
|
|
@ -25,72 +40,422 @@ namespace Microsoft.CodeAnalysis.Razor
|
|||
return;
|
||||
}
|
||||
|
||||
var componentSymbol = compilation.GetTypeByMetadataName(TagHelperTypes.IComponent);
|
||||
if (componentSymbol == null || componentSymbol.TypeKind == TypeKind.Error)
|
||||
{
|
||||
// Could not find attributes we care about in the compilation. Nothing to do.
|
||||
return;
|
||||
}
|
||||
// We need to see private members too
|
||||
compilation = WithMetadataImportOptionsAll(compilation);
|
||||
|
||||
var symbols = BlazorSymbols.Create(compilation);
|
||||
|
||||
var types = new List<INamedTypeSymbol>();
|
||||
var visitor = new ComponentTypeVisitor(componentSymbol, types);
|
||||
|
||||
// We always visit the global namespace.
|
||||
visitor.Visit(compilation.Assembly.GlobalNamespace);
|
||||
var visitor = new ComponentTypeVisitor(symbols, types);
|
||||
|
||||
// Visit the primary output of this compilation, as well as all references.
|
||||
visitor.Visit(compilation.Assembly);
|
||||
foreach (var reference in compilation.References)
|
||||
{
|
||||
// We ignore .netmodules here - there really isn't a case where they are used by user code
|
||||
// even though the Roslyn APIs all support them.
|
||||
if (compilation.GetAssemblyOrModuleSymbol(reference) is IAssemblySymbol assembly)
|
||||
{
|
||||
if (IsTagHelperAssembly(assembly))
|
||||
{
|
||||
visitor.Visit(assembly.GlobalNamespace);
|
||||
}
|
||||
visitor.Visit(assembly);
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < types.Count; i++)
|
||||
{
|
||||
var type = types[i];
|
||||
var descriptor = CreateDescriptor(type);
|
||||
var descriptor = CreateDescriptor(symbols, type);
|
||||
context.Results.Add(descriptor);
|
||||
|
||||
if (descriptor != null)
|
||||
foreach (var childContent in descriptor.GetChildContentProperties())
|
||||
{
|
||||
context.Results.Add(descriptor);
|
||||
// Synthesize a separate tag helper for each child content property that's declared.
|
||||
context.Results.Add(CreateChildContentDescriptor(symbols, descriptor, childContent));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static TagHelperDescriptor CreateDescriptor(INamedTypeSymbol type)
|
||||
private Compilation WithMetadataImportOptionsAll(Compilation compilation)
|
||||
{
|
||||
var typeName = type.ToDisplayString(DefaultTagHelperDescriptorFactory.FullNameTypeDisplayFormat);
|
||||
var newCompilationOptions = (CSharpCompilationOptions)WithMetadataImportOptionsMethodInfo
|
||||
.Invoke(compilation.Options, new object[] { /* All */ (byte)2 });
|
||||
return compilation.WithOptions(newCompilationOptions);
|
||||
}
|
||||
|
||||
private TagHelperDescriptor CreateDescriptor(BlazorSymbols symbols, INamedTypeSymbol type)
|
||||
{
|
||||
var typeName = type.ToDisplayString(FullNameTypeDisplayFormat);
|
||||
var assemblyName = type.ContainingAssembly.Identity.Name;
|
||||
|
||||
var descriptorBuilder = TagHelperDescriptorBuilder.Create(TagHelperConventions.ComponentKind, typeName, assemblyName);
|
||||
descriptorBuilder.SetTypeName(typeName);
|
||||
descriptorBuilder.Metadata[TagHelperMetadata.Runtime.Name] = TagHelperConventions.ComponentKind;
|
||||
var builder = TagHelperDescriptorBuilder.Create(BlazorMetadata.Component.TagHelperKind, typeName, assemblyName);
|
||||
builder.SetTypeName(typeName);
|
||||
|
||||
// This opts out this 'component' tag helper for any processing that's specific to the default
|
||||
// Razor ITagHelper runtime.
|
||||
builder.Metadata[TagHelperMetadata.Runtime.Name] = BlazorMetadata.Component.RuntimeName;
|
||||
|
||||
if (type.IsGenericType)
|
||||
{
|
||||
builder.Metadata[BlazorMetadata.Component.GenericTypedKey] = bool.TrueString;
|
||||
|
||||
for (var i = 0; i < type.TypeArguments.Length; i++)
|
||||
{
|
||||
var typeParameter = type.TypeArguments[i] as ITypeParameterSymbol;
|
||||
if (typeParameter != null)
|
||||
{
|
||||
CreateTypeParameterProperty(builder, typeParameter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var xml = type.GetDocumentationCommentXml();
|
||||
if (!string.IsNullOrEmpty(xml))
|
||||
{
|
||||
builder.Documentation = xml;
|
||||
}
|
||||
|
||||
// Components have very simple matching rules. The type name (short) matches the tag name.
|
||||
descriptorBuilder.TagMatchingRule(r => r.TagName = type.Name);
|
||||
builder.TagMatchingRule(r => r.TagName = type.Name);
|
||||
|
||||
var descriptor = descriptorBuilder.Build();
|
||||
foreach (var property in GetProperties(symbols, type))
|
||||
{
|
||||
if (property.kind == PropertyKind.Ignored)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
CreateProperty(builder, property.property, property.kind);
|
||||
}
|
||||
|
||||
if (builder.BoundAttributes.Any(a => a.IsParameterizedChildContentProperty()) &&
|
||||
!builder.BoundAttributes.Any(a => string.Equals(a.Name, BlazorMetadata.ChildContent.ParameterAttributeName, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
// If we have any parameterized child content parameters, synthesize a 'Context' parameter to be
|
||||
// able to set the variable name (for all child content). If the developer defined a 'Context' parameter
|
||||
// already, then theirs wins.
|
||||
CreateContextParameter(builder, childContentName: null);
|
||||
}
|
||||
|
||||
var descriptor = builder.Build();
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
private bool IsTagHelperAssembly(IAssemblySymbol assembly)
|
||||
private void CreateProperty(TagHelperDescriptorBuilder builder, IPropertySymbol property, PropertyKind kind)
|
||||
{
|
||||
return assembly.Name != null && !assembly.Name.StartsWith("System.", StringComparison.Ordinal);
|
||||
builder.BindAttribute(pb =>
|
||||
{
|
||||
pb.Name = property.Name;
|
||||
pb.TypeName = property.Type.ToDisplayString(FullNameTypeDisplayFormat);
|
||||
pb.SetPropertyName(property.Name);
|
||||
|
||||
if (kind == PropertyKind.Enum)
|
||||
{
|
||||
pb.IsEnum = true;
|
||||
}
|
||||
|
||||
if (kind == PropertyKind.ChildContent)
|
||||
{
|
||||
pb.Metadata.Add(BlazorMetadata.Component.ChildContentKey, bool.TrueString);
|
||||
}
|
||||
|
||||
if (kind == PropertyKind.Delegate)
|
||||
{
|
||||
pb.Metadata.Add(BlazorMetadata.Component.DelegateSignatureKey, bool.TrueString);
|
||||
}
|
||||
|
||||
if (HasTypeParameter(property.Type))
|
||||
{
|
||||
pb.Metadata.Add(BlazorMetadata.Component.GenericTypedKey, bool.TrueString);
|
||||
}
|
||||
|
||||
var xml = property.GetDocumentationCommentXml();
|
||||
if (!string.IsNullOrEmpty(xml))
|
||||
{
|
||||
pb.Documentation = xml;
|
||||
}
|
||||
});
|
||||
|
||||
bool HasTypeParameter(ITypeSymbol type)
|
||||
{
|
||||
if (type is ITypeParameterSymbol)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// We need to check for cases like:
|
||||
// [Parameter] List<T> MyProperty { get; set; }
|
||||
// AND
|
||||
// [Parameter] List<string> MyProperty { get; set; }
|
||||
//
|
||||
// We need to inspect the type arguments to tell the difference between a property that
|
||||
// uses the containing class' type parameter(s) and a vanilla usage of generic types like
|
||||
// List<> and Dictionary<,>
|
||||
//
|
||||
// Since we need to handle cases like RenderFragment<List<T>>, this check must be recursive.
|
||||
if (type is INamedTypeSymbol namedType && namedType.IsGenericType)
|
||||
{
|
||||
var typeArguments = namedType.TypeArguments;
|
||||
for (var i = 0; i < typeArguments.Length; i++)
|
||||
{
|
||||
if (HasTypeParameter(typeArguments[i]))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Another case to handle - if the type being inspected is a nested type
|
||||
// inside a generic containing class. The common usage for this would be a case
|
||||
// where a generic templated component defines a 'context' nested class.
|
||||
if (namedType.ContainingType != null && HasTypeParameter(namedType.ContainingType))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Visits top-level types and finds interface implementations.
|
||||
internal class ComponentTypeVisitor : SymbolVisitor
|
||||
private void CreateTypeParameterProperty(TagHelperDescriptorBuilder builder, ITypeSymbol typeParameter)
|
||||
{
|
||||
private readonly INamedTypeSymbol _componentSymbol;
|
||||
builder.BindAttribute(pb =>
|
||||
{
|
||||
pb.DisplayName = typeParameter.Name;
|
||||
pb.Name = typeParameter.Name;
|
||||
pb.TypeName = typeof(Type).FullName;
|
||||
pb.SetPropertyName(typeParameter.Name);
|
||||
|
||||
pb.Metadata[BlazorMetadata.Component.TypeParameterKey] = bool.TrueString;
|
||||
|
||||
pb.Documentation = string.Format(ComponentResources.ComponentTypeParameter_Documentation, typeParameter.Name, builder.Name);
|
||||
});
|
||||
}
|
||||
|
||||
private TagHelperDescriptor CreateChildContentDescriptor(BlazorSymbols symbols, TagHelperDescriptor component, BoundAttributeDescriptor attribute)
|
||||
{
|
||||
var typeName = component.GetTypeName() + "." + attribute.Name;
|
||||
var assemblyName = component.AssemblyName;
|
||||
|
||||
var builder = TagHelperDescriptorBuilder.Create(BlazorMetadata.ChildContent.TagHelperKind, typeName, assemblyName);
|
||||
builder.SetTypeName(typeName);
|
||||
|
||||
// This opts out this 'component' tag helper for any processing that's specific to the default
|
||||
// Razor ITagHelper runtime.
|
||||
builder.Metadata[TagHelperMetadata.Runtime.Name] = BlazorMetadata.ChildContent.RuntimeName;
|
||||
|
||||
// Opt out of processing as a component. We'll process this specially as part of the component's body.
|
||||
builder.Metadata[BlazorMetadata.SpecialKindKey] = BlazorMetadata.ChildContent.TagHelperKind;
|
||||
|
||||
var xml = attribute.Documentation;
|
||||
if (!string.IsNullOrEmpty(xml))
|
||||
{
|
||||
builder.Documentation = xml;
|
||||
}
|
||||
|
||||
// Child content matches the property name, but only as a direct child of the component.
|
||||
builder.TagMatchingRule(r =>
|
||||
{
|
||||
r.TagName = attribute.Name;
|
||||
r.ParentTag = component.TagMatchingRules.First().TagName;
|
||||
});
|
||||
|
||||
if (attribute.IsParameterizedChildContentProperty())
|
||||
{
|
||||
// For child content attributes with a parameter, synthesize an attribute that allows you to name
|
||||
// the parameter.
|
||||
CreateContextParameter(builder, attribute.Name);
|
||||
}
|
||||
|
||||
var descriptor = builder.Build();
|
||||
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
private void CreateContextParameter(TagHelperDescriptorBuilder builder, string childContentName)
|
||||
{
|
||||
builder.BindAttribute(b =>
|
||||
{
|
||||
b.Name = BlazorMetadata.ChildContent.ParameterAttributeName;
|
||||
b.TypeName = typeof(string).FullName;
|
||||
b.Metadata.Add(BlazorMetadata.Component.ChildContentParameterNameKey, bool.TrueString);
|
||||
|
||||
if (childContentName == null)
|
||||
{
|
||||
b.Documentation = ComponentResources.ChildContentParameterName_TopLevelDocumentation;
|
||||
}
|
||||
else
|
||||
{
|
||||
b.Documentation = string.Format(ComponentResources.ChildContentParameterName_Documentation, childContentName);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Does a walk up the inheritance chain to determine the set of parameters by using
|
||||
// a dictionary keyed on property name.
|
||||
//
|
||||
// We consider parameters to be defined by properties satisfying all of the following:
|
||||
// - are visible (not shadowed)
|
||||
// - have the [Parameter] attribute
|
||||
// - have a setter, even if private
|
||||
// - are not indexers
|
||||
private IEnumerable<(IPropertySymbol property, PropertyKind kind)> GetProperties(BlazorSymbols symbols, INamedTypeSymbol type)
|
||||
{
|
||||
var properties = new Dictionary<string, (IPropertySymbol, PropertyKind)>(StringComparer.Ordinal);
|
||||
do
|
||||
{
|
||||
if (type == symbols.ComponentBase)
|
||||
{
|
||||
// The ComponentBase base class doesn't have any [Parameter].
|
||||
// Bail out now to avoid walking through its many members, plus the members
|
||||
// of the System.Object base class.
|
||||
break;
|
||||
}
|
||||
|
||||
var members = type.GetMembers();
|
||||
for (var i = 0; i < members.Length; i++)
|
||||
{
|
||||
var property = members[i] as IPropertySymbol;
|
||||
if (property == null)
|
||||
{
|
||||
// Not a property
|
||||
continue;
|
||||
}
|
||||
|
||||
if (properties.ContainsKey(property.Name))
|
||||
{
|
||||
// Not visible
|
||||
continue;
|
||||
}
|
||||
|
||||
var kind = PropertyKind.Default;
|
||||
if (property.Parameters.Length != 0)
|
||||
{
|
||||
// Indexer
|
||||
kind = PropertyKind.Ignored;
|
||||
}
|
||||
|
||||
if (property.SetMethod == null)
|
||||
{
|
||||
// No setter
|
||||
kind = PropertyKind.Ignored;
|
||||
}
|
||||
|
||||
if (property.IsStatic)
|
||||
{
|
||||
kind = PropertyKind.Ignored;
|
||||
}
|
||||
|
||||
if (!property.GetAttributes().Any(a => a.AttributeClass == symbols.ParameterAttribute))
|
||||
{
|
||||
// Does not have [Parameter]
|
||||
kind = PropertyKind.Ignored;
|
||||
}
|
||||
|
||||
if (kind == PropertyKind.Default && property.Type.TypeKind == TypeKind.Enum)
|
||||
{
|
||||
kind = PropertyKind.Enum;
|
||||
}
|
||||
|
||||
if (kind == PropertyKind.Default && property.Type == symbols.RenderFragment)
|
||||
{
|
||||
kind = PropertyKind.ChildContent;
|
||||
}
|
||||
|
||||
if (kind == PropertyKind.Default &&
|
||||
property.Type is INamedTypeSymbol namedType &&
|
||||
namedType.IsGenericType &&
|
||||
namedType.ConstructedFrom == symbols.RenderFragmentOfT)
|
||||
{
|
||||
kind = PropertyKind.ChildContent;
|
||||
}
|
||||
|
||||
if (kind == PropertyKind.Default && property.Type.TypeKind == TypeKind.Delegate)
|
||||
{
|
||||
kind = PropertyKind.Delegate;
|
||||
}
|
||||
|
||||
properties.Add(property.Name, (property, kind));
|
||||
}
|
||||
|
||||
type = type.BaseType;
|
||||
}
|
||||
while (type != null);
|
||||
|
||||
return properties.Values;
|
||||
}
|
||||
|
||||
private enum PropertyKind
|
||||
{
|
||||
Ignored,
|
||||
Default,
|
||||
Enum,
|
||||
ChildContent,
|
||||
Delegate,
|
||||
}
|
||||
|
||||
private class BlazorSymbols
|
||||
{
|
||||
public static BlazorSymbols Create(Compilation compilation)
|
||||
{
|
||||
var symbols = new BlazorSymbols();
|
||||
symbols.ComponentBase = compilation.GetTypeByMetadataName(ComponentsApi.ComponentBase.MetadataName);
|
||||
if (symbols.ComponentBase == null)
|
||||
{
|
||||
// No definition for ComponentBase, nothing to do.
|
||||
return null;
|
||||
}
|
||||
|
||||
symbols.IComponent = compilation.GetTypeByMetadataName(ComponentsApi.IComponent.MetadataName);
|
||||
if (symbols.IComponent == null)
|
||||
{
|
||||
// No definition for IComponent, nothing to do.
|
||||
return null;
|
||||
}
|
||||
|
||||
symbols.ParameterAttribute = compilation.GetTypeByMetadataName(ComponentsApi.ParameterAttribute.MetadataName);
|
||||
if (symbols.ParameterAttribute == null)
|
||||
{
|
||||
// No definition for [Parameter], nothing to do.
|
||||
return null;
|
||||
}
|
||||
|
||||
symbols.RenderFragment = compilation.GetTypeByMetadataName(ComponentsApi.RenderFragment.MetadataName);
|
||||
if (symbols.RenderFragment == null)
|
||||
{
|
||||
// No definition for RenderFragment, nothing to do.
|
||||
}
|
||||
|
||||
symbols.RenderFragmentOfT = compilation.GetTypeByMetadataName(ComponentsApi.RenderFragmentOfT.MetadataName);
|
||||
if (symbols.RenderFragmentOfT == null)
|
||||
{
|
||||
// No definition for RenderFragment, nothing to do.
|
||||
}
|
||||
|
||||
return symbols;
|
||||
}
|
||||
|
||||
private BlazorSymbols()
|
||||
{
|
||||
}
|
||||
|
||||
public INamedTypeSymbol ComponentBase { get; private set; }
|
||||
|
||||
public INamedTypeSymbol IComponent { get; private set; }
|
||||
|
||||
public INamedTypeSymbol ParameterAttribute { get; private set; }
|
||||
|
||||
public INamedTypeSymbol RenderFragment { get; private set; }
|
||||
|
||||
public INamedTypeSymbol RenderFragmentOfT { get; private set; }
|
||||
}
|
||||
|
||||
private class ComponentTypeVisitor : SymbolVisitor
|
||||
{
|
||||
private readonly BlazorSymbols _symbols;
|
||||
private readonly List<INamedTypeSymbol> _results;
|
||||
|
||||
public ComponentTypeVisitor(INamedTypeSymbol componentSymbol, List<INamedTypeSymbol> results)
|
||||
public ComponentTypeVisitor(BlazorSymbols symbols, List<INamedTypeSymbol> results)
|
||||
{
|
||||
_componentSymbol = componentSymbol;
|
||||
_symbols = symbols;
|
||||
_results = results;
|
||||
}
|
||||
|
||||
|
|
@ -122,11 +487,16 @@ namespace Microsoft.CodeAnalysis.Razor
|
|||
|
||||
internal bool IsComponent(INamedTypeSymbol symbol)
|
||||
{
|
||||
if (_symbols == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return
|
||||
symbol.DeclaredAccessibility == Accessibility.Public &&
|
||||
!symbol.IsAbstract &&
|
||||
symbol.AllInterfaces.Contains(_componentSymbol);
|
||||
symbol.AllInterfaces.Contains(_symbols.IComponent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
namespace Microsoft.CodeAnalysis.Razor
|
||||
{
|
||||
internal class DefaultTypeNameFeature : TypeNameFeature
|
||||
{
|
||||
public override IReadOnlyList<string> ParseTypeParameters(string typeName)
|
||||
{
|
||||
if (typeName == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(typeName));
|
||||
}
|
||||
|
||||
var parsed = SyntaxFactory.ParseTypeName(typeName);
|
||||
if (parsed is IdentifierNameSyntax identifier)
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
else
|
||||
{
|
||||
return parsed.DescendantNodesAndSelf()
|
||||
.OfType<TypeArgumentListSyntax>()
|
||||
.SelectMany(arg => arg.Arguments)
|
||||
.Select(a => a.ToString()).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public override TypeNameRewriter CreateGenericTypeRewriter(Dictionary<string, string> bindings)
|
||||
{
|
||||
if (bindings == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(bindings));
|
||||
}
|
||||
|
||||
return new GenericTypeNameRewriter(bindings);
|
||||
}
|
||||
|
||||
public override TypeNameRewriter CreateGlobalQualifiedTypeNameRewriter(ICollection<string> ignore)
|
||||
{
|
||||
if (ignore == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(ignore));
|
||||
}
|
||||
|
||||
return new GlobalQualifiedTypeNameRewriter(ignore);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,218 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Language.Components;
|
||||
|
||||
namespace Microsoft.CodeAnalysis.Razor
|
||||
{
|
||||
internal class EventHandlerTagHelperDescriptorProvider : ITagHelperDescriptorProvider
|
||||
{
|
||||
public int Order { get; set; }
|
||||
|
||||
public RazorEngine Engine { get; set; }
|
||||
|
||||
public void Execute(TagHelperDescriptorProviderContext context)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
var compilation = context.GetCompilation();
|
||||
if (compilation == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var bindMethods = compilation.GetTypeByMetadataName(ComponentsApi.BindMethods.FullTypeName);
|
||||
if (bindMethods == null)
|
||||
{
|
||||
// If we can't find BindMethods, then just bail. We won't be able to compile the
|
||||
// generated code anyway.
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
var eventHandlerData = GetEventHandlerData(compilation);
|
||||
|
||||
foreach (var tagHelper in CreateEventHandlerTagHelpers(eventHandlerData))
|
||||
{
|
||||
context.Results.Add(tagHelper);
|
||||
}
|
||||
}
|
||||
|
||||
private List<EventHandlerData> GetEventHandlerData(Compilation compilation)
|
||||
{
|
||||
var eventHandlerAttribute = compilation.GetTypeByMetadataName(ComponentsApi.EventHandlerAttribute.FullTypeName);
|
||||
if (eventHandlerAttribute == null)
|
||||
{
|
||||
// This won't likely happen, but just in case.
|
||||
return new List<EventHandlerData>();
|
||||
}
|
||||
|
||||
var types = new List<INamedTypeSymbol>();
|
||||
var visitor = new EventHandlerDataVisitor(types);
|
||||
|
||||
// Visit the primary output of this compilation, as well as all references.
|
||||
visitor.Visit(compilation.Assembly);
|
||||
foreach (var reference in compilation.References)
|
||||
{
|
||||
// We ignore .netmodules here - there really isn't a case where they are used by user code
|
||||
// even though the Roslyn APIs all support them.
|
||||
if (compilation.GetAssemblyOrModuleSymbol(reference) is IAssemblySymbol assembly)
|
||||
{
|
||||
visitor.Visit(assembly);
|
||||
}
|
||||
}
|
||||
|
||||
var results = new List<EventHandlerData>();
|
||||
|
||||
for (var i = 0; i < types.Count; i++)
|
||||
{
|
||||
var type = types[i];
|
||||
var attributes = type.GetAttributes();
|
||||
|
||||
// Not handling duplicates here for now since we're the primary ones extending this.
|
||||
// If we see users adding to the set of event handler constructs we will want to add deduplication
|
||||
// and potentially diagnostics.
|
||||
for (var j = 0; j < attributes.Length; j++)
|
||||
{
|
||||
var attribute = attributes[j];
|
||||
|
||||
if (attribute.AttributeClass == eventHandlerAttribute)
|
||||
{
|
||||
results.Add(new EventHandlerData(
|
||||
type.ContainingAssembly.Name,
|
||||
type.ToDisplayString(),
|
||||
(string)attribute.ConstructorArguments[0].Value,
|
||||
(INamedTypeSymbol)attribute.ConstructorArguments[1].Value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private List<TagHelperDescriptor> CreateEventHandlerTagHelpers(List<EventHandlerData> data)
|
||||
{
|
||||
var results = new List<TagHelperDescriptor>();
|
||||
|
||||
for (var i = 0; i < data.Count; i++)
|
||||
{
|
||||
var entry = data[i];
|
||||
|
||||
var builder = TagHelperDescriptorBuilder.Create(BlazorMetadata.EventHandler.TagHelperKind, entry.Attribute, entry.Assembly);
|
||||
builder.Documentation = string.Format(
|
||||
ComponentResources.EventHandlerTagHelper_Documentation,
|
||||
entry.Attribute,
|
||||
entry.EventArgsType.ToDisplayString());
|
||||
|
||||
builder.Metadata.Add(BlazorMetadata.SpecialKindKey, BlazorMetadata.EventHandler.TagHelperKind);
|
||||
builder.Metadata.Add(BlazorMetadata.EventHandler.EventArgsType, entry.EventArgsType.ToDisplayString());
|
||||
builder.Metadata[TagHelperMetadata.Runtime.Name] = BlazorMetadata.EventHandler.RuntimeName;
|
||||
|
||||
// WTE has a bug in 15.7p1 where a Tag Helper without a display-name that looks like
|
||||
// a C# property will crash trying to create the tooltips.
|
||||
builder.SetTypeName(entry.TypeName);
|
||||
|
||||
builder.TagMatchingRule(rule =>
|
||||
{
|
||||
rule.TagName = "*";
|
||||
|
||||
rule.Attribute(a =>
|
||||
{
|
||||
a.Name = entry.Attribute;
|
||||
a.NameComparisonMode = RequiredAttributeDescriptor.NameComparisonMode.FullMatch;
|
||||
});
|
||||
});
|
||||
|
||||
builder.BindAttribute(a =>
|
||||
{
|
||||
a.Documentation = string.Format(
|
||||
ComponentResources.EventHandlerTagHelper_Documentation,
|
||||
entry.Attribute,
|
||||
entry.EventArgsType.ToDisplayString());
|
||||
|
||||
a.Name = entry.Attribute;
|
||||
|
||||
// Use a string here so that we get HTML context by default.
|
||||
a.TypeName = typeof(string).FullName;
|
||||
|
||||
// But make this weakly typed (don't type check) - delegates have their own type-checking
|
||||
// logic that we don't want to interfere with.
|
||||
a.Metadata.Add(BlazorMetadata.Component.WeaklyTypedKey, bool.TrueString);
|
||||
|
||||
// WTE has a bug 15.7p1 where a Tag Helper without a display-name that looks like
|
||||
// a C# property will crash trying to create the tooltips.
|
||||
a.SetPropertyName(entry.Attribute);
|
||||
});
|
||||
|
||||
results.Add(builder.Build());
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private struct EventHandlerData
|
||||
{
|
||||
public EventHandlerData(
|
||||
string assembly,
|
||||
string typeName,
|
||||
string element,
|
||||
INamedTypeSymbol eventArgsType)
|
||||
{
|
||||
Assembly = assembly;
|
||||
TypeName = typeName;
|
||||
Attribute = element;
|
||||
EventArgsType = eventArgsType;
|
||||
}
|
||||
|
||||
public string Assembly { get; }
|
||||
|
||||
public string TypeName { get; }
|
||||
|
||||
public string Attribute { get; }
|
||||
|
||||
public INamedTypeSymbol EventArgsType { get; }
|
||||
}
|
||||
|
||||
private class EventHandlerDataVisitor : SymbolVisitor
|
||||
{
|
||||
private List<INamedTypeSymbol> _results;
|
||||
|
||||
public EventHandlerDataVisitor(List<INamedTypeSymbol> results)
|
||||
{
|
||||
_results = results;
|
||||
}
|
||||
|
||||
public override void VisitNamedType(INamedTypeSymbol symbol)
|
||||
{
|
||||
if (symbol.Name == "EventHandlers" && symbol.DeclaredAccessibility == Accessibility.Public)
|
||||
{
|
||||
_results.Add(symbol);
|
||||
}
|
||||
}
|
||||
|
||||
public override void VisitNamespace(INamespaceSymbol symbol)
|
||||
{
|
||||
foreach (var member in symbol.GetMembers())
|
||||
{
|
||||
Visit(member);
|
||||
}
|
||||
}
|
||||
|
||||
public override void VisitAssembly(IAssemblySymbol symbol)
|
||||
{
|
||||
// This as a simple yet high-value optimization that excludes the vast majority of
|
||||
// assemblies that (by definition) can't contain a component.
|
||||
if (symbol.Name != null && !symbol.Name.StartsWith("System.", StringComparison.Ordinal))
|
||||
{
|
||||
Visit(symbol.GlobalNamespace);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
namespace Microsoft.CodeAnalysis.Razor
|
||||
{
|
||||
internal class GenericTypeNameRewriter : TypeNameRewriter
|
||||
{
|
||||
private readonly Dictionary<string, string> _bindings;
|
||||
|
||||
public GenericTypeNameRewriter(Dictionary<string, string> bindings)
|
||||
{
|
||||
_bindings = bindings;
|
||||
}
|
||||
|
||||
public override string Rewrite(string typeName)
|
||||
{
|
||||
var parsed = SyntaxFactory.ParseTypeName(typeName);
|
||||
var rewritten = (TypeSyntax)new Visitor(_bindings).Visit(parsed);
|
||||
return rewritten.ToFullString();
|
||||
}
|
||||
|
||||
private class Visitor : CSharpSyntaxRewriter
|
||||
{
|
||||
private readonly Dictionary<string, string> _bindings;
|
||||
|
||||
public Visitor(Dictionary<string, string> bindings)
|
||||
{
|
||||
_bindings = bindings;
|
||||
}
|
||||
|
||||
public override SyntaxNode Visit(SyntaxNode node)
|
||||
{
|
||||
// We can handle a single IdentifierNameSyntax at the top level (like 'TItem)
|
||||
// OR a GenericNameSyntax recursively (like `List<T>`)
|
||||
if (node is IdentifierNameSyntax identifier && !(identifier.Parent is QualifiedNameSyntax))
|
||||
{
|
||||
if (_bindings.TryGetValue(identifier.Identifier.Text, out var binding))
|
||||
{
|
||||
// If we don't have a valid replacement, use object. This will make the code at least reasonable
|
||||
// compared to leaving the type parameter in place.
|
||||
//
|
||||
// We add our own diagnostics for missing/invalid type parameters anyway.
|
||||
var replacement = binding == null ? typeof(object).FullName : binding;
|
||||
return identifier.Update(SyntaxFactory.Identifier(replacement));
|
||||
}
|
||||
}
|
||||
|
||||
return base.Visit(node);
|
||||
}
|
||||
|
||||
public override SyntaxNode VisitGenericName(GenericNameSyntax node)
|
||||
{
|
||||
var args = node.TypeArgumentList.Arguments;
|
||||
for (var i = 0; i < args.Count; i++)
|
||||
{
|
||||
var typeArgument = args[i];
|
||||
args = args.Replace(typeArgument, (TypeSyntax)Visit(typeArgument));
|
||||
}
|
||||
|
||||
return node.WithTypeArgumentList(node.TypeArgumentList.WithArguments(args));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
namespace Microsoft.CodeAnalysis.Razor
|
||||
{
|
||||
// Rewrites type names to use the 'global::' prefix for identifiers.
|
||||
//
|
||||
// This is useful when we're generating code in a different namespace than
|
||||
// what the user code lives in. When we synthesize a namespace it's easy to have
|
||||
// clashes.
|
||||
internal class GlobalQualifiedTypeNameRewriter : TypeNameRewriter
|
||||
{
|
||||
// List of names to ignore.
|
||||
//
|
||||
// NOTE: this is the list of type parameters defined on the component.
|
||||
private readonly HashSet<string> _ignore;
|
||||
|
||||
public GlobalQualifiedTypeNameRewriter(ICollection<string> ignore)
|
||||
{
|
||||
_ignore = new HashSet<string>(ignore);
|
||||
}
|
||||
|
||||
public override string Rewrite(string typeName)
|
||||
{
|
||||
var parsed = SyntaxFactory.ParseTypeName(typeName);
|
||||
var rewritten = (TypeSyntax)new Visitor(_ignore).Visit(parsed);
|
||||
return rewritten.ToFullString();
|
||||
}
|
||||
|
||||
private class Visitor : CSharpSyntaxRewriter
|
||||
{
|
||||
private readonly HashSet<string> _ignore;
|
||||
|
||||
public Visitor(HashSet<string> ignore)
|
||||
{
|
||||
_ignore = ignore;
|
||||
}
|
||||
|
||||
public override SyntaxNode Visit(SyntaxNode node)
|
||||
{
|
||||
return base.Visit(node);
|
||||
}
|
||||
|
||||
public override SyntaxNode VisitQualifiedName(QualifiedNameSyntax node)
|
||||
{
|
||||
if (node.Parent is QualifiedNameSyntax)
|
||||
{
|
||||
return base.VisitQualifiedName(node);
|
||||
}
|
||||
|
||||
// Need to rewrite postorder so we can rewrite the names of generic type arguments.
|
||||
node = (QualifiedNameSyntax)base.VisitQualifiedName(node);
|
||||
|
||||
// Rewriting these is complicated, best to just tostring and parse again.
|
||||
return SyntaxFactory.ParseTypeName("global::" + node.ToString());
|
||||
}
|
||||
|
||||
public override SyntaxNode VisitIdentifierName(IdentifierNameSyntax node)
|
||||
{
|
||||
if (_ignore.Contains(node.ToString()))
|
||||
{
|
||||
return node;
|
||||
}
|
||||
|
||||
if (node.Parent != null)
|
||||
{
|
||||
return node;
|
||||
}
|
||||
|
||||
return SyntaxFactory.AliasQualifiedName(SyntaxFactory.IdentifierName(SyntaxFactory.Token(CSharp.SyntaxKind.GlobalKeyword)), node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using System.Runtime.CompilerServices;
|
||||
[assembly: InternalsVisibleTo("rzls, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||
[assembly: InternalsVisibleTo("rzc, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Razor.LanguageServer.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||
[assembly: InternalsVisibleTo("Microsoft.VisualStudio.LiveShare.Razor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||
[assembly: InternalsVisibleTo("Microsoft.VisualStudio.LiveShare.Razor.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Language.Components;
|
||||
|
||||
namespace Microsoft.CodeAnalysis.Razor
|
||||
{
|
||||
internal class RefTagHelperDescriptorProvider : ITagHelperDescriptorProvider
|
||||
{
|
||||
// Run after the component tag helper provider, because later we may want component-type-specific variants of this
|
||||
public int Order { get; set; } = 1000;
|
||||
|
||||
public RazorEngine Engine { get; set; }
|
||||
|
||||
public void Execute(TagHelperDescriptorProviderContext context)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
context.Results.Add(CreateRefTagHelper());
|
||||
}
|
||||
|
||||
private TagHelperDescriptor CreateRefTagHelper()
|
||||
{
|
||||
var builder = TagHelperDescriptorBuilder.Create(BlazorMetadata.Ref.TagHelperKind, "Ref", ComponentsApi.AssemblyName);
|
||||
builder.Documentation = ComponentResources.RefTagHelper_Documentation;
|
||||
|
||||
builder.Metadata.Add(BlazorMetadata.SpecialKindKey, BlazorMetadata.Ref.TagHelperKind);
|
||||
builder.Metadata[TagHelperMetadata.Runtime.Name] = BlazorMetadata.Ref.RuntimeName;
|
||||
|
||||
// WTE has a bug in 15.7p1 where a Tag Helper without a display-name that looks like
|
||||
// a C# property will crash trying to create the tooltips.
|
||||
builder.SetTypeName("Microsoft.AspNetCore.Components.Ref");
|
||||
|
||||
builder.TagMatchingRule(rule =>
|
||||
{
|
||||
rule.TagName = "*";
|
||||
rule.Attribute(attribute =>
|
||||
{
|
||||
attribute.Name = "ref";
|
||||
});
|
||||
});
|
||||
|
||||
builder.BindAttribute(attribute =>
|
||||
{
|
||||
attribute.Documentation = ComponentResources.RefTagHelper_Documentation;
|
||||
attribute.Name = "ref";
|
||||
|
||||
// WTE has a bug 15.7p1 where a Tag Helper without a display-name that looks like
|
||||
// a C# property will crash trying to create the tooltips.
|
||||
attribute.SetPropertyName("Ref");
|
||||
attribute.TypeName = typeof(object).FullName;
|
||||
});
|
||||
|
||||
return builder.Build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.Extensions.DependencyModel;
|
||||
|
||||
namespace Microsoft.CodeAnalysis.Razor
|
||||
{
|
||||
public abstract class BaseTagHelperDescriptorProviderTest
|
||||
{
|
||||
static BaseTagHelperDescriptorProviderTest()
|
||||
{
|
||||
BaseCompilation = TestCompilation.Create(typeof(ComponentTagHelperDescriptorProviderTest).Assembly);
|
||||
CSharpParseOptions = new CSharpParseOptions(LanguageVersion.CSharp7_3);
|
||||
}
|
||||
|
||||
protected static Compilation BaseCompilation { get; }
|
||||
|
||||
protected static CSharpParseOptions CSharpParseOptions { get; }
|
||||
|
||||
protected static CSharpSyntaxTree Parse(string text)
|
||||
{
|
||||
return (CSharpSyntaxTree)CSharpSyntaxTree.ParseText(text, CSharpParseOptions);
|
||||
}
|
||||
|
||||
// For simplicity in testing, exclude the built-in components. We'll add more and we
|
||||
// don't want to update the tests when that happens.
|
||||
protected static TagHelperDescriptor[] ExcludeBuiltInComponents(TagHelperDescriptorProviderContext context)
|
||||
{
|
||||
return context.Results
|
||||
.Where(c => c.AssemblyName == "TestAssembly")
|
||||
.OrderBy(c => c.Name)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,683 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Language.Components;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.CodeAnalysis.Razor
|
||||
{
|
||||
public class BindTagHelperDescriptorProviderTest : BaseTagHelperDescriptorProviderTest
|
||||
{
|
||||
[Fact]
|
||||
public void Execute_FindsBindTagHelperOnComponentType_CreatesDescriptor()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = BaseCompilation.AddSyntaxTrees(Parse(@"
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace Test
|
||||
{
|
||||
public class MyComponent : IComponent
|
||||
{
|
||||
public void Init(RenderHandle renderHandle) { }
|
||||
|
||||
public void SetParameters(ParameterCollection parameters) { }
|
||||
|
||||
[Parameter]
|
||||
string MyProperty { get; set; }
|
||||
|
||||
[Parameter]
|
||||
Action<string> MyPropertyChanged { get; set; }
|
||||
}
|
||||
}
|
||||
"));
|
||||
|
||||
Assert.Empty(compilation.GetDiagnostics());
|
||||
|
||||
var context = TagHelperDescriptorProviderContext.Create();
|
||||
context.SetCompilation(compilation);
|
||||
|
||||
// We run after component discovery and depend on the results.
|
||||
var componentProvider = new ComponentTagHelperDescriptorProvider();
|
||||
componentProvider.Execute(context);
|
||||
|
||||
var provider = new BindTagHelperDescriptorProvider();
|
||||
|
||||
// Act
|
||||
provider.Execute(context);
|
||||
|
||||
// Assert
|
||||
var matches = GetBindTagHelpers(context);
|
||||
var bind = Assert.Single(matches);
|
||||
|
||||
// These are features Bind Tags Helpers don't use. Verifying them once here and
|
||||
// then ignoring them.
|
||||
Assert.Empty(bind.AllowedChildTags);
|
||||
Assert.Null(bind.TagOutputHint);
|
||||
|
||||
// These are features that are invariants of all Bind Tag Helpers. Verifying them once
|
||||
// here and then ignoring them.
|
||||
Assert.Empty(bind.Diagnostics);
|
||||
Assert.False(bind.HasErrors);
|
||||
Assert.Equal(BlazorMetadata.Bind.TagHelperKind, bind.Kind);
|
||||
Assert.Equal(BlazorMetadata.Bind.RuntimeName, bind.Metadata[TagHelperMetadata.Runtime.Name]);
|
||||
Assert.False(bind.IsDefaultKind());
|
||||
Assert.False(bind.KindUsesDefaultTagHelperRuntime());
|
||||
|
||||
Assert.Equal("MyProperty", bind.Metadata[BlazorMetadata.Bind.ValueAttribute]);
|
||||
Assert.Equal("MyPropertyChanged", bind.Metadata[BlazorMetadata.Bind.ChangeAttribute]);
|
||||
|
||||
Assert.Equal(
|
||||
"Binds the provided expression to the 'MyProperty' property and a change event " +
|
||||
"delegate to the 'MyPropertyChanged' property of the component.",
|
||||
bind.Documentation);
|
||||
|
||||
// These are all trivially derived from the assembly/namespace/type name
|
||||
Assert.Equal("TestAssembly", bind.AssemblyName);
|
||||
Assert.Equal("Test.MyComponent", bind.Name);
|
||||
Assert.Equal("Test.MyComponent", bind.DisplayName);
|
||||
Assert.Equal("Test.MyComponent", bind.GetTypeName());
|
||||
|
||||
var rule = Assert.Single(bind.TagMatchingRules);
|
||||
Assert.Empty(rule.Diagnostics);
|
||||
Assert.False(rule.HasErrors);
|
||||
Assert.Null(rule.ParentTag);
|
||||
Assert.Equal("MyComponent", rule.TagName);
|
||||
Assert.Equal(TagStructure.Unspecified, rule.TagStructure);
|
||||
|
||||
var requiredAttribute = Assert.Single(rule.Attributes);
|
||||
Assert.Empty(requiredAttribute.Diagnostics);
|
||||
Assert.Equal("bind-MyProperty", requiredAttribute.DisplayName);
|
||||
Assert.Equal("bind-MyProperty", requiredAttribute.Name);
|
||||
Assert.Equal(RequiredAttributeDescriptor.NameComparisonMode.FullMatch, requiredAttribute.NameComparison);
|
||||
Assert.Null(requiredAttribute.Value);
|
||||
Assert.Equal(RequiredAttributeDescriptor.ValueComparisonMode.None, requiredAttribute.ValueComparison);
|
||||
|
||||
var attribute = Assert.Single(bind.BoundAttributes);
|
||||
|
||||
// Invariants
|
||||
Assert.Empty(attribute.Diagnostics);
|
||||
Assert.False(attribute.HasErrors);
|
||||
Assert.Equal(BlazorMetadata.Bind.TagHelperKind, attribute.Kind);
|
||||
Assert.False(attribute.IsDefaultKind());
|
||||
Assert.False(attribute.HasIndexer);
|
||||
Assert.Null(attribute.IndexerNamePrefix);
|
||||
Assert.Null(attribute.IndexerTypeName);
|
||||
Assert.False(attribute.IsIndexerBooleanProperty);
|
||||
Assert.False(attribute.IsIndexerStringProperty);
|
||||
|
||||
Assert.Equal(
|
||||
"Binds the provided expression to the 'MyProperty' property and a change event " +
|
||||
"delegate to the 'MyPropertyChanged' property of the component.",
|
||||
attribute.Documentation);
|
||||
|
||||
Assert.Equal("bind-MyProperty", attribute.Name);
|
||||
Assert.Equal("MyProperty", attribute.GetPropertyName());
|
||||
Assert.Equal("string Test.MyComponent.MyProperty", attribute.DisplayName);
|
||||
|
||||
// Defined from the property type
|
||||
Assert.Equal("System.String", attribute.TypeName);
|
||||
Assert.True(attribute.IsStringProperty);
|
||||
Assert.False(attribute.IsBooleanProperty);
|
||||
Assert.False(attribute.IsEnum);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Execute_NoMatchedPropertiesOnComponent_IgnoresComponent()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = BaseCompilation.AddSyntaxTrees(Parse(@"
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace Test
|
||||
{
|
||||
public class MyComponent : IComponent
|
||||
{
|
||||
public void Init(RenderHandle renderHandle) { }
|
||||
|
||||
public void SetParameters(ParameterCollection parameters) { }
|
||||
|
||||
public string MyProperty { get; set; }
|
||||
|
||||
public Action<string> MyPropertyChangedNotMatch { get; set; }
|
||||
}
|
||||
}
|
||||
"));
|
||||
|
||||
Assert.Empty(compilation.GetDiagnostics());
|
||||
|
||||
var context = TagHelperDescriptorProviderContext.Create();
|
||||
context.SetCompilation(compilation);
|
||||
|
||||
// We run after component discovery and depend on the results.
|
||||
var componentProvider = new ComponentTagHelperDescriptorProvider();
|
||||
componentProvider.Execute(context);
|
||||
|
||||
var provider = new BindTagHelperDescriptorProvider();
|
||||
|
||||
// Act
|
||||
provider.Execute(context);
|
||||
|
||||
// Assert
|
||||
var matches = GetBindTagHelpers(context);
|
||||
Assert.Empty(matches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Execute_BindOnElement_CreatesDescriptor()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = BaseCompilation.AddSyntaxTrees(Parse(@"
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace Test
|
||||
{
|
||||
[BindElement(""div"", null, ""myprop"", ""myevent"")]
|
||||
public class BindAttributes
|
||||
{
|
||||
}
|
||||
}
|
||||
"));
|
||||
|
||||
Assert.Empty(compilation.GetDiagnostics());
|
||||
|
||||
var context = TagHelperDescriptorProviderContext.Create();
|
||||
context.SetCompilation(compilation);
|
||||
|
||||
var provider = new BindTagHelperDescriptorProvider();
|
||||
|
||||
// Act
|
||||
provider.Execute(context);
|
||||
|
||||
// Assert
|
||||
var matches = GetBindTagHelpers(context);
|
||||
var bind = Assert.Single(matches);
|
||||
|
||||
// These are features Bind Tags Helpers don't use. Verifying them once here and
|
||||
// then ignoring them.
|
||||
Assert.Empty(bind.AllowedChildTags);
|
||||
Assert.Null(bind.TagOutputHint);
|
||||
|
||||
// These are features that are invariants of all Bind Tag Helpers. Verifying them once
|
||||
// here and then ignoring them.
|
||||
Assert.Empty(bind.Diagnostics);
|
||||
Assert.False(bind.HasErrors);
|
||||
Assert.Equal(BlazorMetadata.Bind.TagHelperKind, bind.Kind);
|
||||
Assert.Equal(BlazorMetadata.Bind.RuntimeName, bind.Metadata[TagHelperMetadata.Runtime.Name]);
|
||||
Assert.False(bind.IsDefaultKind());
|
||||
Assert.False(bind.KindUsesDefaultTagHelperRuntime());
|
||||
|
||||
Assert.Equal("myprop", bind.Metadata[BlazorMetadata.Bind.ValueAttribute]);
|
||||
Assert.Equal("myevent", bind.Metadata[BlazorMetadata.Bind.ChangeAttribute]);
|
||||
Assert.False(bind.IsInputElementBindTagHelper());
|
||||
Assert.False(bind.IsInputElementFallbackBindTagHelper());
|
||||
|
||||
Assert.Equal(
|
||||
"Binds the provided expression to the 'myprop' attribute and a change event " +
|
||||
"delegate to the 'myevent' attribute.",
|
||||
bind.Documentation);
|
||||
|
||||
// These are all trivially derived from the assembly/namespace/type name
|
||||
Assert.Equal("TestAssembly", bind.AssemblyName);
|
||||
Assert.Equal("Bind", bind.Name);
|
||||
Assert.Equal("Test.BindAttributes", bind.DisplayName);
|
||||
Assert.Equal("Test.BindAttributes", bind.GetTypeName());
|
||||
|
||||
// The tag matching rule for a bind-Component is always the component name + the attribute name
|
||||
var rule = Assert.Single(bind.TagMatchingRules);
|
||||
Assert.Empty(rule.Diagnostics);
|
||||
Assert.False(rule.HasErrors);
|
||||
Assert.Null(rule.ParentTag);
|
||||
Assert.Equal("div", rule.TagName);
|
||||
Assert.Equal(TagStructure.Unspecified, rule.TagStructure);
|
||||
|
||||
var requiredAttribute = Assert.Single(rule.Attributes);
|
||||
Assert.Empty(requiredAttribute.Diagnostics);
|
||||
Assert.Equal("bind", requiredAttribute.DisplayName);
|
||||
Assert.Equal("bind", requiredAttribute.Name);
|
||||
Assert.Equal(RequiredAttributeDescriptor.NameComparisonMode.FullMatch, requiredAttribute.NameComparison);
|
||||
Assert.Null(requiredAttribute.Value);
|
||||
Assert.Equal(RequiredAttributeDescriptor.ValueComparisonMode.None, requiredAttribute.ValueComparison);
|
||||
|
||||
var attribute = Assert.Single(bind.BoundAttributes, a => a.Name.StartsWith("bind"));
|
||||
|
||||
// Invariants
|
||||
Assert.Empty(attribute.Diagnostics);
|
||||
Assert.False(attribute.HasErrors);
|
||||
Assert.Equal(BlazorMetadata.Bind.TagHelperKind, attribute.Kind);
|
||||
Assert.False(attribute.IsDefaultKind());
|
||||
Assert.False(attribute.HasIndexer);
|
||||
Assert.Null(attribute.IndexerNamePrefix);
|
||||
Assert.Null(attribute.IndexerTypeName);
|
||||
Assert.False(attribute.IsIndexerBooleanProperty);
|
||||
Assert.False(attribute.IsIndexerStringProperty);
|
||||
|
||||
Assert.Equal(
|
||||
"Binds the provided expression to the 'myprop' attribute and a change event " +
|
||||
"delegate to the 'myevent' attribute.",
|
||||
attribute.Documentation);
|
||||
|
||||
Assert.Equal("bind", attribute.Name);
|
||||
Assert.Equal("Bind", attribute.GetPropertyName());
|
||||
Assert.Equal("object Test.BindAttributes.Bind", attribute.DisplayName);
|
||||
|
||||
// Defined from the property type
|
||||
Assert.Equal("System.Object", attribute.TypeName);
|
||||
Assert.False(attribute.IsStringProperty);
|
||||
Assert.False(attribute.IsBooleanProperty);
|
||||
Assert.False(attribute.IsEnum);
|
||||
|
||||
attribute = Assert.Single(bind.BoundAttributes, a => a.Name.StartsWith("format"));
|
||||
|
||||
// Invariants
|
||||
Assert.Empty(attribute.Diagnostics);
|
||||
Assert.False(attribute.HasErrors);
|
||||
Assert.Equal(BlazorMetadata.Bind.TagHelperKind, attribute.Kind);
|
||||
Assert.False(attribute.IsDefaultKind());
|
||||
Assert.False(attribute.HasIndexer);
|
||||
Assert.Null(attribute.IndexerNamePrefix);
|
||||
Assert.Null(attribute.IndexerTypeName);
|
||||
Assert.False(attribute.IsIndexerBooleanProperty);
|
||||
Assert.False(attribute.IsIndexerStringProperty);
|
||||
|
||||
Assert.Equal(
|
||||
"Specifies a format to convert the value specified by the 'bind' attribute. " +
|
||||
"The format string can currently only be used with expressions of type <code>DateTime</code>.",
|
||||
attribute.Documentation);
|
||||
|
||||
Assert.Equal("format-myprop", attribute.Name);
|
||||
Assert.Equal("Format_myprop", attribute.GetPropertyName());
|
||||
Assert.Equal("string Test.BindAttributes.Format_myprop", attribute.DisplayName);
|
||||
|
||||
// Defined from the property type
|
||||
Assert.Equal("System.String", attribute.TypeName);
|
||||
Assert.True(attribute.IsStringProperty);
|
||||
Assert.False(attribute.IsBooleanProperty);
|
||||
Assert.False(attribute.IsEnum);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Execute_BindOnElementWithSuffix_CreatesDescriptor()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = BaseCompilation.AddSyntaxTrees(Parse(@"
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace Test
|
||||
{
|
||||
[BindElement(""div"", ""myprop"", ""myprop"", ""myevent"")]
|
||||
public class BindAttributes
|
||||
{
|
||||
}
|
||||
}
|
||||
"));
|
||||
|
||||
Assert.Empty(compilation.GetDiagnostics());
|
||||
|
||||
var context = TagHelperDescriptorProviderContext.Create();
|
||||
context.SetCompilation(compilation);
|
||||
|
||||
var provider = new BindTagHelperDescriptorProvider();
|
||||
|
||||
// Act
|
||||
provider.Execute(context);
|
||||
|
||||
// Assert
|
||||
var matches = GetBindTagHelpers(context);
|
||||
var bind = Assert.Single(matches);
|
||||
|
||||
Assert.Equal("myprop", bind.Metadata[BlazorMetadata.Bind.ValueAttribute]);
|
||||
Assert.Equal("myevent", bind.Metadata[BlazorMetadata.Bind.ChangeAttribute]);
|
||||
Assert.False(bind.IsInputElementBindTagHelper());
|
||||
Assert.False(bind.IsInputElementFallbackBindTagHelper());
|
||||
|
||||
var rule = Assert.Single(bind.TagMatchingRules);
|
||||
Assert.Equal("div", rule.TagName);
|
||||
Assert.Equal(TagStructure.Unspecified, rule.TagStructure);
|
||||
|
||||
var requiredAttribute = Assert.Single(rule.Attributes);
|
||||
Assert.Equal("bind-myprop", requiredAttribute.DisplayName);
|
||||
Assert.Equal("bind-myprop", requiredAttribute.Name);
|
||||
|
||||
var attribute = Assert.Single(bind.BoundAttributes, a => a.Name.StartsWith("bind"));
|
||||
Assert.Equal("bind-myprop", attribute.Name);
|
||||
Assert.Equal("Bind_myprop", attribute.GetPropertyName());
|
||||
Assert.Equal("object Test.BindAttributes.Bind_myprop", attribute.DisplayName);
|
||||
|
||||
attribute = Assert.Single(bind.BoundAttributes, a => a.Name.StartsWith("format"));
|
||||
Assert.Equal("format-myprop", attribute.Name);
|
||||
Assert.Equal("Format_myprop", attribute.GetPropertyName());
|
||||
Assert.Equal("string Test.BindAttributes.Format_myprop", attribute.DisplayName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Execute_BindOnInputElementWithoutTypeAttribute_CreatesDescriptor()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = BaseCompilation.AddSyntaxTrees(Parse(@"
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace Test
|
||||
{
|
||||
[BindInputElement(null, null, ""myprop"", ""myevent"")]
|
||||
public class BindAttributes
|
||||
{
|
||||
}
|
||||
}
|
||||
"));
|
||||
|
||||
Assert.Empty(compilation.GetDiagnostics());
|
||||
|
||||
var context = TagHelperDescriptorProviderContext.Create();
|
||||
context.SetCompilation(compilation);
|
||||
|
||||
var provider = new BindTagHelperDescriptorProvider();
|
||||
|
||||
// Act
|
||||
provider.Execute(context);
|
||||
|
||||
// Assert
|
||||
var matches = GetBindTagHelpers(context);
|
||||
var bind = Assert.Single(matches);
|
||||
|
||||
Assert.Equal("myprop", bind.Metadata[BlazorMetadata.Bind.ValueAttribute]);
|
||||
Assert.Equal("myevent", bind.Metadata[BlazorMetadata.Bind.ChangeAttribute]);
|
||||
Assert.False(bind.Metadata.ContainsKey(BlazorMetadata.Bind.TypeAttribute));
|
||||
Assert.True(bind.IsInputElementBindTagHelper());
|
||||
Assert.True(bind.IsInputElementFallbackBindTagHelper());
|
||||
|
||||
var rule = Assert.Single(bind.TagMatchingRules);
|
||||
Assert.Equal("input", rule.TagName);
|
||||
Assert.Equal(TagStructure.Unspecified, rule.TagStructure);
|
||||
|
||||
var requiredAttribute = Assert.Single(rule.Attributes);
|
||||
Assert.Equal("bind", requiredAttribute.DisplayName);
|
||||
Assert.Equal("bind", requiredAttribute.Name);
|
||||
|
||||
var attribute = Assert.Single(bind.BoundAttributes, a => a.Name.StartsWith("bind"));
|
||||
Assert.Equal("bind", attribute.Name);
|
||||
Assert.Equal("Bind", attribute.GetPropertyName());
|
||||
Assert.Equal("object Test.BindAttributes.Bind", attribute.DisplayName);
|
||||
|
||||
attribute = Assert.Single(bind.BoundAttributes, a => a.Name.StartsWith("format"));
|
||||
Assert.Equal("format-myprop", attribute.Name);
|
||||
Assert.Equal("Format_myprop", attribute.GetPropertyName());
|
||||
Assert.Equal("string Test.BindAttributes.Format_myprop", attribute.DisplayName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Execute_BindOnInputElementWithTypeAttribute_CreatesDescriptor()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = BaseCompilation.AddSyntaxTrees(Parse(@"
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace Test
|
||||
{
|
||||
[BindInputElement(""checkbox"", null, ""myprop"", ""myevent"")]
|
||||
public class BindAttributes
|
||||
{
|
||||
}
|
||||
}
|
||||
"));
|
||||
|
||||
Assert.Empty(compilation.GetDiagnostics());
|
||||
|
||||
var context = TagHelperDescriptorProviderContext.Create();
|
||||
context.SetCompilation(compilation);
|
||||
|
||||
var provider = new BindTagHelperDescriptorProvider();
|
||||
|
||||
// Act
|
||||
provider.Execute(context);
|
||||
|
||||
// Assert
|
||||
var matches = GetBindTagHelpers(context);
|
||||
var bind = Assert.Single(matches);
|
||||
|
||||
Assert.Equal("myprop", bind.Metadata[BlazorMetadata.Bind.ValueAttribute]);
|
||||
Assert.Equal("myevent", bind.Metadata[BlazorMetadata.Bind.ChangeAttribute]);
|
||||
Assert.Equal("checkbox", bind.Metadata[BlazorMetadata.Bind.TypeAttribute]);
|
||||
Assert.True(bind.IsInputElementBindTagHelper());
|
||||
Assert.False(bind.IsInputElementFallbackBindTagHelper());
|
||||
|
||||
var rule = Assert.Single(bind.TagMatchingRules);
|
||||
Assert.Equal("input", rule.TagName);
|
||||
Assert.Equal(TagStructure.Unspecified, rule.TagStructure);
|
||||
|
||||
Assert.Collection(
|
||||
rule.Attributes,
|
||||
a =>
|
||||
{
|
||||
Assert.Equal("type", a.DisplayName);
|
||||
Assert.Equal("type", a.Name);
|
||||
Assert.Equal(RequiredAttributeDescriptor.NameComparisonMode.FullMatch, a.NameComparison);
|
||||
Assert.Equal("checkbox", a.Value);
|
||||
Assert.Equal(RequiredAttributeDescriptor.ValueComparisonMode.FullMatch, a.ValueComparison);
|
||||
},
|
||||
a =>
|
||||
{
|
||||
Assert.Equal("bind", a.DisplayName);
|
||||
Assert.Equal("bind", a.Name);
|
||||
});
|
||||
|
||||
var attribute = Assert.Single(bind.BoundAttributes, a => a.Name.StartsWith("bind"));
|
||||
Assert.Equal("bind", attribute.Name);
|
||||
Assert.Equal("Bind", attribute.GetPropertyName());
|
||||
Assert.Equal("object Test.BindAttributes.Bind", attribute.DisplayName);
|
||||
|
||||
attribute = Assert.Single(bind.BoundAttributes, a => a.Name.StartsWith("format"));
|
||||
Assert.Equal("format-myprop", attribute.Name);
|
||||
Assert.Equal("Format_myprop", attribute.GetPropertyName());
|
||||
Assert.Equal("string Test.BindAttributes.Format_myprop", attribute.DisplayName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Execute_BindOnInputElementWithTypeAttributeAndSuffix_CreatesDescriptor()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = BaseCompilation.AddSyntaxTrees(Parse(@"
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace Test
|
||||
{
|
||||
[BindInputElement(""checkbox"", ""somevalue"", ""myprop"", ""myevent"")]
|
||||
public class BindAttributes
|
||||
{
|
||||
}
|
||||
}
|
||||
"));
|
||||
|
||||
Assert.Empty(compilation.GetDiagnostics());
|
||||
|
||||
var context = TagHelperDescriptorProviderContext.Create();
|
||||
context.SetCompilation(compilation);
|
||||
|
||||
var provider = new BindTagHelperDescriptorProvider();
|
||||
|
||||
// Act
|
||||
provider.Execute(context);
|
||||
|
||||
// Assert
|
||||
var matches = GetBindTagHelpers(context);
|
||||
var bind = Assert.Single(matches);
|
||||
|
||||
Assert.Equal("myprop", bind.Metadata[BlazorMetadata.Bind.ValueAttribute]);
|
||||
Assert.Equal("myevent", bind.Metadata[BlazorMetadata.Bind.ChangeAttribute]);
|
||||
Assert.Equal("checkbox", bind.Metadata[BlazorMetadata.Bind.TypeAttribute]);
|
||||
Assert.True(bind.IsInputElementBindTagHelper());
|
||||
Assert.False(bind.IsInputElementFallbackBindTagHelper());
|
||||
|
||||
var rule = Assert.Single(bind.TagMatchingRules);
|
||||
Assert.Equal("input", rule.TagName);
|
||||
Assert.Equal(TagStructure.Unspecified, rule.TagStructure);
|
||||
|
||||
Assert.Collection(
|
||||
rule.Attributes,
|
||||
a =>
|
||||
{
|
||||
Assert.Equal("type", a.DisplayName);
|
||||
Assert.Equal("type", a.Name);
|
||||
Assert.Equal(RequiredAttributeDescriptor.NameComparisonMode.FullMatch, a.NameComparison);
|
||||
Assert.Equal("checkbox", a.Value);
|
||||
Assert.Equal(RequiredAttributeDescriptor.ValueComparisonMode.FullMatch, a.ValueComparison);
|
||||
},
|
||||
a =>
|
||||
{
|
||||
Assert.Equal("bind-somevalue", a.DisplayName);
|
||||
Assert.Equal("bind-somevalue", a.Name);
|
||||
});
|
||||
|
||||
var attribute = Assert.Single(bind.BoundAttributes, a => a.Name.StartsWith("bind"));
|
||||
Assert.Equal("bind-somevalue", attribute.Name);
|
||||
Assert.Equal("Bind_somevalue", attribute.GetPropertyName());
|
||||
Assert.Equal("object Test.BindAttributes.Bind_somevalue", attribute.DisplayName);
|
||||
|
||||
attribute = Assert.Single(bind.BoundAttributes, a => a.Name.StartsWith("format"));
|
||||
Assert.Equal("format-somevalue", attribute.Name);
|
||||
Assert.Equal("Format_somevalue", attribute.GetPropertyName());
|
||||
Assert.Equal("string Test.BindAttributes.Format_somevalue", attribute.DisplayName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Execute_BindFallback_CreatesDescriptor()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = BaseCompilation;
|
||||
Assert.Empty(compilation.GetDiagnostics());
|
||||
|
||||
var context = TagHelperDescriptorProviderContext.Create();
|
||||
context.SetCompilation(compilation);
|
||||
|
||||
var provider = new BindTagHelperDescriptorProvider();
|
||||
|
||||
// Act
|
||||
provider.Execute(context);
|
||||
|
||||
// Assert
|
||||
var bind = Assert.Single(context.Results, r => r.IsFallbackBindTagHelper());
|
||||
|
||||
// These are features Bind Tags Helpers don't use. Verifying them once here and
|
||||
// then ignoring them.
|
||||
Assert.Empty(bind.AllowedChildTags);
|
||||
Assert.Null(bind.TagOutputHint);
|
||||
|
||||
// These are features that are invariants of all Bind Tag Helpers. Verifying them once
|
||||
// here and then ignoring them.
|
||||
Assert.Empty(bind.Diagnostics);
|
||||
Assert.False(bind.HasErrors);
|
||||
Assert.Equal(BlazorMetadata.Bind.TagHelperKind, bind.Kind);
|
||||
Assert.Equal(BlazorMetadata.Bind.RuntimeName, bind.Metadata[TagHelperMetadata.Runtime.Name]);
|
||||
Assert.False(bind.IsDefaultKind());
|
||||
Assert.False(bind.KindUsesDefaultTagHelperRuntime());
|
||||
|
||||
Assert.False(bind.Metadata.ContainsKey(BlazorMetadata.Bind.ValueAttribute));
|
||||
Assert.False(bind.Metadata.ContainsKey(BlazorMetadata.Bind.ChangeAttribute));
|
||||
Assert.True(bind.IsFallbackBindTagHelper());
|
||||
|
||||
Assert.Equal(
|
||||
"Binds the provided expression to an attribute and a change event, based on the naming of " +
|
||||
"the bind attribute. For example: <code>bind-value-onchange=\"...\"</code> will assign the " +
|
||||
"current value of the expression to the 'value' attribute, and assign a delegate that attempts " +
|
||||
"to set the value to the 'onchange' attribute.",
|
||||
bind.Documentation);
|
||||
|
||||
// These are all trivially derived from the assembly/namespace/type name
|
||||
Assert.Equal("Microsoft.AspNetCore.Components", bind.AssemblyName);
|
||||
Assert.Equal("Bind", bind.Name);
|
||||
Assert.Equal("Microsoft.AspNetCore.Components.Bind", bind.DisplayName);
|
||||
Assert.Equal("Microsoft.AspNetCore.Components.Bind", bind.GetTypeName());
|
||||
|
||||
// The tag matching rule for a bind-Component is always the component name + the attribute name
|
||||
var rule = Assert.Single(bind.TagMatchingRules);
|
||||
Assert.Empty(rule.Diagnostics);
|
||||
Assert.False(rule.HasErrors);
|
||||
Assert.Null(rule.ParentTag);
|
||||
Assert.Equal("*", rule.TagName);
|
||||
Assert.Equal(TagStructure.Unspecified, rule.TagStructure);
|
||||
|
||||
var requiredAttribute = Assert.Single(rule.Attributes);
|
||||
Assert.Empty(requiredAttribute.Diagnostics);
|
||||
Assert.Equal("bind-...", requiredAttribute.DisplayName);
|
||||
Assert.Equal("bind-", requiredAttribute.Name);
|
||||
Assert.Equal(RequiredAttributeDescriptor.NameComparisonMode.PrefixMatch, requiredAttribute.NameComparison);
|
||||
Assert.Null(requiredAttribute.Value);
|
||||
Assert.Equal(RequiredAttributeDescriptor.ValueComparisonMode.None, requiredAttribute.ValueComparison);
|
||||
|
||||
var attribute = Assert.Single(bind.BoundAttributes, a => a.Name.StartsWith("bind"));
|
||||
|
||||
// Invariants
|
||||
Assert.Empty(attribute.Diagnostics);
|
||||
Assert.False(attribute.HasErrors);
|
||||
Assert.Equal(BlazorMetadata.Bind.TagHelperKind, attribute.Kind);
|
||||
Assert.False(attribute.IsDefaultKind());
|
||||
Assert.False(attribute.IsIndexerBooleanProperty);
|
||||
Assert.False(attribute.IsIndexerStringProperty);
|
||||
|
||||
Assert.True(attribute.HasIndexer);
|
||||
Assert.Equal("bind-", attribute.IndexerNamePrefix);
|
||||
Assert.Equal("System.Object", attribute.IndexerTypeName);
|
||||
|
||||
Assert.Equal(
|
||||
"Binds the provided expression to an attribute and a change event, based on the naming of " +
|
||||
"the bind attribute. For example: <code>bind-value-onchange=\"...\"</code> will assign the " +
|
||||
"current value of the expression to the 'value' attribute, and assign a delegate that attempts " +
|
||||
"to set the value to the 'onchange' attribute.",
|
||||
attribute.Documentation);
|
||||
|
||||
Assert.Equal("bind-...", attribute.Name);
|
||||
Assert.Equal("Bind", attribute.GetPropertyName());
|
||||
Assert.Equal(
|
||||
"System.Collections.Generic.Dictionary<string, object> Microsoft.AspNetCore.Components.Bind.Bind",
|
||||
attribute.DisplayName);
|
||||
|
||||
// Defined from the property type
|
||||
Assert.Equal("System.Collections.Generic.Dictionary<string, object>", attribute.TypeName);
|
||||
Assert.False(attribute.IsStringProperty);
|
||||
Assert.False(attribute.IsBooleanProperty);
|
||||
Assert.False(attribute.IsEnum);
|
||||
|
||||
attribute = Assert.Single(bind.BoundAttributes, a => a.Name.StartsWith("format"));
|
||||
|
||||
// Invariants
|
||||
Assert.Empty(attribute.Diagnostics);
|
||||
Assert.False(attribute.HasErrors);
|
||||
Assert.Equal(BlazorMetadata.Bind.TagHelperKind, attribute.Kind);
|
||||
Assert.False(attribute.IsDefaultKind());
|
||||
Assert.True(attribute.HasIndexer);
|
||||
Assert.Equal("format-", attribute.IndexerNamePrefix);
|
||||
Assert.Equal("System.String", attribute.IndexerTypeName);
|
||||
Assert.False(attribute.IsIndexerBooleanProperty);
|
||||
Assert.True(attribute.IsIndexerStringProperty);
|
||||
|
||||
Assert.Equal(
|
||||
"Specifies a format to convert the value specified by the corresponding bind attribute. " +
|
||||
"For example: <code>format-value=\"...\"</code> will apply a format string to the value " +
|
||||
"specified in <code>bind-value-...</code>. The format string can currently only be used with " +
|
||||
"expressions of type <code>DateTime</code>.",
|
||||
attribute.Documentation);
|
||||
|
||||
Assert.Equal("format-...", attribute.Name);
|
||||
Assert.Equal("Format", attribute.GetPropertyName());
|
||||
Assert.Equal(
|
||||
"System.Collections.Generic.Dictionary<string, string> Microsoft.AspNetCore.Components.Bind.Format",
|
||||
attribute.DisplayName);
|
||||
|
||||
// Defined from the property type
|
||||
Assert.Equal("System.Collections.Generic.Dictionary<string, string>", attribute.TypeName);
|
||||
Assert.False(attribute.IsStringProperty);
|
||||
Assert.False(attribute.IsBooleanProperty);
|
||||
Assert.False(attribute.IsEnum);
|
||||
}
|
||||
|
||||
|
||||
private static TagHelperDescriptor[] GetBindTagHelpers(TagHelperDescriptorProviderContext context)
|
||||
{
|
||||
return ExcludeBuiltInComponents(context).Where(t => t.IsBindTagHelper()).ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,125 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.CodeAnalysis.Razor;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language.Components
|
||||
{
|
||||
public class EventHandlerTagHelperDescriptorProviderTest : BaseTagHelperDescriptorProviderTest
|
||||
{
|
||||
[Fact]
|
||||
public void Execute_EventHandler_CreatesDescriptor()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = BaseCompilation.AddSyntaxTrees(Parse(@"
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace Test
|
||||
{
|
||||
[EventHandler(""onclick"", typeof(Action<UIMouseEventArgs>))]
|
||||
public class EventHandlers
|
||||
{
|
||||
}
|
||||
}
|
||||
"));
|
||||
|
||||
Assert.Empty(compilation.GetDiagnostics());
|
||||
|
||||
var context = TagHelperDescriptorProviderContext.Create();
|
||||
context.SetCompilation(compilation);
|
||||
|
||||
var provider = new EventHandlerTagHelperDescriptorProvider();
|
||||
|
||||
// Act
|
||||
provider.Execute(context);
|
||||
|
||||
// Assert
|
||||
var matches = GetEventHandlerTagHelpers(context);
|
||||
var item = Assert.Single(matches);
|
||||
|
||||
// These are features Event Handler Tag Helpers don't use. Verifying them once here and
|
||||
// then ignoring them.
|
||||
Assert.Empty(item.AllowedChildTags);
|
||||
Assert.Null(item.TagOutputHint);
|
||||
|
||||
// These are features that are invariants of all Event Handler Helpers. Verifying them once
|
||||
// here and then ignoring them.
|
||||
Assert.Empty(item.Diagnostics);
|
||||
Assert.False(item.HasErrors);
|
||||
Assert.Equal(BlazorMetadata.EventHandler.TagHelperKind, item.Kind);
|
||||
Assert.Equal(BlazorMetadata.EventHandler.RuntimeName, item.Metadata[TagHelperMetadata.Runtime.Name]);
|
||||
Assert.False(item.IsDefaultKind());
|
||||
Assert.False(item.KindUsesDefaultTagHelperRuntime());
|
||||
|
||||
Assert.Equal(
|
||||
"Sets the 'onclick' attribute to the provided string or delegate value. " +
|
||||
"A delegate value should be of type 'System.Action<Microsoft.AspNetCore.Components.UIMouseEventArgs>'.",
|
||||
item.Documentation);
|
||||
|
||||
// These are all trivially derived from the assembly/namespace/type name
|
||||
Assert.Equal("TestAssembly", item.AssemblyName);
|
||||
Assert.Equal("onclick", item.Name);
|
||||
Assert.Equal("Test.EventHandlers", item.DisplayName);
|
||||
Assert.Equal("Test.EventHandlers", item.GetTypeName());
|
||||
|
||||
// The tag matching rule for an event handler is just the attribute name
|
||||
var rule = Assert.Single(item.TagMatchingRules);
|
||||
Assert.Empty(rule.Diagnostics);
|
||||
Assert.False(rule.HasErrors);
|
||||
Assert.Null(rule.ParentTag);
|
||||
Assert.Equal("*", rule.TagName);
|
||||
Assert.Equal(TagStructure.Unspecified, rule.TagStructure);
|
||||
|
||||
var requiredAttribute = Assert.Single(rule.Attributes);
|
||||
Assert.Empty(requiredAttribute.Diagnostics);
|
||||
Assert.Equal("onclick", requiredAttribute.DisplayName);
|
||||
Assert.Equal("onclick", requiredAttribute.Name);
|
||||
Assert.Equal(RequiredAttributeDescriptor.NameComparisonMode.FullMatch, requiredAttribute.NameComparison);
|
||||
Assert.Null(requiredAttribute.Value);
|
||||
Assert.Equal(RequiredAttributeDescriptor.ValueComparisonMode.None, requiredAttribute.ValueComparison);
|
||||
|
||||
var attribute = Assert.Single(item.BoundAttributes);
|
||||
|
||||
// Invariants
|
||||
Assert.Empty(attribute.Diagnostics);
|
||||
Assert.False(attribute.HasErrors);
|
||||
Assert.Equal(BlazorMetadata.EventHandler.TagHelperKind, attribute.Kind);
|
||||
Assert.False(attribute.IsDefaultKind());
|
||||
Assert.False(attribute.HasIndexer);
|
||||
Assert.Null(attribute.IndexerNamePrefix);
|
||||
Assert.Null(attribute.IndexerTypeName);
|
||||
Assert.False(attribute.IsIndexerBooleanProperty);
|
||||
Assert.False(attribute.IsIndexerStringProperty);
|
||||
|
||||
Assert.Collection(
|
||||
attribute.Metadata.OrderBy(kvp => kvp.Key),
|
||||
kvp => Assert.Equal(kvp, new KeyValuePair<string, string>(BlazorMetadata.Component.WeaklyTypedKey, bool.TrueString)),
|
||||
kvp => Assert.Equal(kvp, new KeyValuePair<string, string>("Common.PropertyName", "onclick")));
|
||||
|
||||
Assert.Equal(
|
||||
"Sets the 'onclick' attribute to the provided string or delegate value. " +
|
||||
"A delegate value should be of type 'System.Action<Microsoft.AspNetCore.Components.UIMouseEventArgs>'.",
|
||||
attribute.Documentation);
|
||||
|
||||
Assert.Equal("onclick", attribute.Name);
|
||||
Assert.Equal("onclick", attribute.GetPropertyName());
|
||||
Assert.Equal("string Test.EventHandlers.onclick", attribute.DisplayName);
|
||||
|
||||
// Defined from the property type
|
||||
Assert.Equal("System.String", attribute.TypeName);
|
||||
Assert.True(attribute.IsStringProperty);
|
||||
Assert.False(attribute.IsBooleanProperty);
|
||||
Assert.False(attribute.IsEnum);
|
||||
}
|
||||
|
||||
private static TagHelperDescriptor[] GetEventHandlerTagHelpers(TagHelperDescriptorProviderContext context)
|
||||
{
|
||||
return ExcludeBuiltInComponents(context).Where(t => t.IsEventHandlerTagHelper()).ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.CodeAnalysis.Razor
|
||||
{
|
||||
public class GenericTypeNameRewriterTest
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("TItem2", "Type2")]
|
||||
|
||||
// Unspecified argument -> System.Object
|
||||
[InlineData("TItem3", "System.Object")]
|
||||
|
||||
// Not a type parameter
|
||||
[InlineData("TItem4", "TItem4")]
|
||||
|
||||
// In a qualified name, not a type parameter
|
||||
[InlineData("TItem1.TItem2", "TItem1.TItem2")]
|
||||
|
||||
// Type parameters can't have type parameters
|
||||
[InlineData("TItem1.TItem2<TItem1, TItem2, TItem3>", "TItem1.TItem2<Type1, Type2, System.Object>")]
|
||||
[InlineData("TItem2<TItem1<TItem3>, System.TItem2, RenderFragment<List<TItem1>>", "TItem2<TItem1<System.Object>, System.TItem2, RenderFragment<List<Type1>>")]
|
||||
public void GenericTypeNameRewriter_CanReplaceTypeParametersWithTypeArguments(string original, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var visitor = new GenericTypeNameRewriter(new Dictionary<string, string>()
|
||||
{
|
||||
{ "TItem1", "Type1" },
|
||||
{ "TItem2", "Type2" },
|
||||
{ "TItem3", null },
|
||||
});
|
||||
|
||||
// Act
|
||||
var actual = visitor.Rewrite(original);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, actual.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
// 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.CodeAnalysis.CSharp;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.CodeAnalysis.Razor
|
||||
{
|
||||
public class GlobalQualifiedTypeNameRewriterTest
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("String", "global::String")]
|
||||
[InlineData("System.String", "global::System.String")]
|
||||
[InlineData("TItem2", "TItem2")]
|
||||
[InlineData("System.Collections.Generic.List<System.String>", "global::System.Collections.Generic.List<global::System.String>")]
|
||||
[InlineData("System.Collections.Generic.Dictionary<System.String, TItem1>", "global::System.Collections.Generic.Dictionary<global::System.String, TItem1>")]
|
||||
[InlineData("System.Collections.TItem3.Dictionary<System.String, TItem1>", "global::System.Collections.TItem3.Dictionary<global::System.String, TItem1>")]
|
||||
[InlineData("System.Collections.TItem3.TItem1<System.String, TItem1>", "global::System.Collections.TItem3.TItem1<global::System.String, TItem1>")]
|
||||
|
||||
// This case is interesting because we know TITem2 to be a generic type parameter,
|
||||
// and we know that this will never be valid, which is why we don't bother rewriting.
|
||||
[InlineData("TItem2<System.String, TItem1>", "TItem2<global::System.String, TItem1>")]
|
||||
public void GlobalQualifiedTypeNameRewriter_CanQualifyNames(string original, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var visitor = new GlobalQualifiedTypeNameRewriter(new[] { "TItem1", "TItem2", "TItem3" });
|
||||
|
||||
// Act
|
||||
var actual = visitor.Rewrite(original);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, actual.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Language.Components;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.CodeAnalysis.Razor
|
||||
{
|
||||
public class RefTagHelperDescriptorProviderTest : BaseTagHelperDescriptorProviderTest
|
||||
{
|
||||
[Fact]
|
||||
public void Execute_CreatesDescriptor()
|
||||
{
|
||||
// Arrange
|
||||
var context = TagHelperDescriptorProviderContext.Create();
|
||||
var provider = new RefTagHelperDescriptorProvider();
|
||||
|
||||
// Act
|
||||
provider.Execute(context);
|
||||
|
||||
// Assert
|
||||
var matches = context.Results.Where(result => result.IsRefTagHelper());
|
||||
var item = Assert.Single(matches);
|
||||
|
||||
Assert.Empty(item.AllowedChildTags);
|
||||
Assert.Null(item.TagOutputHint);
|
||||
Assert.Empty(item.Diagnostics);
|
||||
Assert.False(item.HasErrors);
|
||||
Assert.Equal(BlazorMetadata.Ref.TagHelperKind, item.Kind);
|
||||
Assert.Equal(BlazorMetadata.Ref.RuntimeName, item.Metadata[TagHelperMetadata.Runtime.Name]);
|
||||
Assert.False(item.IsDefaultKind());
|
||||
Assert.False(item.KindUsesDefaultTagHelperRuntime());
|
||||
|
||||
Assert.Equal(
|
||||
"Populates the specified field or property with a reference to the element or component.",
|
||||
item.Documentation);
|
||||
|
||||
Assert.Equal("Microsoft.AspNetCore.Components", item.AssemblyName);
|
||||
Assert.Equal("Ref", item.Name);
|
||||
Assert.Equal("Microsoft.AspNetCore.Components.Ref", item.DisplayName);
|
||||
Assert.Equal("Microsoft.AspNetCore.Components.Ref", item.GetTypeName());
|
||||
|
||||
// The tag matching rule for a ref is just the attribute name "ref"
|
||||
var rule = Assert.Single(item.TagMatchingRules);
|
||||
Assert.Empty(rule.Diagnostics);
|
||||
Assert.False(rule.HasErrors);
|
||||
Assert.Null(rule.ParentTag);
|
||||
Assert.Equal("*", rule.TagName);
|
||||
Assert.Equal(TagStructure.Unspecified, rule.TagStructure);
|
||||
|
||||
var requiredAttribute = Assert.Single(rule.Attributes);
|
||||
Assert.Empty(requiredAttribute.Diagnostics);
|
||||
Assert.Equal("ref", requiredAttribute.DisplayName);
|
||||
Assert.Equal("ref", requiredAttribute.Name);
|
||||
Assert.Equal(RequiredAttributeDescriptor.NameComparisonMode.FullMatch, requiredAttribute.NameComparison);
|
||||
Assert.Null(requiredAttribute.Value);
|
||||
Assert.Equal(RequiredAttributeDescriptor.ValueComparisonMode.None, requiredAttribute.ValueComparison);
|
||||
|
||||
var attribute = Assert.Single(item.BoundAttributes);
|
||||
Assert.Empty(attribute.Diagnostics);
|
||||
Assert.False(attribute.HasErrors);
|
||||
Assert.Equal(BlazorMetadata.Ref.TagHelperKind, attribute.Kind);
|
||||
Assert.False(attribute.IsDefaultKind());
|
||||
Assert.False(attribute.HasIndexer);
|
||||
Assert.Null(attribute.IndexerNamePrefix);
|
||||
Assert.Null(attribute.IndexerTypeName);
|
||||
Assert.False(attribute.IsIndexerBooleanProperty);
|
||||
Assert.False(attribute.IsIndexerStringProperty);
|
||||
|
||||
Assert.Equal(
|
||||
"Populates the specified field or property with a reference to the element or component.",
|
||||
attribute.Documentation);
|
||||
|
||||
Assert.Equal("ref", attribute.Name);
|
||||
Assert.Equal("Ref", attribute.GetPropertyName());
|
||||
Assert.Equal("object Microsoft.AspNetCore.Components.Ref.Ref", attribute.DisplayName);
|
||||
Assert.Equal("System.Object", attribute.TypeName);
|
||||
Assert.False(attribute.IsStringProperty);
|
||||
Assert.False(attribute.IsBooleanProperty);
|
||||
Assert.False(attribute.IsEnum);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -48,14 +48,18 @@
|
|||
<MvcRazorExtenionOutput Include="$(ArtifactsBinDir)Microsoft.AspNetCore.Mvc.Razor.Extensions\$(Configuration)\netstandard2.0\Microsoft.AspNetCore.Mvc.Razor.Extensions.dll" />
|
||||
</ItemGroup>
|
||||
|
||||
<Copy SourceFiles="@(MvcRazorExtenionOutput)" DestinationFolder="$(SdkOutputPath)extensions\mvc-3-0\" />
|
||||
<Copy SourceFiles="@(MvcRazorExtenionOutput)" DestinationFolder="$(SdkOutputPath)extensions\mvc-3-0\" SkipUnchangedFiles="true">
|
||||
<Output TaskParameter="CopiedFiles" ItemName="FileWrites" />
|
||||
</Copy>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectOutput Include="$(ArtifactsBinDir)Microsoft.NET.Sdk.Razor\$(Configuration)\net46*\Microsoft.NET.Sdk.Razor.Tasks.*" />
|
||||
<ProjectOutput Include="$(ArtifactsBinDir)Microsoft.NET.Sdk.Razor\$(Configuration)\netstandard2.0*\Microsoft.NET.Sdk.Razor.Tasks.*" />
|
||||
</ItemGroup>
|
||||
|
||||
<Copy SourceFiles="@(ProjectOutput)" DestinationFiles="$(SdkOutputPath)tasks\%(RecursiveDir)%(FileName)%(Extension)" />
|
||||
<Copy SourceFiles="@(ProjectOutput)" DestinationFiles="$(SdkOutputPath)tasks\%(RecursiveDir)%(FileName)%(Extension)" SkipUnchangedFiles="true">
|
||||
<Output TaskParameter="CopiedFiles" ItemName="FileWrites" />
|
||||
</Copy>
|
||||
</Target>
|
||||
|
||||
<Target Name="PopulateNuspec" AfterTargets="InitializeStandardNuspecProperties" DependsOnTargets="LayoutDependencies">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Configures options for binding specific element types.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
|
||||
public sealed class BindElementAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructs an instance of <see cref="BindElementAttribute"/>.
|
||||
/// </summary>
|
||||
/// <param name="element">The tag name of the element.</param>
|
||||
/// <param name="suffix">The suffix value. For example, set this to <code>value</code> for <code>bind-value</code>, or set this to <code>null</code> for <code>bind</code>.</param>
|
||||
/// <param name="valueAttribute">The name of the value attribute to be bound.</param>
|
||||
/// <param name="changeAttribute">The name of an attribute that will register an associated change event.</param>
|
||||
public BindElementAttribute(string element, string suffix, string valueAttribute, string changeAttribute)
|
||||
{
|
||||
if (element == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(element));
|
||||
}
|
||||
|
||||
if (valueAttribute == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(valueAttribute));
|
||||
}
|
||||
|
||||
if (changeAttribute == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(changeAttribute));
|
||||
}
|
||||
|
||||
Element = element;
|
||||
ValueAttribute = valueAttribute;
|
||||
ChangeAttribute = changeAttribute;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the tag name of the element.
|
||||
/// </summary>
|
||||
public string Element { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the suffix value.
|
||||
/// For example, this will be <code>value</code> to mean <code>bind-value</code>, or <code>null</code> to mean <code>bind</code>.
|
||||
/// </summary>
|
||||
public string Suffix { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the value attribute to be bound.
|
||||
/// </summary>
|
||||
public string ValueAttribute { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of an attribute that will register an associated change event.
|
||||
/// </summary>
|
||||
public string ChangeAttribute { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Configures options for binding subtypes of an HTML <code>input</code> element.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
|
||||
public sealed class BindInputElementAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructs an instance of <see cref="BindInputElementAttribute"/>.
|
||||
/// </summary>
|
||||
/// <param name="type">The value of the element's <code>type</code> attribute.</param>
|
||||
/// <param name="suffix">The suffix value.</param>
|
||||
/// <param name="valueAttribute">The name of the value attribute to be bound.</param>
|
||||
/// <param name="changeAttribute">The name of an attribute that will register an associated change event.</param>
|
||||
public BindInputElementAttribute(string type, string suffix, string valueAttribute, string changeAttribute)
|
||||
{
|
||||
if (valueAttribute == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(valueAttribute));
|
||||
}
|
||||
|
||||
if (changeAttribute == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(changeAttribute));
|
||||
}
|
||||
|
||||
Type = type;
|
||||
Suffix = suffix;
|
||||
ValueAttribute = valueAttribute;
|
||||
ChangeAttribute = changeAttribute;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value of the element's <code>type</code> attribute.
|
||||
/// </summary>
|
||||
public string Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the suffix value.
|
||||
/// </summary>
|
||||
public string Suffix { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the value attribute to be bound.
|
||||
/// </summary>
|
||||
public string ValueAttribute { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of an attribute that will register an associated change event.
|
||||
/// </summary>
|
||||
public string ChangeAttribute { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,229 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Methods used internally by @bind syntax. Not intended to be used directly.
|
||||
/// </summary>
|
||||
public static class BindMethods
|
||||
{
|
||||
/// <summary>
|
||||
/// Not intended to be used directly.
|
||||
/// </summary>
|
||||
public static T GetValue<T>(T value) => value;
|
||||
|
||||
/// <summary>
|
||||
/// Not intended to be used directly.
|
||||
/// </summary>
|
||||
public static string GetValue(DateTime value, string format) =>
|
||||
value == default ? null
|
||||
: (format == null ? value.ToString() : value.ToString(format));
|
||||
|
||||
/// <summary>
|
||||
/// Not intended to be used directly.
|
||||
/// </summary>
|
||||
public static string GetEventHandlerValue<T>(string value)
|
||||
where T : UIEventArgs
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Not intended to be used directly.
|
||||
/// </summary>
|
||||
public static MulticastDelegate GetEventHandlerValue<T>(Action value)
|
||||
where T : UIEventArgs
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Not intended to be used directly.
|
||||
/// </summary>
|
||||
public static MulticastDelegate GetEventHandlerValue<T>(Func<Task> value)
|
||||
where T : UIEventArgs
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Not intended to be used directly.
|
||||
/// </summary>
|
||||
public static MulticastDelegate GetEventHandlerValue<T>(Action<T> value)
|
||||
where T : UIEventArgs
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Not intended to be used directly.
|
||||
/// </summary>
|
||||
public static MulticastDelegate GetEventHandlerValue<T>(Func<T, Task> value)
|
||||
where T : UIEventArgs
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Not intended to be used directly.
|
||||
/// </summary>
|
||||
public static Action<UIEventArgs> SetValueHandler(Action<string> setter, string existingValue)
|
||||
{
|
||||
return _ => setter((string)((UIChangeEventArgs)_).Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Not intended to be used directly.
|
||||
/// </summary>
|
||||
public static Action<UIEventArgs> SetValueHandler(Action<bool> setter, bool existingValue)
|
||||
{
|
||||
return _ => setter((bool)((UIChangeEventArgs)_).Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Not intended to be used directly.
|
||||
/// </summary>
|
||||
public static Action<UIEventArgs> SetValueHandler(Action<bool?> setter, bool? existingValue)
|
||||
{
|
||||
return _ => setter((bool?)((UIChangeEventArgs)_).Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Not intended to be used directly.
|
||||
/// </summary>
|
||||
public static Action<UIEventArgs> SetValueHandler(Action<int> setter, int existingValue)
|
||||
{
|
||||
return _ => setter(int.Parse((string)((UIChangeEventArgs)_).Value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Not intended to be used directly.
|
||||
/// </summary>
|
||||
public static Action<UIEventArgs> SetValueHandler(Action<int?> setter, int? existingValue)
|
||||
{
|
||||
return _ => setter(int.TryParse((string)((UIChangeEventArgs)_).Value, out var tmpvalue)
|
||||
? tmpvalue
|
||||
: (int?)null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Not intended to be used directly.
|
||||
/// </summary>
|
||||
public static Action<UIEventArgs> SetValueHandler(Action<long> setter, long existingValue)
|
||||
{
|
||||
return _ => setter(long.Parse((string)((UIChangeEventArgs)_).Value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Not intended to be used directly.
|
||||
/// </summary>
|
||||
public static Action<UIEventArgs> SetValueHandler(Action<long?> setter, long? existingValue)
|
||||
{
|
||||
return _ => setter(long.TryParse((string)((UIChangeEventArgs)_).Value, out var tmpvalue)
|
||||
? tmpvalue
|
||||
: (long?)null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Not intended to be used directly.
|
||||
/// </summary>
|
||||
public static Action<UIEventArgs> SetValueHandler(Action<float> setter, float existingValue)
|
||||
{
|
||||
return _ => setter(float.Parse((string)((UIChangeEventArgs)_).Value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Not intended to be used directly.
|
||||
/// </summary>
|
||||
public static Action<UIEventArgs> SetValueHandler(Action<float?> setter, float? existingValue)
|
||||
{
|
||||
return _ => setter(float.TryParse((string)((UIChangeEventArgs)_).Value, out var tmpvalue)
|
||||
? tmpvalue
|
||||
: (float?)null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Not intended to be used directly.
|
||||
/// </summary>
|
||||
public static Action<UIEventArgs> SetValueHandler(Action<double> setter, double existingValue)
|
||||
{
|
||||
return _ => setter(double.Parse((string)((UIChangeEventArgs)_).Value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Not intended to be used directly.
|
||||
/// </summary>
|
||||
public static Action<UIEventArgs> SetValueHandler(Action<double?> setter, double? existingValue)
|
||||
{
|
||||
return _ => setter(double.TryParse((string)((UIChangeEventArgs)_).Value, out var tmpvalue)
|
||||
? tmpvalue
|
||||
: (double?)null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Not intended to be used directly.
|
||||
/// </summary>
|
||||
public static Action<UIEventArgs> SetValueHandler(Action<decimal> setter, decimal existingValue)
|
||||
{
|
||||
return _ => setter(decimal.Parse((string)((UIChangeEventArgs)_).Value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Not intended to be used directly.
|
||||
/// </summary>
|
||||
public static Action<UIEventArgs> SetValueHandler(Action<decimal?> setter, decimal? existingValue)
|
||||
{
|
||||
return _ => setter(decimal.TryParse((string)((UIChangeEventArgs)_).Value, out var tmpvalue)
|
||||
? tmpvalue
|
||||
: (decimal?)null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Not intended to be used directly.
|
||||
/// </summary>
|
||||
public static Action<UIEventArgs> SetValueHandler(Action<DateTime> setter, DateTime existingValue)
|
||||
{
|
||||
return _ => SetDateTimeValue(setter, ((UIChangeEventArgs)_).Value, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Not intended to be used directly.
|
||||
/// </summary>
|
||||
public static Action<UIEventArgs> SetValueHandler(Action<DateTime> setter, DateTime existingValue, string format)
|
||||
{
|
||||
return _ => SetDateTimeValue(setter, ((UIChangeEventArgs)_).Value, format);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Not intended to be used directly.
|
||||
/// </summary>
|
||||
public static Action<UIEventArgs> SetValueHandler<T>(Action<T> setter, T existingValue)
|
||||
{
|
||||
if (!typeof(T).IsEnum)
|
||||
{
|
||||
throw new ArgumentException($"'bind' does not accept values of type {typeof(T).FullName}. To read and write this value type, wrap it in a property of type string with suitable getters and setters.");
|
||||
}
|
||||
|
||||
return _ =>
|
||||
{
|
||||
var value = (string)((UIChangeEventArgs)_).Value;
|
||||
var parsed = (T)Enum.Parse(typeof(T), value);
|
||||
setter(parsed);
|
||||
};
|
||||
}
|
||||
|
||||
private static void SetDateTimeValue(Action<DateTime> setter, object objValue, string format)
|
||||
{
|
||||
var stringValue = (string)objValue;
|
||||
var parsedValue = string.IsNullOrEmpty(stringValue) ? default
|
||||
: format != null && DateTime.TryParseExact(stringValue, format, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var parsedExact) ? parsedExact
|
||||
: DateTime.Parse(stringValue);
|
||||
setter(parsedValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Associates an event argument type with an event attribute name.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
|
||||
public sealed class EventHandlerAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructs an instance of <see cref="EventHandlerAttribute"/>.
|
||||
/// </summary>
|
||||
/// <param name="attributeName"></param>
|
||||
/// <param name="eventArgsType"></param>
|
||||
public EventHandlerAttribute(string attributeName, Type eventArgsType)
|
||||
{
|
||||
if (attributeName == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(attributeName));
|
||||
}
|
||||
|
||||
if (eventArgsType == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(eventArgsType));
|
||||
}
|
||||
|
||||
AttributeName = attributeName;
|
||||
EventArgsType = eventArgsType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the attribute name.
|
||||
/// </summary>
|
||||
public string AttributeName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the event argument type.
|
||||
/// </summary>
|
||||
public Type EventArgsType { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -12,9 +12,6 @@
|
|||
<RazorSdkCurrentVersionTargets>$(SolutionRoot)src\Microsoft.NET.Sdk.Razor\build\netstandard2.0\Sdk.Razor.CurrentVersion.targets</RazorSdkCurrentVersionTargets>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Import solution dependencies.props when building in place -->
|
||||
<Import Project="..\..\build\dependencies.props" Condition="'$(BinariesRoot)'==''"/>
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- aspnet/BuildTools#662 Don't police what version of NetCoreApp we use -->
|
||||
<NETCoreAppMaximumVersion>99.9</NETCoreAppMaximumVersion>
|
||||
|
|
|
|||
Loading…
Reference in New Issue