From 50fa3ee3e32cdb2530ced59ef4daeb28d2c7a249 Mon Sep 17 00:00:00 2001 From: "N. Taylor Mullen" Date: Sat, 13 Sep 2014 14:41:31 -0700 Subject: [PATCH] Create TagHelper specific C# code Generation. - Added TagHelperChunk generation. - Added CSharp visitors to understand TagHelperChunks and render corresponding C# code. - Refactored some code in the CSharpCodeVisitor so it could be utilized in other classes. - Added a CSharpFieldDeclarationVisitor to render declaration pieces for TagHelper's - Added metadata to represent specific TagHelper code generation constructs. #72 --- .../Common/HashCodeCombiner.cs | 6 + .../CodeBuilder/CSharp/CSharpCodeBuilder.cs | 2 +- .../CodeBuilder/CSharp/CSharpCodeWriter.cs | 31 +- .../CSharp/CSharpTagHelperCodeRenderer.cs | 486 ++++++++++++++++++ .../CSharp/Visitors/CSharpCodeVisitor.cs | 76 ++- .../CSharpTagHelperFieldDeclarationVisitor.cs | 93 ++++ .../CSharp/Visitors/CSharpUsingVisitor.cs | 19 + .../Compiler/CodeBuilder/ChunkVisitor.cs | 5 + .../Compiler/CodeBuilder/CodeVisitor.cs | 3 + .../Chunks/TagHelpers/TagHelperChunk.cs | 33 ++ .../Compiler/CodeTree/CodeTreeBuilder.cs | 13 +- .../Generator/GeneratedClassContext.cs | 103 ++-- .../Generator/GeneratedTagHelperContext.cs | 117 +++++ .../Generator/TagHelperCodeGenerator.cs | 62 ++- .../TagHelpers/TagHelperBlockBuilder.cs | 9 +- .../TagHelpers/TagHelperParseTreeRewriter.cs | 24 +- .../Properties/RazorResources.Designer.cs | 32 ++ src/Microsoft.AspNet.Razor/RazorEngineHost.cs | 8 + .../RazorResources.resx | 6 + .../TagHelperAttributeValueCodeRenderer.cs | 35 ++ .../TagHelpers/TagHelperDescriptor.cs | 28 +- .../TagHelpers/TagHelperDescriptorComparer.cs | 59 +++ .../TagHelpers/TagHelperDescriptorProvider.cs | 24 +- 23 files changed, 1164 insertions(+), 110 deletions(-) create mode 100644 src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/CSharpTagHelperCodeRenderer.cs create mode 100644 src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/Visitors/CSharpTagHelperFieldDeclarationVisitor.cs create mode 100644 src/Microsoft.AspNet.Razor/Generator/Compiler/CodeTree/Chunks/TagHelpers/TagHelperChunk.cs create mode 100644 src/Microsoft.AspNet.Razor/Generator/GeneratedTagHelperContext.cs create mode 100644 src/Microsoft.AspNet.Razor/TagHelpers/TagHelperAttributeValueCodeRenderer.cs create mode 100644 src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptorComparer.cs diff --git a/src/Microsoft.AspNet.Razor/Common/HashCodeCombiner.cs b/src/Microsoft.AspNet.Razor/Common/HashCodeCombiner.cs index 8653f38ded..b8158f3af7 100644 --- a/src/Microsoft.AspNet.Razor/Common/HashCodeCombiner.cs +++ b/src/Microsoft.AspNet.Razor/Common/HashCodeCombiner.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections; +using System.Collections.Generic; namespace Microsoft.Internal.Web.Utils { @@ -46,6 +47,11 @@ namespace Microsoft.Internal.Web.Utils return this; } + public HashCodeCombiner Add(TValue value, IEqualityComparer comparer) + { + return Add(comparer.GetHashCode(value)); + } + public static HashCodeCombiner Start() { return new HashCodeCombiner(); diff --git a/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/CSharpCodeBuilder.cs b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/CSharpCodeBuilder.cs index 9fea08fa89..97c4987f4f 100644 --- a/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/CSharpCodeBuilder.cs +++ b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/CSharpCodeBuilder.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Open Technologies, Inc. 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.Threading.Tasks; @@ -56,6 +55,7 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp new CSharpHelperVisitor(writer, Context).Accept(Tree.Chunks); new CSharpTypeMemberVisitor(writer, Context).Accept(Tree.Chunks); new CSharpDesignTimeHelpersVisitor(writer, Context).AcceptTree(Tree); + new CSharpTagHelperFieldDeclarationVisitor(writer, Context).Accept(Tree.Chunks); BuildConstructor(writer); diff --git a/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/CSharpCodeWriter.cs b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/CSharpCodeWriter.cs index 4109d3657e..41435495d1 100644 --- a/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/CSharpCodeWriter.cs +++ b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/CSharpCodeWriter.cs @@ -12,6 +12,8 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp { public class CSharpCodeWriter : CodeWriter { + private const string InstanceMethodFormat = "{0}.{1}"; + public CSharpCodeWriter() { LineMappingManager = new LineMappingManager(); @@ -208,7 +210,7 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp return WriteStartMethodInvocation(methodName, new string[0]); } - public CSharpCodeWriter WriteStartMethodInvocation(string methodName, string[] genericArguments) + public CSharpCodeWriter WriteStartMethodInvocation(string methodName, params string[] genericArguments) { Write(methodName); @@ -235,6 +237,33 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp return this; } + // Writes a method invocation for the given instance name. + public CSharpCodeWriter WriteInstanceMethodInvocation([NotNull] string instanceName, + [NotNull] string methodName, + params string[] parameters) + { + return WriteInstanceMethodInvocation(instanceName, methodName, endLine: true, parameters: parameters); + } + + // Writes a method invocation for the given instance name. + public CSharpCodeWriter WriteInstanceMethodInvocation([NotNull] string instanceName, + [NotNull] string methodName, + bool endLine, + params string[] parameters) + { + return WriteMethodInvocation( + string.Format(CultureInfo.InvariantCulture, InstanceMethodFormat, instanceName, methodName), + endLine, + parameters); + } + + public CSharpCodeWriter WriteStartInstanceMethodInvocation([NotNull] string instanceName, + [NotNull] string methodName) + { + return WriteStartMethodInvocation( + string.Format(CultureInfo.InvariantCulture, InstanceMethodFormat, instanceName, methodName)); + } + public CSharpCodeWriter WriteMethodInvocation(string methodName, params string[] parameters) { return WriteMethodInvocation(methodName, endLine: true, parameters: parameters); diff --git a/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/CSharpTagHelperCodeRenderer.cs b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/CSharpTagHelperCodeRenderer.cs new file mode 100644 index 0000000000..a4efa1ff8b --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/CSharpTagHelperCodeRenderer.cs @@ -0,0 +1,486 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.Globalization; +using System.Linq; +using Microsoft.AspNet.Razor.TagHelpers; + +namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp +{ + /// + /// Renders tag helper rendering code. + /// + public class CSharpTagHelperCodeRenderer + { + internal static readonly string ExecutionContextVariableName = "__tagHelperExecutionContext"; + internal static readonly string StringValueBufferVariableName = "__tagHelperStringValueBuffer"; + internal static readonly string ScopeManagerVariableName = "__tagHelperScopeManager"; + internal static readonly string RunnerVariableName = "__tagHelperRunner"; + + private static readonly TagHelperAttributeDescriptorComparer AttributeDescriptorComparer = + new TagHelperAttributeDescriptorComparer(); + + // TODO: The work to properly implement this will be done in: https://github.com/aspnet/Razor/issues/74 + private readonly TagHelperAttributeValueCodeRenderer _attributeValueCodeRenderer = + new TagHelperAttributeValueCodeRenderer(); + private readonly CSharpCodeWriter _writer; + private readonly CodeBuilderContext _context; + private readonly IChunkVisitor _bodyVisitor; + private readonly GeneratedTagHelperContext _tagHelperContext; + + /// + /// Instantiates a new . + /// + /// The used to render chunks found in the body. + /// The used to write code. + /// A instance that contains information about + /// the current code generation process. + public CSharpTagHelperCodeRenderer([NotNull] IChunkVisitor bodyVisitor, + [NotNull] CSharpCodeWriter writer, + [NotNull] CodeBuilderContext context) + { + _writer = writer; + _context = context; + _bodyVisitor = bodyVisitor; + _tagHelperContext = context.Host.GeneratedClassContext.GeneratedTagHelperContext; + } + + /// + /// Renders the code for the given . + /// + /// A to render. + public void RenderTagHelper(TagHelperChunk chunk) + { + // TODO: Implement design time support for tag helpers in https://github.com/aspnet/Razor/issues/83 + if (_context.Host.DesignTimeMode) + { + return; + } + + var tagHelperDescriptors = chunk.Descriptors; + + // Find the first content behavior that doesn't have a content behavior of None. + // The resolver restricts content behavior collisions so the first one that's not None will be + // the content behavior we need to abide by. None can work in unison with other ContentBehaviors. + var contentBehavior = tagHelperDescriptors.Select(descriptor => descriptor.ContentBehavior) + .FirstOrDefault( + behavior => behavior != ContentBehavior.None); + + RenderBeginTagHelperScope(chunk.TagName); + + RenderTagHelpersCreation(chunk); + + var attributeDescriptors = tagHelperDescriptors.SelectMany(descriptor => descriptor.Attributes); + var boundHTMLAttributes = attributeDescriptors.Select(descriptor => descriptor.AttributeName); + var htmlAttributes = chunk.Attributes; + var unboundHTMLAttributes = + htmlAttributes.Where(htmlAttribute => !boundHTMLAttributes.Contains(htmlAttribute.Key, + StringComparer.OrdinalIgnoreCase)); + + RenderUnboundHTMLAttributes(unboundHTMLAttributes); + + switch (contentBehavior) + { + case ContentBehavior.None: + RenderRunTagHelpers(bufferedBody: false); + RenderTagOutput(_tagHelperContext.OutputGenerateStartTagMethodName); + RenderTagHelperBody(chunk.Children, bufferBody: false); + RenderTagOutput(_tagHelperContext.OutputGenerateEndTagMethodName); + break; + case ContentBehavior.Append: + RenderRunTagHelpers(bufferedBody: false); + RenderTagOutput(_tagHelperContext.OutputGenerateStartTagMethodName); + RenderTagHelperBody(chunk.Children, bufferBody: false); + RenderTagOutput(_tagHelperContext.OutputGenerateContentMethodName); + RenderTagOutput(_tagHelperContext.OutputGenerateEndTagMethodName); + break; + case ContentBehavior.Prepend: + RenderRunTagHelpers(bufferedBody: false); + RenderTagOutput(_tagHelperContext.OutputGenerateStartTagMethodName); + RenderTagOutput(_tagHelperContext.OutputGenerateContentMethodName); + RenderTagHelperBody(chunk.Children, bufferBody: false); + RenderTagOutput(_tagHelperContext.OutputGenerateEndTagMethodName); + break; + case ContentBehavior.Replace: + RenderRunTagHelpers(bufferedBody: false); + RenderTagOutput(_tagHelperContext.OutputGenerateStartTagMethodName); + RenderTagOutput(_tagHelperContext.OutputGenerateContentMethodName); + RenderTagOutput(_tagHelperContext.OutputGenerateEndTagMethodName); + break; + case ContentBehavior.Modify: + RenderTagHelperBody(chunk.Children, bufferBody: true); + RenderRunTagHelpers(bufferedBody: true); + RenderTagOutput(_tagHelperContext.OutputGenerateStartTagMethodName); + RenderTagOutput(_tagHelperContext.OutputGenerateContentMethodName); + RenderTagOutput(_tagHelperContext.OutputGenerateEndTagMethodName); + break; + } + + RenderEndTagHelpersScope(); + } + + internal static string GetVariableName(TagHelperDescriptor descriptor) + { + return "__" + descriptor.TagHelperName.Replace('.', '_'); + } + + private void RenderBeginTagHelperScope(string tagName) + { + // Call into the tag helper scope manager to start a new tag helper scope. + // Also capture the value as the current execution context. + _writer.WriteStartAssignment(ExecutionContextVariableName) + .WriteStartInstanceMethodInvocation(ScopeManagerVariableName, + _tagHelperContext.ScopeManagerBeginMethodName); + _writer.WriteStringLiteral(tagName) + .WriteEndMethodInvocation(); + } + + private void RenderTagHelpersCreation(TagHelperChunk chunk) + { + var tagHelperDescriptors = chunk.Descriptors; + + // This is to maintain value accessors for attributes when creating the TagHelpers. + // Ultimately it enables us to do scenarios like this: + // myTagHelper1.Foo = DateTime.Now; + // myTagHelper2.Foo = myTagHelper1.Foo; + var htmlAttributeValues = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var tagHelperDescriptor in tagHelperDescriptors) + { + var tagHelperVariableName = GetVariableName(tagHelperDescriptor); + + // Create the tag helper + _writer.WriteStartAssignment(tagHelperVariableName) + .WriteStartMethodInvocation(_tagHelperContext.CreateTagHelperMethodName, + tagHelperDescriptor.TagHelperName) + .WriteEndMethodInvocation(); + + _writer.WriteInstanceMethodInvocation(ExecutionContextVariableName, + _tagHelperContext.ExecutionContextAddMethodName, + tagHelperVariableName); + + // Render all of the bound attribute values for the tag helper. + RenderBoundHTMLAttributes(chunk.Attributes, + tagHelperVariableName, + tagHelperDescriptor.Attributes, + htmlAttributeValues); + } + } + + private void RenderBoundHTMLAttributes(IDictionary chunkAttributes, + string tagHelperVariableName, + IEnumerable attributeDescriptors, + Dictionary htmlAttributeValues) + { + foreach (var attributeDescriptor in attributeDescriptors) + { + Chunk attributeValueChunk; + + var providedAttribute = chunkAttributes.TryGetValue(attributeDescriptor.AttributeName, + out attributeValueChunk); + + if (providedAttribute) + { + var attributeValueRecorded = htmlAttributeValues.ContainsKey(attributeDescriptor.AttributeName); + + // Bufferable attributes are attributes that can have Razor code inside of them. + var bufferableAttribute = IsStringAttribute(attributeDescriptor); + + // Plain text values are non Razor code (@DateTime.Now) values. If an attribute is bufferable it + // may be more than just a plain text value, it may also contain Razor code which is why we attempt + // to retrieve a plain text value here. + string textValue; + var isPlainTextValue = TryGetPlainTextValue(attributeValueChunk, out textValue); + + // If we haven't recorded a value and we need to buffer an attribute value and the value is not + // plain text then we need to prepare the value prior to setting it below. + if (!attributeValueRecorded && bufferableAttribute && !isPlainTextValue) + { + BuildBufferedWritingScope(attributeValueChunk); + } + + // We capture the tag helpers property value accessor so we can retrieve it later (if we need to). + var valueAccessor = string.Format(CultureInfo.InvariantCulture, + "{0}.{1}", + tagHelperVariableName, + attributeDescriptor.AttributePropertyName); + + _writer.WriteStartAssignment(valueAccessor); + + // If we haven't recorded this attribute value before then we need to record its value. + if (!attributeValueRecorded) + { + // We only need to create attribute values once per HTML element (not once per tag helper). + // We're saving the value accessor so we can retrieve it later if there are more tag helpers that + // need the value. + htmlAttributeValues.Add(attributeDescriptor.AttributeName, valueAccessor); + + if (bufferableAttribute) + { + // If the attribute is bufferable but has a plain text value that means the value + // is a string which needs to be surrounded in quotes. + if (isPlainTextValue) + { + RenderQuotedAttributeValue(textValue, attributeDescriptor); + } + else + { + // The value contains more than plain text. e.g. someAttribute="Time: @DateTime.Now" + RenderBufferedAttributeValue(attributeDescriptor); + } + } + else + { + // TODO: Make complex types in non-bufferable attributes work in + // https://github.com/aspnet/Razor/issues/129 + if (!isPlainTextValue) + { + throw new InvalidOperationException( + RazorResources.FormatTagHelpers_AttributesThatAreNotStringsMustNotContainAtSymbols( + attributeDescriptor.AttributePropertyName)); + } + + // We aren't a bufferable attribute which means we have no Razor code in our value. + // Therefore we can just use the "textValue" as the attribute value. + RenderRawAttributeValue(textValue, attributeDescriptor); + } + + // End the assignment to the attribute. + _writer.WriteLine(";"); + + // We need to inform the context of the attribute value. + _writer.WriteStartInstanceMethodInvocation( + ExecutionContextVariableName, + _tagHelperContext.ExecutionContextAddTagHelperAttributeMethodName); + + _writer.WriteStringLiteral(attributeDescriptor.AttributeName) + .WriteParameterSeparator() + .Write(valueAccessor) + .WriteEndMethodInvocation(); + } + else + { + // The attribute value has already been recorded, lets retrieve it from the stored value accessors. + _writer.Write(htmlAttributeValues[attributeDescriptor.AttributeName]) + .WriteLine(";"); + } + } + } + } + + private void RenderUnboundHTMLAttributes(IEnumerable> unboundHTMLAttributes) + { + // Build out the unbound HTML attributes for the tag builder + foreach (var htmlAttribute in unboundHTMLAttributes) + { + string textValue; + var attributeValue = htmlAttribute.Value; + var isPlainTextValue = TryGetPlainTextValue(attributeValue, out textValue); + + // HTML attributes are always strings. So if this value is not plain text i.e. if the value contains + // C# code, then we need to buffer it. + if (!isPlainTextValue) + { + BuildBufferedWritingScope(attributeValue); + } + + _writer.WriteStartInstanceMethodInvocation(ExecutionContextVariableName, + _tagHelperContext.ExecutionContextAddHtmlAttributeMethodName); + _writer.WriteStringLiteral(htmlAttribute.Key) + .WriteParameterSeparator(); + + // If it's a plain text value then we need to surround the value with quotes. + if (isPlainTextValue) + { + _writer.WriteStringLiteral(textValue); + } + else + { + RenderBufferedAttributeValueAccessor(_writer); + } + + _writer.WriteEndMethodInvocation(); + } + } + + private void RenderTagHelperBody(IList children, bool bufferBody) + { + // If we want to buffer the body we need to create a writing scope to capture the body content. + if (bufferBody) + { + // Render all of the tag helper children in a buffered writing scope. + BuildBufferedWritingScope(children); + } + else + { + // Render all of the tag helper children. + _bodyVisitor.Accept(children); + } + } + + private void RenderEndTagHelpersScope() + { + _writer.WriteStartAssignment(ExecutionContextVariableName) + .WriteInstanceMethodInvocation(ScopeManagerVariableName, + _tagHelperContext.ScopeManagerEndMethodName); + } + + private void RenderTagOutput(string tagOutputMethodName) + { + CSharpCodeVisitor.RenderPreWriteStart(_writer, _context); + + _writer.Write(ExecutionContextVariableName) + .Write(".") + .Write(_tagHelperContext.ExecutionContextOutputPropertyName) + .Write(".") + .WriteMethodInvocation(tagOutputMethodName, endLine: false) + .WriteEndMethodInvocation(); + } + + private void RenderRunTagHelpers(bool bufferedBody) + { + _writer.Write(ExecutionContextVariableName) + .Write(".") + .Write(_tagHelperContext.ExecutionContextOutputPropertyName) + .Write(" = ") + .WriteStartInstanceMethodInvocation(RunnerVariableName, + _tagHelperContext.RunnerRunAsyncMethodName); + _writer.Write(ExecutionContextVariableName); + + if (bufferedBody) + { + _writer.WriteParameterSeparator() + .Write(StringValueBufferVariableName); + } + + _writer.WriteEndMethodInvocation(endLine: false) + .WriteLine(".Result;"); + } + + private void RenderBufferedAttributeValue(TagHelperAttributeDescriptor attributeDescriptor) + { + RenderAttributeValue( + attributeDescriptor, + valueRenderer: (writer) => + { + RenderBufferedAttributeValueAccessor(writer); + }); + } + + private void RenderRawAttributeValue(string value, TagHelperAttributeDescriptor attributeDescriptor) + { + RenderAttributeValue( + attributeDescriptor, + valueRenderer: (writer) => + { + writer.Write(value); + }); + } + + private void RenderQuotedAttributeValue(string value, TagHelperAttributeDescriptor attributeDescriptor) + { + RenderAttributeValue( + attributeDescriptor, + valueRenderer: (writer) => + { + writer.WriteStringLiteral(value); + }); + } + + private void BuildBufferedWritingScope(Chunk htmlAttributeChunk) + { + // Render a buffered writing scope for the html attribute value. + BuildBufferedWritingScope(new[] { htmlAttributeChunk }); + } + + private void BuildBufferedWritingScope(IList chunks) + { + // We're building a writing scope around the provided chunks which captures everything written from the + // page. Therefore, we do not want to write to any other buffer since we're using the pages buffer to + // ensure we capture all content that's written, directly or indirectly. + var oldWriter = _context.TargetWriterName; + _context.TargetWriterName = null; + + // Need to disable instrumentation inside of writing scopes, the instrumentation will not detect + // content written inside writing scopes. + var oldInstrumentation = _context.Host.EnableInstrumentation; + + try + { + _context.Host.EnableInstrumentation = false; + + _writer.WriteMethodInvocation(_tagHelperContext.StartWritingScopeMethodName); + + _bodyVisitor.Accept(chunks); + + _writer.WriteStartAssignment(StringValueBufferVariableName) + .WriteMethodInvocation(_tagHelperContext.EndWritingScopeMethodName); + } + finally + { + // Reset instrumentation back to what it was, leaving the writing scope. + _context.Host.EnableInstrumentation = oldInstrumentation; + + // Reset the writer/buffer back to what it was, leaving the writing scope. + _context.TargetWriterName = oldWriter; + } + } + + private void RenderAttributeValue(TagHelperAttributeDescriptor attributeDescriptor, + Action valueRenderer) + { + _attributeValueCodeRenderer.RenderAttributeValue(attributeDescriptor, _writer, _context, valueRenderer); + } + + private static void RenderBufferedAttributeValueAccessor(CSharpCodeWriter writer) + { + writer.WriteInstanceMethodInvocation(StringValueBufferVariableName, + "ToString", + endLine: false); + } + + private static bool IsStringAttribute(TagHelperAttributeDescriptor attributeDescriptor) + { + return attributeDescriptor.PropertyInfo.PropertyType == typeof(string); + } + + private static bool TryGetPlainTextValue(Chunk chunk, out string plainText) + { + var chunkBlock = chunk as ChunkBlock; + + plainText = null; + + if (chunkBlock == null || chunkBlock.Children.Count != 1) + { + return false; + } + + var literalChildChunk = chunkBlock.Children[0] as LiteralChunk; + + if (literalChildChunk == null) + { + return false; + } + + plainText = literalChildChunk.Text; + + return true; + } + + // This class is used to compare tag helper attributes by comparing only the HTML attribute name. + private class TagHelperAttributeDescriptorComparer : IEqualityComparer + { + public bool Equals(TagHelperAttributeDescriptor descriptorX, TagHelperAttributeDescriptor descriptorY) + { + return descriptorX.AttributeName.Equals(descriptorY.AttributeName, StringComparison.OrdinalIgnoreCase); + } + + public int GetHashCode(TagHelperAttributeDescriptor descriptor) + { + return StringComparer.OrdinalIgnoreCase.GetHashCode(descriptor.AttributeName); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/Visitors/CSharpCodeVisitor.cs b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/Visitors/CSharpCodeVisitor.cs index 0fb7aae03d..49e308e116 100644 --- a/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/Visitors/CSharpCodeVisitor.cs +++ b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/Visitors/CSharpCodeVisitor.cs @@ -6,6 +6,7 @@ using System.Globalization; using System.Linq; using Microsoft.AspNet.Razor.Parser.SyntaxTree; using Microsoft.AspNet.Razor.Text; +using Microsoft.AspNet.Razor.TagHelpers; namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp { @@ -16,11 +17,23 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp private const string TemplateWriterName = "__razor_template_writer"; private CSharpPaddingBuilder _paddingBuilder; + private CSharpTagHelperCodeRenderer _tagHelperCodeRenderer; public CSharpCodeVisitor(CSharpCodeWriter writer, CodeBuilderContext context) : base(writer, context) { _paddingBuilder = new CSharpPaddingBuilder(context.Host); + _tagHelperCodeRenderer = new CSharpTagHelperCodeRenderer(this, writer, context); + } + + protected override void Visit(TagHelperChunk chunk) + { + _tagHelperCodeRenderer.RenderTagHelper(chunk); + } + + protected override void Visit(ChunkBlock chunk) + { + Accept(chunk.Children); } protected override void Visit(SetLayoutChunk chunk) @@ -72,21 +85,12 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp { if (Context.ExpressionRenderingMode == ExpressionRenderingMode.WriteToOutput) { - if (!String.IsNullOrEmpty(Context.TargetWriterName)) - { - Writer.WriteStartMethodInvocation(Context.Host.GeneratedClassContext.WriteLiteralToMethodName) - .Write(Context.TargetWriterName) - .WriteParameterSeparator(); - } - else - { - Writer.WriteStartMethodInvocation(Context.Host.GeneratedClassContext.WriteLiteralMethodName); - } + RenderPreWriteStart(); } Writer.WriteStartMethodInvocation(Context.Host.GeneratedClassContext.ResolveUrlMethodName) - .WriteStringLiteral(chunk.Url) - .WriteEndMethodInvocation(endLine: false); + .WriteStringLiteral(chunk.Url) + .WriteEndMethodInvocation(endLine: false); if (Context.ExpressionRenderingMode == ExpressionRenderingMode.WriteToOutput) { @@ -115,17 +119,18 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp if (!string.IsNullOrEmpty(Context.TargetWriterName)) { - Writer.WriteStartMethodInvocation(Context.Host.GeneratedClassContext.WriteLiteralToMethodName) - .Write(Context.TargetWriterName) - .WriteParameterSeparator(); - } - else - { - Writer.WriteStartMethodInvocation(Context.Host.GeneratedClassContext.WriteLiteralMethodName); - } + if (Context.ExpressionRenderingMode == ExpressionRenderingMode.WriteToOutput) + { + RenderPreWriteStart(); + } - Writer.WriteStringLiteral(chunk.Text) - .WriteEndMethodInvocation(); + Writer.WriteStringLiteral(chunk.Text); + + if (Context.ExpressionRenderingMode == ExpressionRenderingMode.WriteToOutput) + { + Writer.WriteEndMethodInvocation(); + } + } if (Context.Host.EnableInstrumentation) { @@ -187,10 +192,10 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp Writer.WriteParameterSeparator() .Write(chunk.Start.AbsoluteIndex.ToString(CultureInfo.CurrentCulture)) - .WriteEndMethodInvocation(false) + .WriteEndMethodInvocation(endLine: false) .WriteParameterSeparator() - .WriteBooleanLiteral(false) - .WriteEndMethodInvocation(false); + .WriteBooleanLiteral(value: false) + .WriteEndMethodInvocation(endLine: false); } else { @@ -479,5 +484,26 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp return Context.Host.EnableInstrumentation && Context.ExpressionRenderingMode == ExpressionRenderingMode.WriteToOutput; } + + private CSharpCodeWriter RenderPreWriteStart() + { + return RenderPreWriteStart(Writer, Context); + } + + public static CSharpCodeWriter RenderPreWriteStart(CSharpCodeWriter writer, CodeBuilderContext context) + { + if (!string.IsNullOrEmpty(context.TargetWriterName)) + { + writer.WriteStartMethodInvocation(context.Host.GeneratedClassContext.WriteLiteralToMethodName) + .Write(context.TargetWriterName) + .WriteParameterSeparator(); + } + else + { + writer.WriteStartMethodInvocation(context.Host.GeneratedClassContext.WriteLiteralMethodName); + } + + return writer; + } } } diff --git a/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/Visitors/CSharpTagHelperFieldDeclarationVisitor.cs b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/Visitors/CSharpTagHelperFieldDeclarationVisitor.cs new file mode 100644 index 0000000000..be9705f518 --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/Visitors/CSharpTagHelperFieldDeclarationVisitor.cs @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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; + +namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp +{ + public class CSharpTagHelperFieldDeclarationVisitor : CodeVisitor + { + private readonly HashSet _declaredTagHelpers; + private readonly GeneratedTagHelperContext _tagHelperContext; + private bool _foundTagHelpers; + + public CSharpTagHelperFieldDeclarationVisitor([NotNull] CSharpCodeWriter writer, + [NotNull] CodeBuilderContext context) + : base(writer, context) + { + _declaredTagHelpers = new HashSet(StringComparer.Ordinal); + _tagHelperContext = Context.Host.GeneratedClassContext.GeneratedTagHelperContext; + } + + protected override void Visit(TagHelperChunk chunk) + { + // We only want to setup tag helper manager fields if there are tag helpers, and only once + if (!_foundTagHelpers) + { + _foundTagHelpers = true; + + Writer.WriteLineHiddenDirective(); + + WritePrivateField(typeof(TextWriter).FullName, + CSharpTagHelperCodeRenderer.StringValueBufferVariableName, + value: null); + + WritePrivateField(_tagHelperContext.ExecutionContextTypeName, + CSharpTagHelperCodeRenderer.ExecutionContextVariableName, + value: null); + + WritePrivateField(_tagHelperContext.RunnerTypeName, + CSharpTagHelperCodeRenderer.RunnerVariableName, + "new " + _tagHelperContext.RunnerTypeName + "()"); + + WritePrivateField(_tagHelperContext.ScopeManagerTypeName, + CSharpTagHelperCodeRenderer.ScopeManagerVariableName, + "new " + _tagHelperContext.ScopeManagerTypeName + "()"); + } + + foreach (var descriptor in chunk.Descriptors) + { + if (!_declaredTagHelpers.Contains(descriptor.TagHelperName)) + { + _declaredTagHelpers.Add(descriptor.TagHelperName); + + WritePrivateField(descriptor.TagHelperName, + CSharpTagHelperCodeRenderer.GetVariableName(descriptor), + value: null); + } + } + + // We need to dive deeper to ensure we pick up any nested tag helpers. + Accept(chunk.Children); + } + + public override void Accept(Chunk chunk) + { + var chunkBlock = chunk as ChunkBlock; + + // If we're any ChunkBlock other than TagHelperChunk then we want to dive into its Children + // to search for more TagHelperChunk chunks. This if-statement enables us to not override + // each of the special ChunkBlock types and then dive into their children. + if (chunkBlock != null && !(chunkBlock is TagHelperChunk)) + { + Accept(chunkBlock.Children); + } + else + { + // If we're a TagHelperChunk or any other non ChunkBlock we ".Accept" it. This ensures + // that our overriden Visit(TagHelperChunk) method gets called and is not skipped over. + // If we're a non ChunkBlock or a TagHelperChunk then we want to just invoke the Visit + // method for that given chunk (base.Accept indirectly calls the Visit method). + base.Accept(chunk); + } + } + + private void WritePrivateField(string type, string name, string value) + { + Writer.Write("private ") + .WriteVariableDeclaration(type, name, value); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/Visitors/CSharpUsingVisitor.cs b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/Visitors/CSharpUsingVisitor.cs index 3208ba740e..eda684ede8 100644 --- a/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/Visitors/CSharpUsingVisitor.cs +++ b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/Visitors/CSharpUsingVisitor.cs @@ -9,6 +9,10 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp { public class CSharpUsingVisitor : CodeVisitor { + private const string TagHelpersRuntimeNamespace = "Microsoft.AspNet.Razor.Runtime.TagHelpers"; + + private bool _foundTagHelpers; + public CSharpUsingVisitor(CSharpCodeWriter writer, CodeBuilderContext context) : base(writer, context) { @@ -46,5 +50,20 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp Writer.WriteLine(";"); } } + + protected override void Visit(TagHelperChunk chunk) + { + if (!_foundTagHelpers) + { + _foundTagHelpers = true; + + if (!ImportedUsings.Contains(TagHelpersRuntimeNamespace)) + { + // If we find TagHelpers then we need to add the TagHelper runtime namespace to our list of usings. + Writer.WriteUsing(TagHelpersRuntimeNamespace); + ImportedUsings.Add(TagHelpersRuntimeNamespace); + } + } + } } } diff --git a/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/ChunkVisitor.cs b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/ChunkVisitor.cs index 595be02fa3..4d2a9389f1 100644 --- a/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/ChunkVisitor.cs +++ b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/ChunkVisitor.cs @@ -53,6 +53,10 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler { Visit((StatementChunk)chunk); } + else if(chunk is TagHelperChunk) + { + Visit((TagHelperChunk)chunk); + } else if(chunk is SetLayoutChunk) { Visit((SetLayoutChunk)chunk); @@ -110,6 +114,7 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler protected abstract void Visit(LiteralChunk chunk); protected abstract void Visit(ExpressionChunk chunk); protected abstract void Visit(StatementChunk chunk); + protected abstract void Visit(TagHelperChunk chunk); protected abstract void Visit(UsingChunk chunk); protected abstract void Visit(ChunkBlock chunk); protected abstract void Visit(DynamicCodeAttributeChunk chunk); diff --git a/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CodeVisitor.cs b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CodeVisitor.cs index f8536e7486..5e964489cd 100644 --- a/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CodeVisitor.cs +++ b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CodeVisitor.cs @@ -29,6 +29,9 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler protected override void Visit(DynamicCodeAttributeChunk chunk) { } + protected override void Visit(TagHelperChunk chunk) + { + } protected override void Visit(LiteralCodeAttributeChunk chunk) { } diff --git a/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeTree/Chunks/TagHelpers/TagHelperChunk.cs b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeTree/Chunks/TagHelpers/TagHelperChunk.cs new file mode 100644 index 0000000000..4e5f5c1478 --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeTree/Chunks/TagHelpers/TagHelperChunk.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.AspNet.Razor.TagHelpers; + +namespace Microsoft.AspNet.Razor.Generator.Compiler +{ + /// + /// A that represents a special HTML tag. + /// + public class TagHelperChunk : ChunkBlock + { + /// + /// The HTML attributes. + /// + /// + /// These attributes are => so attribute values can consist + /// of all sorts of Razor specific pieces. + /// + public IDictionary Attributes { get; set; } + + /// + /// The s that are associated with the tag helpers HTML element. + /// + public IEnumerable Descriptors { get; set; } + + /// + /// The HTML tag name. + /// + public string TagName { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeTree/CodeTreeBuilder.cs b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeTree/CodeTreeBuilder.cs index 1f1be5f723..1dcb51a214 100644 --- a/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeTree/CodeTreeBuilder.cs +++ b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeTree/CodeTreeBuilder.cs @@ -146,13 +146,18 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler public T StartChunkBlock(SyntaxTreeNode association, bool topLevel) where T : ChunkBlock, new() { - var chunk = new T(); + var chunkBlock = new T(); - AddChunk(chunk, association, topLevel); + return StartChunkBlock(chunkBlock, association, topLevel); + } - _blockChain.Push(chunk); + public T StartChunkBlock(T chunkBlock, SyntaxTreeNode association, bool topLevel) where T : ChunkBlock + { + AddChunk(chunkBlock, association, topLevel); - return chunk; + _blockChain.Push(chunkBlock); + + return chunkBlock; } public void EndChunkBlock() diff --git a/src/Microsoft.AspNet.Razor/Generator/GeneratedClassContext.cs b/src/Microsoft.AspNet.Razor/Generator/GeneratedClassContext.cs index 5ad2684a29..12dd5f060f 100644 --- a/src/Microsoft.AspNet.Razor/Generator/GeneratedClassContext.cs +++ b/src/Microsoft.AspNet.Razor/Generator/GeneratedClassContext.cs @@ -3,8 +3,6 @@ using System; using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using Microsoft.Internal.Web.Utils; namespace Microsoft.AspNet.Razor.Generator { @@ -17,35 +15,39 @@ namespace Microsoft.AspNet.Razor.Generator public static readonly string DefaultWriteAttributeMethodName = "WriteAttribute"; public static readonly string DefaultWriteAttributeToMethodName = "WriteAttributeTo"; - public static readonly GeneratedClassContext Default = new GeneratedClassContext(DefaultExecuteMethodName, - DefaultWriteMethodName, - DefaultWriteLiteralMethodName); + public static readonly GeneratedClassContext Default = + new GeneratedClassContext(DefaultExecuteMethodName, + DefaultWriteMethodName, + DefaultWriteLiteralMethodName, + new GeneratedTagHelperContext()); - public GeneratedClassContext(string executeMethodName, string writeMethodName, string writeLiteralMethodName) + public GeneratedClassContext(string executeMethodName, + string writeMethodName, + string writeLiteralMethodName, + [NotNull] GeneratedTagHelperContext generatedTagHelperContext) : this() { - if (String.IsNullOrEmpty(executeMethodName)) + if (string.IsNullOrEmpty(executeMethodName)) { - throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, - CommonResources.Argument_Cannot_Be_Null_Or_Empty, - "executeMethodName"), - "executeMethodName"); + throw new ArgumentException( + CommonResources.Argument_Cannot_Be_Null_Or_Empty, + nameof(executeMethodName)); } - if (String.IsNullOrEmpty(writeMethodName)) + if (string.IsNullOrEmpty(writeMethodName)) { - throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, - CommonResources.Argument_Cannot_Be_Null_Or_Empty, - "writeMethodName"), - "writeMethodName"); + throw new ArgumentException( + CommonResources.Argument_Cannot_Be_Null_Or_Empty, + nameof(writeMethodName)); } - if (String.IsNullOrEmpty(writeLiteralMethodName)) + if (string.IsNullOrEmpty(writeLiteralMethodName)) { - throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, - CommonResources.Argument_Cannot_Be_Null_Or_Empty, - "writeLiteralMethodName"), - "writeLiteralMethodName"); + throw new ArgumentException( + CommonResources.Argument_Cannot_Be_Null_Or_Empty, + nameof(writeLiteralMethodName)); } + GeneratedTagHelperContext = generatedTagHelperContext; + WriteMethodName = writeMethodName; WriteLiteralMethodName = writeLiteralMethodName; ExecuteMethodName = executeMethodName; @@ -65,8 +67,12 @@ namespace Microsoft.AspNet.Razor.Generator string writeLiteralMethodName, string writeToMethodName, string writeLiteralToMethodName, - string templateTypeName) - : this(executeMethodName, writeMethodName, writeLiteralMethodName) + string templateTypeName, + GeneratedTagHelperContext generatedTagHelperContext) + : this(executeMethodName, + writeMethodName, + writeLiteralMethodName, + generatedTagHelperContext) { WriteToMethodName = writeToMethodName; WriteLiteralToMethodName = writeLiteralToMethodName; @@ -79,8 +85,15 @@ namespace Microsoft.AspNet.Razor.Generator string writeToMethodName, string writeLiteralToMethodName, string templateTypeName, - string defineSectionMethodName) - : this(executeMethodName, writeMethodName, writeLiteralMethodName, writeToMethodName, writeLiteralToMethodName, templateTypeName) + string defineSectionMethodName, + GeneratedTagHelperContext generatedTagHelperContext) + : this(executeMethodName, + writeMethodName, + writeLiteralMethodName, + writeToMethodName, + writeLiteralToMethodName, + templateTypeName, + generatedTagHelperContext) { DefineSectionMethodName = defineSectionMethodName; } @@ -93,18 +106,28 @@ namespace Microsoft.AspNet.Razor.Generator string templateTypeName, string defineSectionMethodName, string beginContextMethodName, - string endContextMethodName) - : this(executeMethodName, writeMethodName, writeLiteralMethodName, writeToMethodName, writeLiteralToMethodName, templateTypeName, defineSectionMethodName) + string endContextMethodName, + GeneratedTagHelperContext generatedTagHelperContext) + : this(executeMethodName, + writeMethodName, + writeLiteralMethodName, + writeToMethodName, + writeLiteralToMethodName, + templateTypeName, + defineSectionMethodName, + generatedTagHelperContext) { BeginContextMethodName = beginContextMethodName; EndContextMethodName = endContextMethodName; } + // Required Items public string WriteMethodName { get; private set; } public string WriteLiteralMethodName { get; private set; } public string WriteToMethodName { get; private set; } public string WriteLiteralToMethodName { get; private set; } public string ExecuteMethodName { get; private set; } + public GeneratedTagHelperContext GeneratedTagHelperContext { get; private set; } // Optional Items public string BeginContextMethodName { get; set; } @@ -120,17 +143,17 @@ namespace Microsoft.AspNet.Razor.Generator public bool AllowSections { - get { return !String.IsNullOrEmpty(DefineSectionMethodName); } + get { return !string.IsNullOrEmpty(DefineSectionMethodName); } } public bool AllowTemplates { - get { return !String.IsNullOrEmpty(TemplateTypeName); } + get { return !string.IsNullOrEmpty(TemplateTypeName); } } public bool SupportsInstrumentation { - get { return !String.IsNullOrEmpty(BeginContextMethodName) && !String.IsNullOrEmpty(EndContextMethodName); } + get { return !string.IsNullOrEmpty(BeginContextMethodName) && !string.IsNullOrEmpty(EndContextMethodName); } } public override bool Equals(object obj) @@ -139,16 +162,16 @@ namespace Microsoft.AspNet.Razor.Generator { return false; } - GeneratedClassContext other = (GeneratedClassContext)obj; - return String.Equals(DefineSectionMethodName, other.DefineSectionMethodName, StringComparison.Ordinal) && - String.Equals(WriteMethodName, other.WriteMethodName, StringComparison.Ordinal) && - String.Equals(WriteLiteralMethodName, other.WriteLiteralMethodName, StringComparison.Ordinal) && - String.Equals(WriteToMethodName, other.WriteToMethodName, StringComparison.Ordinal) && - String.Equals(WriteLiteralToMethodName, other.WriteLiteralToMethodName, StringComparison.Ordinal) && - String.Equals(ExecuteMethodName, other.ExecuteMethodName, StringComparison.Ordinal) && - String.Equals(TemplateTypeName, other.TemplateTypeName, StringComparison.Ordinal) && - String.Equals(BeginContextMethodName, other.BeginContextMethodName, StringComparison.Ordinal) && - String.Equals(EndContextMethodName, other.EndContextMethodName, StringComparison.Ordinal); + var other = (GeneratedClassContext)obj; + return string.Equals(DefineSectionMethodName, other.DefineSectionMethodName, StringComparison.Ordinal) && + string.Equals(WriteMethodName, other.WriteMethodName, StringComparison.Ordinal) && + string.Equals(WriteLiteralMethodName, other.WriteLiteralMethodName, StringComparison.Ordinal) && + string.Equals(WriteToMethodName, other.WriteToMethodName, StringComparison.Ordinal) && + string.Equals(WriteLiteralToMethodName, other.WriteLiteralToMethodName, StringComparison.Ordinal) && + string.Equals(ExecuteMethodName, other.ExecuteMethodName, StringComparison.Ordinal) && + string.Equals(TemplateTypeName, other.TemplateTypeName, StringComparison.Ordinal) && + string.Equals(BeginContextMethodName, other.BeginContextMethodName, StringComparison.Ordinal) && + string.Equals(EndContextMethodName, other.EndContextMethodName, StringComparison.Ordinal); } public override int GetHashCode() diff --git a/src/Microsoft.AspNet.Razor/Generator/GeneratedTagHelperContext.cs b/src/Microsoft.AspNet.Razor/Generator/GeneratedTagHelperContext.cs new file mode 100644 index 0000000000..be427b916b --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Generator/GeneratedTagHelperContext.cs @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Razor.Generator +{ + /// + /// Contains necessary information for the tag helper code generation process. + /// + public class GeneratedTagHelperContext + { + /// + /// Instantiates a new instance of the with default values. + /// + public GeneratedTagHelperContext() + { + CreateTagHelperMethodName = "CreateTagHelper"; + RunnerRunAsyncMethodName = "RunAsync"; + ScopeManagerBeginMethodName = "Begin"; + ScopeManagerEndMethodName = "End"; + OutputGenerateStartTagMethodName = "GenerateStartTag"; + OutputGenerateContentMethodName = "GenerateContent"; + OutputGenerateEndTagMethodName = "GenerateEndTag"; + ExecutionContextAddMethodName = "Add"; + ExecutionContextAddTagHelperAttributeMethodName = "AddTagHelperAttribute"; + ExecutionContextAddHtmlAttributeMethodName = "AddHtmlAttribute"; + ExecutionContextOutputPropertyName = "Output"; + StartWritingScopeMethodName = "StartWritingScope"; + EndWritingScopeMethodName = "EndWritingScope"; + RunnerTypeName = "TagHelperRunner"; + ScopeManagerTypeName = "TagHelperScopeManager"; + ExecutionContextTypeName = "TagHelperExecutionContext"; + } + + /// + /// The name of the method used to create a tag helper. + /// + public string CreateTagHelperMethodName { get; set; } + + /// + /// The name of the method used to run tag helpers. + /// + public string RunnerRunAsyncMethodName { get; set; } + + /// + /// The name of the method used to start a scope. + /// + public string ScopeManagerBeginMethodName { get; set; } + + /// + /// The name of the method used to end a scope. + /// + public string ScopeManagerEndMethodName { get; set; } + + /// + /// The name of the method used to generate a tag helper output's start tag. + /// + public string OutputGenerateStartTagMethodName { get; set; } + + /// + /// The name of the method used to generate a tag helper output's content. + /// + public string OutputGenerateContentMethodName { get; set; } + + /// + /// The name of the method used to generate a tag helper output's end tag. + /// + public string OutputGenerateEndTagMethodName { get; set; } + + /// + /// The name of the method used to add tag helper attributes. + /// + public string ExecutionContextAddTagHelperAttributeMethodName { get; set; } + + /// + /// The name of the method used to add HTML attributes. + /// + public string ExecutionContextAddHtmlAttributeMethodName { get; set; } + + /// + /// The name of the method used to add tag helpers. + /// + public string ExecutionContextAddMethodName { get; set; } + + /// + /// The property accessor for the tag helper's output. + /// + public string ExecutionContextOutputPropertyName { get; set; } + + /// + /// The name of the method used to start a new writing scope. + /// + public string StartWritingScopeMethodName { get; set; } + + /// + /// The name of the method used to end a writing scope. + /// + public string EndWritingScopeMethodName { get; set; } + + /// + /// The name of the type used to run tag helpers. + /// + public string RunnerTypeName { get; set; } + + /// + /// The name of the type used to create scoped instances. + /// + public string ScopeManagerTypeName { get; set; } + + /// + /// The name of the type describing a specific tag helper scope. + /// + /// + /// Contains information about in-scope tag helpers, HTML attributes, and the tag helpers' output. + /// + public string ExecutionContextTypeName { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor/Generator/TagHelperCodeGenerator.cs b/src/Microsoft.AspNet.Razor/Generator/TagHelperCodeGenerator.cs index d44101b208..c291972ea6 100644 --- a/src/Microsoft.AspNet.Razor/Generator/TagHelperCodeGenerator.cs +++ b/src/Microsoft.AspNet.Razor/Generator/TagHelperCodeGenerator.cs @@ -1,7 +1,12 @@ // Copyright (c) Microsoft Open Technologies, Inc. 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.AspNet.Razor.Generator.Compiler; using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Parser.TagHelpers; +using Microsoft.AspNet.Razor.TagHelpers; namespace Microsoft.AspNet.Razor.Generator { @@ -10,6 +15,19 @@ namespace Microsoft.AspNet.Razor.Generator /// public class TagHelperCodeGenerator : BlockCodeGenerator { + private IEnumerable _tagHelperDescriptors; + + /// + /// Instantiates a new . + /// + /// + /// s associated with the current HTML tag. + /// + public TagHelperCodeGenerator(IEnumerable tagHelperDescriptors) + { + _tagHelperDescriptors = tagHelperDescriptors; + } + /// /// Starts the generation of a . /// @@ -20,11 +38,52 @@ namespace Microsoft.AspNet.Razor.Generator /// the current code generation process. public override void GenerateStartBlockCode(Block target, CodeGeneratorContext context) { + var tagHelperBlock = target as TagHelperBlock; + + if (tagHelperBlock == null) + { + throw new ArgumentException( + RazorResources.TagHelpers_TagHelperCodeGeneartorMustBeAssociatedWithATagHelperBlock); + } + + var attributes = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // We need to create a code generator to create chunks for each of the attributes. + var codeGenerator = context.Host.CreateCodeGenerator( + context.ClassName, + context.RootNamespace, + context.SourceFile); + + foreach (var attribute in tagHelperBlock.Attributes) + { + // Populates the code tree with chunks associated with attributes + attribute.Value.Accept(codeGenerator); + + var chunks = codeGenerator.Context.CodeTreeBuilder.CodeTree.Chunks; + + attributes[attribute.Key] = new ChunkBlock + { + Children = chunks + }; + + // Reset the code tree builder so we can build a new one for the next attribute + codeGenerator.Context.CodeTreeBuilder = new CodeTreeBuilder(); + } + + context.CodeTreeBuilder.StartChunkBlock( + new TagHelperChunk + { + TagName = tagHelperBlock.TagName, + Attributes = attributes, + Descriptors = _tagHelperDescriptors + }, + target, + topLevel: false); } /// /// Ends the generation of a capturing all previously visited children - /// since method was called. + /// since the method was called. /// /// /// The responsible for this . @@ -33,6 +92,7 @@ namespace Microsoft.AspNet.Razor.Generator /// the current code generation process. public override void GenerateEndBlockCode(Block target, CodeGeneratorContext context) { + context.CodeTreeBuilder.EndChunkBlock(); } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockBuilder.cs b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockBuilder.cs index c2f7b9a2ce..3fc4fc7dc3 100644 --- a/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockBuilder.cs +++ b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockBuilder.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.AspNet.Razor.Generator; using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.TagHelpers; using Microsoft.AspNet.Razor.Tokenizer.Symbols; namespace Microsoft.AspNet.Razor.Parser.TagHelpers @@ -33,12 +34,14 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers /// and from the . /// /// An HTML tag name. + /// The s associated with the current HTML + /// tag. /// The that contains all information about the start /// of the HTML element. - public TagHelperBlockBuilder(string tagName, Block startTag) + public TagHelperBlockBuilder(string tagName, IEnumerable descriptors, Block startTag) { TagName = tagName; - CodeGenerator = new TagHelperCodeGenerator(); + CodeGenerator = new TagHelperCodeGenerator(descriptors); Type = startTag.Type; Attributes = GetTagAttributes(startTag); } @@ -51,7 +54,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers TagName = tagName; Attributes = attributes; Type = BlockType.Tag; - CodeGenerator = new TagHelperCodeGenerator(); + CodeGenerator = new TagHelperCodeGenerator(tagHelperDescriptors: null); // Children is IList, no AddRange foreach (var child in children) diff --git a/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperParseTreeRewriter.cs b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperParseTreeRewriter.cs index 2e536f0b05..01330aaf96 100644 --- a/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperParseTreeRewriter.cs +++ b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperParseTreeRewriter.cs @@ -65,19 +65,25 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal { // We're in a begin tag block - if (IsPotentialTagHelper(tagName, childBlock) && IsRegisteredTagHelper(tagName)) + if (IsPotentialTagHelper(tagName, childBlock)) { - // Found a new tag helper block - TrackTagHelperBlock(new TagHelperBlockBuilder(tagName, childBlock)); + var descriptors = _provider.GetTagHelpers(tagName); - // If it's a self closing block then we don't have to worry about nested children - // within the tag... complete it. - if (IsSelfClosing(childBlock)) + // We could be a tag helper, but only if we have descriptors registered + if (descriptors.Any()) { - BuildCurrentlyTrackedTagHelperBlock(); - } + // Found a new tag helper block + TrackTagHelperBlock(new TagHelperBlockBuilder(tagName, descriptors, childBlock)); - continue; + // If it's a self closing block then we don't have to worry about nested children + // within the tag... complete it. + if (IsSelfClosing(childBlock)) + { + BuildCurrentlyTrackedTagHelperBlock(); + } + + continue; + } } } else diff --git a/src/Microsoft.AspNet.Razor/Properties/RazorResources.Designer.cs b/src/Microsoft.AspNet.Razor/Properties/RazorResources.Designer.cs index bf85a90990..34467e11d5 100644 --- a/src/Microsoft.AspNet.Razor/Properties/RazorResources.Designer.cs +++ b/src/Microsoft.AspNet.Razor/Properties/RazorResources.Designer.cs @@ -1462,6 +1462,38 @@ namespace Microsoft.AspNet.Razor return GetString("TagHelpers_CannotHaveCSharpInTagDeclaration"); } + /// + /// A TagHelperCodeGenerator must only be used with TagHelperBlocks. + /// + internal static string TagHelpers_TagHelperCodeGeneartorMustBeAssociatedWithATagHelperBlock + { + get { return GetString("TagHelpers_TagHelperCodeGeneartorMustBeAssociatedWithATagHelperBlock"); } + } + + /// + /// A TagHelperCodeGenerator must only be used with TagHelperBlocks. + /// + internal static string FormatTagHelpers_TagHelperCodeGeneartorMustBeAssociatedWithATagHelperBlock() + { + return GetString("TagHelpers_TagHelperCodeGeneartorMustBeAssociatedWithATagHelperBlock"); + } + + /// + /// TagHelper attributes that do not expect strings must not have @ symbols within them. Found attribute '{0}' with an invalid value. + /// + internal static string TagHelpers_AttributesThatAreNotStringsMustNotContainAtSymbols + { + get { return GetString("TagHelpers_AttributesThatAreNotStringsMustNotContainAtSymbols"); } + } + + /// + /// TagHelper attributes that do not expect strings must not have @ symbols within them. Found attribute '{0}' with an invalid value. + /// + internal static string FormatTagHelpers_AttributesThatAreNotStringsMustNotContainAtSymbols(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelpers_AttributesThatAreNotStringsMustNotContainAtSymbols"), p0); + } + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNet.Razor/RazorEngineHost.cs b/src/Microsoft.AspNet.Razor/RazorEngineHost.cs index 6a08de75b8..39d4edcdb2 100644 --- a/src/Microsoft.AspNet.Razor/RazorEngineHost.cs +++ b/src/Microsoft.AspNet.Razor/RazorEngineHost.cs @@ -215,5 +215,13 @@ namespace Microsoft.AspNet.Razor } return incomingBuilder; } + + // If a user wants to modify the code generation process they do it via the DecorateCodeGenerator method which + // is why this is internal. + internal RazorCodeGenerator CreateCodeGenerator(string className, string rootNamespace, string sourceFileName) + { + return DecorateCodeGenerator( + CodeLanguage.CreateCodeGenerator(className, rootNamespace, sourceFileName, host: this)); + } } } diff --git a/src/Microsoft.AspNet.Razor/RazorResources.resx b/src/Microsoft.AspNet.Razor/RazorResources.resx index 9ea5df2833..6fafd8e7b9 100644 --- a/src/Microsoft.AspNet.Razor/RazorResources.resx +++ b/src/Microsoft.AspNet.Razor/RazorResources.resx @@ -409,4 +409,10 @@ Instead, wrap the contents of the block in "{{}}": Tag Helpers cannot have C# in an HTML tag element's attribute declaration area. + + A TagHelperCodeGenerator must only be used with TagHelperBlocks. + + + TagHelper attributes that do not expect strings must not have @ symbols within them. Found attribute '{0}' with an invalid value. + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperAttributeValueCodeRenderer.cs b/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperAttributeValueCodeRenderer.cs new file mode 100644 index 0000000000..e5334c99c5 --- /dev/null +++ b/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperAttributeValueCodeRenderer.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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 System.Linq; +using System.Reflection; +using Microsoft.AspNet.Razor.Generator; +using Microsoft.AspNet.Razor.Generator.Compiler.CSharp; + +namespace Microsoft.AspNet.Razor.TagHelpers +{ + /// + /// Renders code for tag helper property initialization. + /// + public class TagHelperAttributeValueCodeRenderer + { + /// + /// Called during Razor's code generation process to generate code that instantiates the value of the tag + /// helper's property. Last value written should not be or end with a semicolon. + /// + /// The to generate code for. + /// The that's used to write code. + /// A instance that contains information about + /// the current code generation process. + /// that renders the raw value of the HTML attribute. + public void RenderAttributeValue([NotNull] TagHelperAttributeDescriptor attributeDescriptor, + [NotNull] CSharpCodeWriter writer, + [NotNull] CodeBuilderContext context, + [NotNull] Action renderAttributeValue) + { + renderAttributeValue(writer); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptor.cs b/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptor.cs index 01a25ac9b8..c3631e16fc 100644 --- a/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptor.cs +++ b/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptor.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using Microsoft.Internal.Web.Utils; namespace Microsoft.AspNet.Razor.TagHelpers @@ -20,14 +21,35 @@ namespace Microsoft.AspNet.Razor.TagHelpers /// The full name of the tag helper class. /// The /// of the tag helper. - public TagHelperDescriptor(string tagName, - string tagHelperName, + public TagHelperDescriptor([NotNull] string tagName, + [NotNull] string tagHelperName, ContentBehavior contentBehavior) + : this(tagName, tagHelperName, contentBehavior, Enumerable.Empty()) + { + } + + /// + /// Instantiates a new instance of the class with the given + /// . + /// + /// The tag name that the tag helper targets. '*' indicates a catch-all + /// which applies to every HTML tag. + /// The code class used to render the tag helper. Corresponds to + /// the tag helper's . + /// The + /// of the tag helper. + /// + /// The s to request from the HTML tag. + /// + public TagHelperDescriptor([NotNull] string tagName, + [NotNull] string tagHelperName, + ContentBehavior contentBehavior, + [NotNull] IEnumerable attributes) { TagName = tagName; TagHelperName = tagHelperName; ContentBehavior = contentBehavior; - Attributes = new List(); + Attributes = new List(attributes); } /// diff --git a/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptorComparer.cs b/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptorComparer.cs new file mode 100644 index 0000000000..06b1fce0e5 --- /dev/null +++ b/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptorComparer.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.Internal.Web.Utils; + +namespace Microsoft.AspNet.Razor.TagHelpers +{ + /// + /// Defines a an that is used to check equality between + /// two s. + /// + public class TagHelperDescriptorComparer : IEqualityComparer + { + /// + /// A default instance of the . + /// + public static readonly TagHelperDescriptorComparer Default = new TagHelperDescriptorComparer(); + + private TagHelperDescriptorComparer() + { + } + + /// + /// Determines if the two given tag helpers are equal. + /// + /// A to compare with the given + /// . + /// A to compare with the given + /// . + /// true if and are equal, + /// false otherwise. + /// + /// Determines equality based on , + /// and . + /// + public bool Equals(TagHelperDescriptor descriptorX, TagHelperDescriptor descriptorY) + { + return string.Equals(descriptorX.TagHelperName, descriptorY.TagHelperName, StringComparison.Ordinal) && + string.Equals(descriptorX.TagName, descriptorY.TagName, StringComparison.OrdinalIgnoreCase) && + descriptorX.ContentBehavior == descriptorY.ContentBehavior; + } + + /// + /// Returns an value that uniquely identifies the given . + /// + /// The to create a hash code for. + /// An that uniquely identifies the given . + public int GetHashCode(TagHelperDescriptor descriptor) + { + return HashCodeCombiner.Start() + .Add(descriptor.TagName, StringComparer.OrdinalIgnoreCase) + .Add(descriptor.TagHelperName, StringComparer.Ordinal) + .Add(descriptor.ContentBehavior) + .CombinedHash; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptorProvider.cs b/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptorProvider.cs index 15a083b3a7..9ba5e0e949 100644 --- a/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptorProvider.cs +++ b/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptorProvider.cs @@ -15,9 +15,6 @@ namespace Microsoft.AspNet.Razor.TagHelpers { private const string CatchAllDescriptorTarget = "*"; - private static readonly TagHelperDescriptorComparer DefaultTagHelperDescriptorComparer = - new TagHelperDescriptorComparer(); - private IDictionary> _registrations; /// @@ -79,30 +76,11 @@ namespace Microsoft.AspNet.Razor.TagHelpers // Ensure there's a List to add the descriptor to. if (!_registrations.TryGetValue(descriptor.TagName, out descriptorSet)) { - descriptorSet = new HashSet(DefaultTagHelperDescriptorComparer); + descriptorSet = new HashSet(TagHelperDescriptorComparer.Default); _registrations[descriptor.TagName] = descriptorSet; } descriptorSet.Add(descriptor); } - - private class TagHelperDescriptorComparer : IEqualityComparer - { - public bool Equals(TagHelperDescriptor descriptorX, TagHelperDescriptor descriptorY) - { - return descriptorX.TagHelperName == descriptorY.TagHelperName && - descriptorX.TagName == descriptorY.TagName && - descriptorX.ContentBehavior == descriptorY.ContentBehavior; - } - - public int GetHashCode(TagHelperDescriptor descriptor) - { - return HashCodeCombiner.Start() - .Add(descriptor.TagName) - .Add(descriptor.TagHelperName) - .Add(descriptor.ContentBehavior) - .CombinedHash; - } - } } } \ No newline at end of file