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 System.Linq;
|
||||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Razor.Language
|
namespace Microsoft.AspNetCore.Razor.Language.Components
|
||||||
{
|
{
|
||||||
internal static class ComponentDiagnosticFactory
|
internal static class ComponentDiagnosticFactory
|
||||||
{
|
{
|
||||||
|
|
@ -217,11 +217,11 @@ namespace Microsoft.AspNetCore.Razor.Language
|
||||||
"following top-level items: {1}.",
|
"following top-level items: {1}.",
|
||||||
RazorDiagnosticSeverity.Error);
|
RazorDiagnosticSeverity.Error);
|
||||||
|
|
||||||
//public static RazorDiagnostic Create_ChildContentMixedWithExplicitChildContent(SourceSpan? source, ComponentExtensionNode component)
|
public static RazorDiagnostic Create_ChildContentMixedWithExplicitChildContent(SourceSpan? source, ComponentExtensionNode component)
|
||||||
//{
|
{
|
||||||
// var supportedElements = string.Join(", ", component.Component.GetChildContentProperties().Select(p => $"'{p.Name}'"));
|
var supportedElements = string.Join(", ", component.Component.GetChildContentProperties().Select(p => $"'{p.Name}'"));
|
||||||
// return RazorDiagnostic.Create(ChildContentMixedWithExplicitChildContent, source ?? SourceSpan.Undefined, component.TagName, supportedElements);
|
return RazorDiagnostic.Create(ChildContentMixedWithExplicitChildContent, source ?? SourceSpan.Undefined, component.TagName, supportedElements);
|
||||||
//}
|
}
|
||||||
|
|
||||||
public static readonly RazorDiagnosticDescriptor ChildContentHasInvalidAttribute =
|
public static readonly RazorDiagnosticDescriptor ChildContentHasInvalidAttribute =
|
||||||
new RazorDiagnosticDescriptor(
|
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",
|
"element '{3}' of component '{4}'. Specify the parameter name like: '<{0} Context=\"another_name\"> to resolve the ambiguity",
|
||||||
RazorDiagnosticSeverity.Error);
|
RazorDiagnosticSeverity.Error);
|
||||||
|
|
||||||
//public static RazorDiagnostic Create_ChildContentRepeatedParameterName(
|
public static RazorDiagnostic Create_ChildContentRepeatedParameterName(
|
||||||
// SourceSpan? source,
|
SourceSpan? source,
|
||||||
// ComponentChildContentIntermediateNode childContent1,
|
ComponentChildContentIntermediateNode childContent1,
|
||||||
// ComponentExtensionNode component1,
|
ComponentExtensionNode component1,
|
||||||
// ComponentChildContentIntermediateNode childContent2,
|
ComponentChildContentIntermediateNode childContent2,
|
||||||
// ComponentExtensionNode component2)
|
ComponentExtensionNode component2)
|
||||||
//{
|
{
|
||||||
// Debug.Assert(childContent1.ParameterName == childContent2.ParameterName);
|
Debug.Assert(childContent1.ParameterName == childContent2.ParameterName);
|
||||||
// Debug.Assert(childContent1.IsParameterized);
|
Debug.Assert(childContent1.IsParameterized);
|
||||||
// Debug.Assert(childContent2.IsParameterized);
|
Debug.Assert(childContent2.IsParameterized);
|
||||||
|
|
||||||
// return RazorDiagnostic.Create(
|
return RazorDiagnostic.Create(
|
||||||
// ChildContentRepeatedParameterName,
|
ChildContentRepeatedParameterName,
|
||||||
// source ?? SourceSpan.Undefined,
|
source ?? SourceSpan.Undefined,
|
||||||
// childContent1.AttributeName,
|
childContent1.AttributeName,
|
||||||
// component1.TagName,
|
component1.TagName,
|
||||||
// childContent1.ParameterName,
|
childContent1.ParameterName,
|
||||||
// childContent2.AttributeName,
|
childContent2.AttributeName,
|
||||||
// component2.TagName);
|
component2.TagName);
|
||||||
//}
|
}
|
||||||
|
|
||||||
public static readonly RazorDiagnosticDescriptor GenericComponentMissingTypeArgument =
|
public static readonly RazorDiagnosticDescriptor GenericComponentMissingTypeArgument =
|
||||||
new RazorDiagnosticDescriptor(
|
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}.",
|
() => "The component '{0}' is missing required type arguments. Specify the missing types using the attributes: {1}.",
|
||||||
RazorDiagnosticSeverity.Error);
|
RazorDiagnosticSeverity.Error);
|
||||||
|
|
||||||
//public static RazorDiagnostic Create_GenericComponentMissingTypeArgument(
|
public static RazorDiagnostic Create_GenericComponentMissingTypeArgument(
|
||||||
// SourceSpan? source,
|
SourceSpan? source,
|
||||||
// ComponentExtensionNode component,
|
ComponentExtensionNode component,
|
||||||
// IEnumerable<BoundAttributeDescriptor> attributes)
|
IEnumerable<BoundAttributeDescriptor> attributes)
|
||||||
//{
|
{
|
||||||
// Debug.Assert(component.Component.IsGenericTypedComponent());
|
Debug.Assert(component.Component.IsGenericTypedComponent());
|
||||||
|
|
||||||
// var attributesText = string.Join(", ", attributes.Select(a => $"'{a.Name}'"));
|
var attributesText = string.Join(", ", attributes.Select(a => $"'{a.Name}'"));
|
||||||
// return RazorDiagnostic.Create(GenericComponentMissingTypeArgument, source ?? SourceSpan.Undefined, component.TagName, attributesText);
|
return RazorDiagnostic.Create(GenericComponentMissingTypeArgument, source ?? SourceSpan.Undefined, component.TagName, attributesText);
|
||||||
//}
|
}
|
||||||
|
|
||||||
public static readonly RazorDiagnosticDescriptor GenericComponentTypeInferenceUnderspecified =
|
public static readonly RazorDiagnosticDescriptor GenericComponentTypeInferenceUnderspecified =
|
||||||
new RazorDiagnosticDescriptor(
|
new RazorDiagnosticDescriptor(
|
||||||
|
|
@ -297,16 +297,16 @@ namespace Microsoft.AspNetCore.Razor.Language
|
||||||
"directly using the following attributes: {1}.",
|
"directly using the following attributes: {1}.",
|
||||||
RazorDiagnosticSeverity.Error);
|
RazorDiagnosticSeverity.Error);
|
||||||
|
|
||||||
//public static RazorDiagnostic Create_GenericComponentTypeInferenceUnderspecified(
|
public static RazorDiagnostic Create_GenericComponentTypeInferenceUnderspecified(
|
||||||
// SourceSpan? source,
|
SourceSpan? source,
|
||||||
// ComponentExtensionNode component,
|
ComponentExtensionNode component,
|
||||||
// IEnumerable<BoundAttributeDescriptor> attributes)
|
IEnumerable<BoundAttributeDescriptor> attributes)
|
||||||
//{
|
{
|
||||||
// Debug.Assert(component.Component.IsGenericTypedComponent());
|
Debug.Assert(component.Component.IsGenericTypedComponent());
|
||||||
|
|
||||||
// var attributesText = string.Join(", ", attributes.Select(a => $"'{a.Name}'"));
|
var attributesText = string.Join(", ", attributes.Select(a => $"'{a.Name}'"));
|
||||||
// return RazorDiagnostic.Create(GenericComponentTypeInferenceUnderspecified, source ?? SourceSpan.Undefined, component.TagName, attributesText);
|
return RazorDiagnostic.Create(GenericComponentTypeInferenceUnderspecified, source ?? SourceSpan.Undefined, component.TagName, attributesText);
|
||||||
//}
|
}
|
||||||
|
|
||||||
public static readonly RazorDiagnosticDescriptor ChildContentHasInvalidParameterOnComponent =
|
public static readonly RazorDiagnosticDescriptor ChildContentHasInvalidParameterOnComponent =
|
||||||
new RazorDiagnosticDescriptor(
|
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.",
|
() => "Invalid parameter name. The parameter name attribute '{0}' on component '{1}' can only include literal text.",
|
||||||
RazorDiagnosticSeverity.Error);
|
RazorDiagnosticSeverity.Error);
|
||||||
|
|
||||||
//public static RazorDiagnostic Create_ChildContentHasInvalidParameterOnComponent(SourceSpan? source, string attribute, string element)
|
public static RazorDiagnostic Create_ChildContentHasInvalidParameterOnComponent(SourceSpan? source, string attribute, string element)
|
||||||
//{
|
{
|
||||||
// return RazorDiagnostic.Create(ChildContentHasInvalidParameterOnComponent, source ?? SourceSpan.Undefined, attribute, 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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
@ -135,5 +135,8 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
|
||||||
|
|
||||||
return true;
|
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);
|
visitor.Visit(node);
|
||||||
return visitor.Directives;
|
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
|
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);
|
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.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Razor.Language;
|
using Microsoft.AspNetCore.Razor.Language;
|
||||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
|
||||||
using Microsoft.Extensions.CommandLineUtils;
|
using Microsoft.Extensions.CommandLineUtils;
|
||||||
using Microsoft.VisualStudio.LanguageServices.Razor.Serialization;
|
using Microsoft.VisualStudio.LanguageServices.Razor.Serialization;
|
||||||
using Newtonsoft.Json;
|
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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
using Microsoft.AspNetCore.Razor.Language;
|
using Microsoft.AspNetCore.Razor.Language;
|
||||||
|
using Microsoft.AspNetCore.Razor.Language.Components;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp;
|
||||||
|
|
||||||
namespace Microsoft.CodeAnalysis.Razor
|
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 int Order { get; set; }
|
||||||
|
|
||||||
public void Execute(TagHelperDescriptorProviderContext context)
|
public void Execute(TagHelperDescriptorProviderContext context)
|
||||||
|
|
@ -25,72 +40,422 @@ namespace Microsoft.CodeAnalysis.Razor
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var componentSymbol = compilation.GetTypeByMetadataName(TagHelperTypes.IComponent);
|
// We need to see private members too
|
||||||
if (componentSymbol == null || componentSymbol.TypeKind == TypeKind.Error)
|
compilation = WithMetadataImportOptionsAll(compilation);
|
||||||
{
|
|
||||||
// Could not find attributes we care about in the compilation. Nothing to do.
|
var symbols = BlazorSymbols.Create(compilation);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var types = new List<INamedTypeSymbol>();
|
var types = new List<INamedTypeSymbol>();
|
||||||
var visitor = new ComponentTypeVisitor(componentSymbol, types);
|
var visitor = new ComponentTypeVisitor(symbols, types);
|
||||||
|
|
||||||
// We always visit the global namespace.
|
|
||||||
visitor.Visit(compilation.Assembly.GlobalNamespace);
|
|
||||||
|
|
||||||
|
// Visit the primary output of this compilation, as well as all references.
|
||||||
|
visitor.Visit(compilation.Assembly);
|
||||||
foreach (var reference in compilation.References)
|
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 (compilation.GetAssemblyOrModuleSymbol(reference) is IAssemblySymbol assembly)
|
||||||
{
|
{
|
||||||
if (IsTagHelperAssembly(assembly))
|
visitor.Visit(assembly);
|
||||||
{
|
|
||||||
visitor.Visit(assembly.GlobalNamespace);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var i = 0; i < types.Count; i++)
|
for (var i = 0; i < types.Count; i++)
|
||||||
{
|
{
|
||||||
var type = types[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 assemblyName = type.ContainingAssembly.Identity.Name;
|
||||||
|
|
||||||
var descriptorBuilder = TagHelperDescriptorBuilder.Create(TagHelperConventions.ComponentKind, typeName, assemblyName);
|
var builder = TagHelperDescriptorBuilder.Create(BlazorMetadata.Component.TagHelperKind, typeName, assemblyName);
|
||||||
descriptorBuilder.SetTypeName(typeName);
|
builder.SetTypeName(typeName);
|
||||||
descriptorBuilder.Metadata[TagHelperMetadata.Runtime.Name] = TagHelperConventions.ComponentKind;
|
|
||||||
|
// 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.
|
// 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;
|
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.
|
private void CreateTypeParameterProperty(TagHelperDescriptorBuilder builder, ITypeSymbol typeParameter)
|
||||||
internal class ComponentTypeVisitor : SymbolVisitor
|
|
||||||
{
|
{
|
||||||
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;
|
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;
|
_results = results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -122,11 +487,16 @@ namespace Microsoft.CodeAnalysis.Razor
|
||||||
|
|
||||||
internal bool IsComponent(INamedTypeSymbol symbol)
|
internal bool IsComponent(INamedTypeSymbol symbol)
|
||||||
{
|
{
|
||||||
|
if (_symbols == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
symbol.DeclaredAccessibility == Accessibility.Public &&
|
symbol.DeclaredAccessibility == Accessibility.Public &&
|
||||||
!symbol.IsAbstract &&
|
!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;
|
using System.Runtime.CompilerServices;
|
||||||
[assembly: InternalsVisibleTo("rzls, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
[assembly: InternalsVisibleTo("rzls, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||||
|
[assembly: InternalsVisibleTo("rzc, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||||
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Razor.LanguageServer.Test, 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, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||||
[assembly: InternalsVisibleTo("Microsoft.VisualStudio.LiveShare.Razor.Test, 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" />
|
<MvcRazorExtenionOutput Include="$(ArtifactsBinDir)Microsoft.AspNetCore.Mvc.Razor.Extensions\$(Configuration)\netstandard2.0\Microsoft.AspNetCore.Mvc.Razor.Extensions.dll" />
|
||||||
</ItemGroup>
|
</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>
|
<ItemGroup>
|
||||||
<ProjectOutput Include="$(ArtifactsBinDir)Microsoft.NET.Sdk.Razor\$(Configuration)\net46*\Microsoft.NET.Sdk.Razor.Tasks.*" />
|
<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.*" />
|
<ProjectOutput Include="$(ArtifactsBinDir)Microsoft.NET.Sdk.Razor\$(Configuration)\netstandard2.0*\Microsoft.NET.Sdk.Razor.Tasks.*" />
|
||||||
</ItemGroup>
|
</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>
|
||||||
|
|
||||||
<Target Name="PopulateNuspec" AfterTargets="InitializeStandardNuspecProperties" DependsOnTargets="LayoutDependencies">
|
<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>
|
<RazorSdkCurrentVersionTargets>$(SolutionRoot)src\Microsoft.NET.Sdk.Razor\build\netstandard2.0\Sdk.Razor.CurrentVersion.targets</RazorSdkCurrentVersionTargets>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<!-- Import solution dependencies.props when building in place -->
|
|
||||||
<Import Project="..\..\build\dependencies.props" Condition="'$(BinariesRoot)'==''"/>
|
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- aspnet/BuildTools#662 Don't police what version of NetCoreApp we use -->
|
<!-- aspnet/BuildTools#662 Don't police what version of NetCoreApp we use -->
|
||||||
<NETCoreAppMaximumVersion>99.9</NETCoreAppMaximumVersion>
|
<NETCoreAppMaximumVersion>99.9</NETCoreAppMaximumVersion>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue