diff --git a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/IReadOnlyTagHelperAttribute.cs b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/IReadOnlyTagHelperAttribute.cs index 0114f895f..cdf90a0db 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/IReadOnlyTagHelperAttribute.cs +++ b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/IReadOnlyTagHelperAttribute.cs @@ -19,5 +19,11 @@ public interface IReadOnlyTagHelperAttribute : IEquatable object Value { get; } + + /// + /// Gets an indication whether the attribute is minimized or not. + /// + /// If true, will be ignored. + bool Minimized { get; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperAttribute.cs b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperAttribute.cs index a2f1e1613..96954d934 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperAttribute.cs +++ b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperAttribute.cs @@ -41,6 +41,12 @@ public TagHelperAttribute(string name, object value) /// public object Value { get; set; } + /// + /// Gets or sets an indication whether the attribute is minimized or not. + /// + /// If true, will be ignored. + public bool Minimized { get; set; } + /// /// Converts the specified into a . /// @@ -61,7 +67,8 @@ public bool Equals(IReadOnlyTagHelperAttribute other) return other != null && string.Equals(Name, other.Name, StringComparison.OrdinalIgnoreCase) && - Equals(Value, other.Value); + Minimized == other.Minimized && + (Minimized || Equals(Value, other.Value)); } /// diff --git a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperExecutionContext.cs b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperExecutionContext.cs index d72e854f4..2fc571a4a 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperExecutionContext.cs +++ b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperExecutionContext.cs @@ -39,7 +39,7 @@ internal TagHelperExecutionContext(string tagName, bool selfClosing) /// The HTML tag name in the Razor source. /// /// indicating whether or not the tag in the Razor source was self-closing. - /// The collection of items used to communicate with other + /// The collection of items used to communicate with other /// s /// An identifier unique to the HTML element this context is for. /// A delegate used to execute the child content asynchronously. @@ -133,6 +133,26 @@ public void Add([NotNull] ITagHelper tagHelper) _tagHelpers.Add(tagHelper); } + /// + /// Tracks the minimized HTML attribute in and . + /// + /// The minimized HTML attribute name. + public void AddMinimizedHtmlAttribute([NotNull] string name) + { + HTMLAttributes.Add( + new TagHelperAttribute + { + Name = name, + Minimized = true + }); + AllAttributes.Add( + new TagHelperAttribute + { + Name = name, + Minimized = true + }); + } + /// /// Tracks the HTML attribute in and . /// @@ -168,7 +188,7 @@ public Task ExecuteChildContentAsync() /// /// A that on completion returns the rendered child content. /// - /// Child content is only executed once. Successive calls to this method or successive executions of the + /// Child content is only executed once. Successive calls to this method or successive executions of the /// returned return a cached result. /// public async Task GetChildContentAsync() diff --git a/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/CSharpTagHelperCodeRenderer.cs b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/CSharpTagHelperCodeRenderer.cs index 0de12eaec..910d80637 100644 --- a/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/CSharpTagHelperCodeRenderer.cs +++ b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/CSharpTagHelperCodeRenderer.cs @@ -203,6 +203,13 @@ private void RenderBoundHTMLAttributes(IList> chunkA var firstAttribute = matchingAttributes.First(); var attributeValueChunk = firstAttribute.Value; + // Minimized attributes are not valid for bound attributes. There will be an error for the bound + // attribute logged by TagHelperBlockRewriter already so we can skip. + if (attributeValueChunk == null) + { + continue; + } + var attributeValueRecorded = htmlAttributeValues.ContainsKey(attributeDescriptor.Name); // Bufferable attributes are attributes that can have Razor code inside of them. @@ -323,15 +330,21 @@ private void RenderUnboundHTMLAttributes(IEnumerable // Build out the unbound HTML attributes for the tag builder foreach (var htmlAttribute in unboundHTMLAttributes) { - string textValue; + string textValue = null; + var isPlainTextValue = false; 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) + // A null attribute value means the HTML attribute is minimized. + if (attributeValue != null) { - BuildBufferedWritingScope(attributeValue, htmlEncodeValues: true); + 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, htmlEncodeValues: true); + } } // Execution contexts are a runtime feature, therefore no need to add anything to them. @@ -340,27 +353,39 @@ private void RenderUnboundHTMLAttributes(IEnumerable continue; } - _writer - .WriteStartInstanceMethodInvocation( - ExecutionContextVariableName, - _tagHelperContext.ExecutionContextAddHtmlAttributeMethodName) - .WriteStringLiteral(htmlAttribute.Key) - .WriteParameterSeparator() - .WriteStartMethodInvocation(_tagHelperContext.MarkAsHtmlEncodedMethodName); - - // If it's a plain text value then we need to surround the value with quotes. - if (isPlainTextValue) + // If we have a minimized attribute there is no value + if (attributeValue == null) { - _writer.WriteStringLiteral(textValue); + _writer + .WriteStartInstanceMethodInvocation( + ExecutionContextVariableName, + _tagHelperContext.ExecutionContextAddMinimizedHtmlAttributeMethodName) + .WriteStringLiteral(htmlAttribute.Key) + .WriteEndMethodInvocation(); } else { - RenderBufferedAttributeValueAccessor(_writer); - } + _writer + .WriteStartInstanceMethodInvocation( + ExecutionContextVariableName, + _tagHelperContext.ExecutionContextAddHtmlAttributeMethodName) + .WriteStringLiteral(htmlAttribute.Key) + .WriteParameterSeparator() + .WriteStartMethodInvocation(_tagHelperContext.MarkAsHtmlEncodedMethodName); + + // 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(endLine: false) - .WriteEndMethodInvocation(); + _writer.WriteEndMethodInvocation(endLine: false) + .WriteEndMethodInvocation(); + } } } diff --git a/src/Microsoft.AspNet.Razor/Generator/GeneratedTagHelperContext.cs b/src/Microsoft.AspNet.Razor/Generator/GeneratedTagHelperContext.cs index 4a566cd79..57eb270b5 100644 --- a/src/Microsoft.AspNet.Razor/Generator/GeneratedTagHelperContext.cs +++ b/src/Microsoft.AspNet.Razor/Generator/GeneratedTagHelperContext.cs @@ -19,6 +19,7 @@ public GeneratedTagHelperContext() ScopeManagerEndMethodName = "End"; ExecutionContextAddMethodName = "Add"; ExecutionContextAddTagHelperAttributeMethodName = "AddTagHelperAttribute"; + ExecutionContextAddMinimizedHtmlAttributeMethodName = "AddMinimizedHtmlAttribute"; ExecutionContextAddHtmlAttributeMethodName = "AddHtmlAttribute"; ExecutionContextOutputPropertyName = "Output"; MarkAsHtmlEncodedMethodName = "Html.Raw"; @@ -57,6 +58,11 @@ public GeneratedTagHelperContext() /// public string ExecutionContextAddTagHelperAttributeMethodName { get; set; } + /// + /// The name of the method used to add minimized HTML attributes. + /// + public string ExecutionContextAddMinimizedHtmlAttributeMethodName { get; set; } + /// /// The name of the method used to add HTML attributes. /// diff --git a/src/Microsoft.AspNet.Razor/Generator/TagHelperCodeGenerator.cs b/src/Microsoft.AspNet.Razor/Generator/TagHelperCodeGenerator.cs index bb924aa39..84818fc09 100644 --- a/src/Microsoft.AspNet.Razor/Generator/TagHelperCodeGenerator.cs +++ b/src/Microsoft.AspNet.Razor/Generator/TagHelperCodeGenerator.cs @@ -57,19 +57,25 @@ public override void GenerateStartBlockCode(Block target, CodeGeneratorContext c foreach (var attribute in tagHelperBlock.Attributes) { - // Populates the code tree with chunks associated with attributes - attribute.Value.Accept(codeGenerator); + ChunkBlock attributeChunkValue = null; - var chunks = codeGenerator.Context.CodeTreeBuilder.CodeTree.Chunks; - var first = chunks.FirstOrDefault(); + if (attribute.Value != null) + { + // Populates the code tree with chunks associated with attributes + attribute.Value.Accept(codeGenerator); - attributes.Add(new KeyValuePair(attribute.Key, - new ChunkBlock + var chunks = codeGenerator.Context.CodeTreeBuilder.CodeTree.Chunks; + var first = chunks.FirstOrDefault(); + + attributeChunkValue = new ChunkBlock { Association = first?.Association, Children = chunks, Start = first == null ? SourceLocation.Zero : first.Start - })); + }; + } + + attributes.Add(new KeyValuePair(attribute.Key, attributeChunkValue)); // Reset the code tree builder so we can build a new one for the next attribute codeGenerator.Context.CodeTreeBuilder = new CodeTreeBuilder(); diff --git a/src/Microsoft.AspNet.Razor/Parser/HtmlMarkupParser.Block.cs b/src/Microsoft.AspNet.Razor/Parser/HtmlMarkupParser.Block.cs index 351db4664..ef2a33094 100644 --- a/src/Microsoft.AspNet.Razor/Parser/HtmlMarkupParser.Block.cs +++ b/src/Microsoft.AspNet.Razor/Parser/HtmlMarkupParser.Block.cs @@ -473,12 +473,25 @@ private void BeforeAttribute() if (!At(HtmlSymbolType.Equals)) { - // Saw a space or newline after the name, so just skip this attribute and continue around the loop - Accept(whitespace); - Accept(name); + // Minimized attribute + + // Output anything prior to the attribute, in most cases this will be the tag name: + // |. If in-between other attributes this will noop or output malformed attribute + // content (if the previous attribute was malformed). + Output(SpanKind.Markup); + + using (Context.StartBlock(BlockType.Markup)) + { + Accept(whitespace); + Accept(name); + Output(SpanKind.Markup); + } + return; } + // Not a minimized attribute, parse as if it were well-formed (if attribute turns out to be malformed we + // will go into recovery). Output(SpanKind.Markup); // Start a new markup block for the attribute diff --git a/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlock.cs b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlock.cs index 48fb4df16..6b3f5f336 100644 --- a/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlock.cs +++ b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlock.cs @@ -38,7 +38,10 @@ public TagHelperBlock(TagHelperBlockBuilder source) foreach (var attributeChildren in Attributes) { - attributeChildren.Value.Parent = this; + if (attributeChildren.Value != null) + { + attributeChildren.Value.Parent = this; + } } } diff --git a/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockRewriter.cs b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockRewriter.cs index b564d7815..001d3d3de 100644 --- a/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockRewriter.cs +++ b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockRewriter.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; @@ -71,11 +71,12 @@ private static IList> GetTagAttributes( // Only want to track the attribute if we succeeded in parsing its corresponding Block/Span. if (succeeded) { - // Check if it's a bound attribute that is not of type string and happens to be null or whitespace. + // Check if it's a bound attribute that is minimized or not of type string and null or whitespace. string attributeValueType; if (attributeValueTypes.TryGetValue(attribute.Key, out attributeValueType) && + (attribute.Value == null || !IsStringAttribute(attributeValueType) && - IsNullOrWhitespaceAttributeValue(attribute.Value)) + IsNullOrWhitespaceAttributeValue(attribute.Value))) { var errorLocation = GetAttributeNameStartLocation(child); @@ -167,7 +168,7 @@ private static bool TryParseSpan( { Debug.Assert( name != null, - "Name should never be null here. The parser should guaruntee an attribute has a name."); + "Name should never be null here. The parser should guarantee an attribute has a name."); // We've captured all leading whitespace and the attribute name. // We're now at: " asp-for|='...'" or " asp-for|=..." @@ -239,7 +240,9 @@ private static bool TryParseSpan( return false; } - attribute = CreateMarkupAttribute(name, builder, attributeValueTypes); + // If we're not after an equal then we should treat the value as if it were a minimized attribute. + var attributeValueBuilder = afterEquals ? builder : null; + attribute = CreateMarkupAttribute(name, attributeValueBuilder, attributeValueTypes); return true; } @@ -432,17 +435,24 @@ private static KeyValuePair CreateMarkupAttribute( IReadOnlyDictionary attributeValueTypes) { string attributeTypeName; + Span value = null; - // If the attribute was requested by the tag helper and doesn't happen to be a string then we need to treat - // its value as code. Any non-string value can be any C# value so we need to ensure the SyntaxTreeNode - // reflects that. - if (attributeValueTypes.TryGetValue(name, out attributeTypeName) && - !IsStringAttribute(attributeTypeName)) + // Builder will be null in the case of minimized attributes + if (builder != null) { - builder.Kind = SpanKind.Code; + // If the attribute was requested by the tag helper and doesn't happen to be a string then we need to treat + // its value as code. Any non-string value can be any C# value so we need to ensure the SyntaxTreeNode + // reflects that. + if (attributeValueTypes.TryGetValue(name, out attributeTypeName) && + !IsStringAttribute(attributeTypeName)) + { + builder.Kind = SpanKind.Code; + } + + value = builder.Build(); } - return new KeyValuePair(name, builder.Build()); + return new KeyValuePair(name, value); } private static bool IsNullOrWhitespaceAttributeValue(SyntaxTreeNode attributeValue) diff --git a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/CaseSensitiveTagHelperAttributeComparer.cs b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/CaseSensitiveTagHelperAttributeComparer.cs index a5ba8e300..0bb90234b 100644 --- a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/CaseSensitiveTagHelperAttributeComparer.cs +++ b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/CaseSensitiveTagHelperAttributeComparer.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using Microsoft.Internal.Web.Utils; namespace Microsoft.AspNet.Razor.Runtime.TagHelpers { @@ -26,16 +25,13 @@ public bool Equals(IReadOnlyTagHelperAttribute attributeX, IReadOnlyTagHelperAtt // Normal comparer (TagHelperAttribute.Equals()) doesn't care about the Name case, in tests we do. return attributeX != null && string.Equals(attributeX.Name, attributeY.Name, StringComparison.Ordinal) && - Equals(attributeX.Value, attributeY.Value); + attributeX.Minimized == attributeY.Minimized && + (attributeX.Minimized || Equals(attributeX.Value, attributeY.Value)); } public int GetHashCode(IReadOnlyTagHelperAttribute attribute) { - return HashCodeCombiner - .Start() - .Add(attribute.Name, StringComparer.Ordinal) - .Add(attribute.Value) - .CombinedHash; + return attribute.GetHashCode(); } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperExecutionContextTest.cs b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperExecutionContextTest.cs index 865c4c894..2d5e012c5 100644 --- a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperExecutionContextTest.cs +++ b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperExecutionContextTest.cs @@ -20,7 +20,7 @@ public void SelfClosing_ReturnsTrueOrFalseAsExpected(bool selfClosing) // Arrange & Act var executionContext = new TagHelperExecutionContext("p", selfClosing); - // Assert + // Assert Assert.Equal(selfClosing, executionContext.SelfClosing); } @@ -202,6 +202,54 @@ public void AddHtmlAttribute_MaintainsHTMLAttributes() CaseSensitiveTagHelperAttributeComparer.Default); } + [Fact] + public void AddMinimizedHtmlAttribute_MaintainsHTMLAttributes() + { + // Arrange + var executionContext = new TagHelperExecutionContext("input", selfClosing: true); + var expectedAttributes = new TagHelperAttributeList + { + ["checked"] = new TagHelperAttribute { Name = "checked", Minimized = true }, + ["visible"] = new TagHelperAttribute { Name = "visible", Minimized = true } + }; + + // Act + executionContext.AddMinimizedHtmlAttribute("checked"); + executionContext.AddMinimizedHtmlAttribute("visible"); + + // Assert + Assert.Equal( + expectedAttributes, + executionContext.HTMLAttributes, + CaseSensitiveTagHelperAttributeComparer.Default); + } + + [Fact] + public void AddMinimizedHtmlAttribute_MaintainsHTMLAttributes_SomeMinimized() + { + // Arrange + var executionContext = new TagHelperExecutionContext("input", selfClosing: true); + var expectedAttributes = new TagHelperAttributeList + { + { "class", "btn" }, + { "foo", "bar" } + }; + expectedAttributes.Add(new TagHelperAttribute { Name = "checked", Minimized = true }); + expectedAttributes.Add(new TagHelperAttribute { Name = "visible", Minimized = true }); + + // Act + executionContext.AddHtmlAttribute("class", "btn"); + executionContext.AddHtmlAttribute("foo", "bar"); + executionContext.AddMinimizedHtmlAttribute("checked"); + executionContext.AddMinimizedHtmlAttribute("visible"); + + // Assert + Assert.Equal( + expectedAttributes, + executionContext.HTMLAttributes, + CaseSensitiveTagHelperAttributeComparer.Default); + } + [Fact] public void TagHelperExecutionContext_MaintainsAllAttributes() { diff --git a/test/Microsoft.AspNet.Razor.Test/Framework/BlockFactory.cs b/test/Microsoft.AspNet.Razor.Test/Framework/BlockFactory.cs index 27dfa2b7f..ff56d4e2d 100644 --- a/test/Microsoft.AspNet.Razor.Test/Framework/BlockFactory.cs +++ b/test/Microsoft.AspNet.Razor.Test/Framework/BlockFactory.cs @@ -1,6 +1,7 @@ // 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.AspNet.Razor.Parser.SyntaxTree; namespace Microsoft.AspNet.Razor.Test.Framework @@ -19,12 +20,28 @@ public Block EscapedMarkupTagBlock(string prefix, string suffix) return EscapedMarkupTagBlock(prefix, suffix, AcceptedCharacters.Any); } - public Block EscapedMarkupTagBlock(string prefix, string suffix, AcceptedCharacters acceptedCharacters) + public Block EscapedMarkupTagBlock(string prefix, string suffix, params SyntaxTreeNode[] children) { - return new MarkupTagBlock( - _factory.Markup(prefix), - _factory.BangEscape(), - _factory.Markup(suffix).Accepts(acceptedCharacters)); + return EscapedMarkupTagBlock(prefix, suffix, AcceptedCharacters.Any, children); + } + + public Block EscapedMarkupTagBlock( + string prefix, + string suffix, + AcceptedCharacters acceptedCharacters, + params SyntaxTreeNode[] children) + { + var newChildren = new List( + new SyntaxTreeNode[] + { + _factory.Markup(prefix), + _factory.BangEscape(), + _factory.Markup(suffix).Accepts(acceptedCharacters) + }); + + newChildren.AddRange(children); + + return new MarkupTagBlock(newChildren.ToArray()); } public Block MarkupTagBlock(string content) diff --git a/test/Microsoft.AspNet.Razor.Test/Framework/ParserTestBase.cs b/test/Microsoft.AspNet.Razor.Test/Framework/ParserTestBase.cs index 439d8643d..8c0f89a32 100644 --- a/test/Microsoft.AspNet.Razor.Test/Framework/ParserTestBase.cs +++ b/test/Microsoft.AspNet.Razor.Test/Framework/ParserTestBase.cs @@ -303,6 +303,7 @@ private static void EvaluateSyntaxTreeNode(ErrorCollector collector, SyntaxTreeN if (actual == null) { AddNullActualError(collector, actual, expected); + return; } if (actual.IsBlock != expected.IsBlock) @@ -335,7 +336,14 @@ private static void EvaluateTagHelperAttribute(ErrorCollector collector, collector.AddMessage("{0} - PASSED :: Attribute names match", expected.Key); } - EvaluateSyntaxTreeNode(collector, actual.Value, expected.Value); + if (actual.Value == null && expected.Value == null) + { + collector.AddMessage("{0} - PASSED :: Minimized attribute values match", expected.Key); + } + else + { + EvaluateSyntaxTreeNode(collector, actual.Value, expected.Value); + } } private static void EvaluateSpan(ErrorCollector collector, Span actual, Span expected) diff --git a/test/Microsoft.AspNet.Razor.Test/Generator/CSharpTagHelperRenderingTest.cs b/test/Microsoft.AspNet.Razor.Test/Generator/CSharpTagHelperRenderingTest.cs index 1c0e33c29..02670e278 100644 --- a/test/Microsoft.AspNet.Razor.Test/Generator/CSharpTagHelperRenderingTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/Generator/CSharpTagHelperRenderingTest.cs @@ -19,6 +19,44 @@ private static IEnumerable DefaultPAndInputTagHelperDescrip private static IEnumerable PrefixedPAndInputTagHelperDescriptors => BuildPAndInputTagHelperDescriptors("THS"); + private static IEnumerable MinimizedTagHelpers_Descriptors + { + get + { + return new[] + { + new TagHelperDescriptor( + tagName: "*", + typeName: "CatchAllTagHelper", + assemblyName: "SomeAssembly", + attributes: new[] + { + new TagHelperAttributeDescriptor( + "catchall-bound-string", + "BoundRequiredString", + typeof(string).FullName), + }, + requiredAttributes: new[] { "catchall-unbound-required" }), + new TagHelperDescriptor( + tagName: "input", + typeName: "InputTagHelper", + assemblyName: "SomeAssembly", + attributes: new[] + { + new TagHelperAttributeDescriptor( + "input-bound-required-string", + "BoundRequiredString", + typeof(string).FullName), + new TagHelperAttributeDescriptor( + "input-bound-string", + "BoundString", + typeof(string).FullName) + }, + requiredAttributes: new[] { "input-bound-required-string", "input-unbound-required" }), + }; + } + } + private static IEnumerable DuplicateTargetTagHelperDescriptors { get @@ -207,6 +245,20 @@ public static TheoryData TagHelperDescriptorFlowTestData AttributeTargetingTagHelperDescriptors, AttributeTargetingTagHelperDescriptors, true + }, + { + "MinimizedTagHelpers", + "MinimizedTagHelpers", + MinimizedTagHelpers_Descriptors, + MinimizedTagHelpers_Descriptors, + false + }, + { + "MinimizedTagHelpers", + "MinimizedTagHelpers.DesignTime", + MinimizedTagHelpers_Descriptors, + MinimizedTagHelpers_Descriptors, + true } }; } diff --git a/test/Microsoft.AspNet.Razor.Test/Parser/Html/HtmlAttributeTest.cs b/test/Microsoft.AspNet.Razor.Test/Parser/Html/HtmlAttributeTest.cs index 5f139cc6e..26f44600a 100644 --- a/test/Microsoft.AspNet.Razor.Test/Parser/Html/HtmlAttributeTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/Parser/Html/HtmlAttributeTest.cs @@ -70,7 +70,9 @@ public void UnquotedLiteralAttribute() new MarkupBlock(new AttributeBlockCodeGenerator(name: "href", prefix: new LocationTagged(" href=", 2, 0, 2), suffix: new LocationTagged(string.Empty, 11, 0, 11)), Factory.Markup(" href=").With(SpanCodeGenerator.Null), Factory.Markup("Foo").With(new LiteralAttributeCodeGenerator(prefix: new LocationTagged(string.Empty, 8, 0, 8), value: new LocationTagged("Foo", 8, 0, 8)))), - Factory.Markup(" Bar Baz />").Accepts(AcceptedCharacters.None)))); + new MarkupBlock(Factory.Markup(" Bar")), + new MarkupBlock(Factory.Markup(" Baz")), + Factory.Markup(" />").Accepts(AcceptedCharacters.None)))); } [Fact] diff --git a/test/Microsoft.AspNet.Razor.Test/Parser/Html/HtmlDocumentTest.cs b/test/Microsoft.AspNet.Razor.Test/Parser/Html/HtmlDocumentTest.cs index ed797f121..81776a164 100644 --- a/test/Microsoft.AspNet.Razor.Test/Parser/Html/HtmlDocumentTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/Parser/Html/HtmlDocumentTest.cs @@ -72,7 +72,11 @@ public void ParseDocumentCorrectlyHandlesOddlySpacedHTMLElements() ParseDocumentTest("

Foo

", new MarkupBlock( BlockFactory.MarkupTagBlock("
"), - BlockFactory.MarkupTagBlock("

"), + new MarkupTagBlock( + Factory.Markup("")), Factory.Markup(" Foo "), BlockFactory.MarkupTagBlock("

"), BlockFactory.MarkupTagBlock("
"))); diff --git a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperBlockRewriterTest.cs b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperBlockRewriterTest.cs new file mode 100644 index 000000000..7c1886f20 --- /dev/null +++ b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperBlockRewriterTest.cs @@ -0,0 +1,766 @@ +// 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.AspNet.Razor.Parser; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Test.Framework; +using Microsoft.AspNet.Razor.Test.TagHelpers; +using Xunit; + +namespace Microsoft.AspNet.Razor.TagHelpers +{ + public class TagHelperBlockRewriterTest : TagHelperRewritingTestBase + { + public static TheoryData MinimizedAttributeData_Document + { + get + { + var factory = CreateDefaultSpanFactory(); + var noErrors = new RazorError[0]; + var errorFormat = "Attribute '{0}' on tag helper element '{1}' requires a value. Tag helper bound " + + "attributes of type '{2}' cannot be empty or contain only whitespace."; + var stringType = typeof(string).FullName; + var intType = typeof(int).FullName; + var expressionString = "@DateTime.Now + 1"; + var expression = new MarkupBlock( + new MarkupBlock( + new ExpressionBlock( + factory.CodeTransition(), + factory.Code("DateTime.Now") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace))), + factory.Markup(" +"), + factory.Markup(" 1")); + + // documentContent, expectedOutput, expectedErrors + return new TheoryData + { + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "input", + selfClosing: true, + attributes: new List>() + { + new KeyValuePair("unbound-required", null), + })), + noErrors + }, + { + "

", + new MarkupBlock( + new MarkupTagHelperBlock( + "p", + selfClosing: false, + attributes: new List>() + { + new KeyValuePair("bound-string", null), + })), + new[] + { + new RazorError( + string.Format(errorFormat, "bound-string", "p", stringType), 3, 0, 3, 12) + } + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "input", + selfClosing: true, + attributes: new List>() + { + new KeyValuePair("bound-required-string", null), + })), + new[] + { + new RazorError( + string.Format(errorFormat, "bound-required-string", "input", stringType), 7, 0, 7, 21) + } + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "input", + selfClosing: true, + attributes: new List>() + { + new KeyValuePair("bound-required-int", null), + })), + new[] + { + new RazorError( + string.Format(errorFormat, "bound-required-int", "input", intType), 7, 0, 7, 18) + } + }, + { + "

", + new MarkupBlock( + new MarkupTagHelperBlock( + "p", + selfClosing: false, + attributes: new List>() + { + new KeyValuePair("bound-int", null), + })), + new[] { new RazorError(string.Format(errorFormat, "bound-int", "p", intType), 3, 0, 3, 9) } + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "input", + selfClosing: true, + attributes: new List>() + { + new KeyValuePair("unbound-required", null), + new KeyValuePair("bound-required-string", null), + })), + new[] + { + new RazorError( + string.Format(errorFormat, "bound-required-string", "input", stringType), + absoluteIndex: 24, + lineIndex: 0, + columnIndex: 24, + length: 21) + } + }, + { + "

", + new MarkupBlock( + new MarkupTagHelperBlock( + "p", + selfClosing: false, + attributes: new List>() + { + new KeyValuePair("bound-int", null), + new KeyValuePair("bound-string", null), + })), + new[] + { + new RazorError(string.Format(errorFormat, "bound-int", "p", intType), 3, 0, 3, 9), + new RazorError(string.Format(errorFormat, "bound-string", "p", stringType), 13, 0, 13, 12), + } + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "input", + selfClosing: true, + attributes: new List>() + { + new KeyValuePair("bound-required-int", null), + new KeyValuePair("unbound-required", null), + new KeyValuePair("bound-required-string", null), + })), + new[] + { + new RazorError( + string.Format(errorFormat, "bound-required-int", "input", intType), 7, 0, 7, 18), + new RazorError( + string.Format(errorFormat, "bound-required-string", "input", stringType), + absoluteIndex: 43, + lineIndex: 0, + columnIndex: 43, + length: 21) + } + }, + { + "

", + new MarkupBlock( + new MarkupTagHelperBlock( + "p", + selfClosing: false, + attributes: new List>() + { + new KeyValuePair("bound-int", null), + new KeyValuePair("bound-string", null), + new KeyValuePair("bound-string", null), + })), + new[] + { + new RazorError(string.Format(errorFormat, "bound-int", "p", intType), 3, 0, 3, 9), + new RazorError(string.Format(errorFormat, "bound-string", "p", stringType), 13, 0, 13, 12), + new RazorError(string.Format(errorFormat, "bound-string", "p", stringType), 26, 0, 26, 12), + } + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "input", + selfClosing: true, + attributes: new List>() + { + new KeyValuePair("unbound-required", null), + new KeyValuePair("class", factory.Markup("btn")), + })), + noErrors + }, + { + "

", + new MarkupBlock( + new MarkupTagHelperBlock( + "p", + selfClosing: false, + attributes: new List>() + { + new KeyValuePair("bound-string", null), + new KeyValuePair("class", factory.Markup("btn")), + })), + new[] + { + new RazorError( + string.Format(errorFormat, "bound-string", "p", stringType), + absoluteIndex: 3, + lineIndex: 0, + columnIndex: 3, + length: 12) + } + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "input", + selfClosing: true, + attributes: new List>() + { + new KeyValuePair("class", factory.Markup("btn")), + new KeyValuePair("unbound-required", null), + })), + noErrors + }, + { + "

", + new MarkupBlock( + new MarkupTagHelperBlock( + "p", + selfClosing: false, + attributes: new List>() + { + new KeyValuePair("class", factory.Markup("btn")), + new KeyValuePair("bound-string", null), + })), + new[] + { + new RazorError( + string.Format(errorFormat, "bound-string", "p", stringType), + absoluteIndex: 15, + lineIndex: 0, + columnIndex: 15, + length: 12) + } + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "input", + selfClosing: true, + attributes: new List>() + { + new KeyValuePair("bound-required-string", null), + new KeyValuePair("class", factory.Markup("btn")), + })), + new[] + { + new RazorError( + string.Format(errorFormat, "bound-required-string", "input", stringType), + absoluteIndex: 7, + lineIndex: 0, + columnIndex: 7, + length: 21) + } + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "input", + selfClosing: true, + attributes: new List>() + { + new KeyValuePair("class", factory.Markup("btn")), + new KeyValuePair("bound-required-string", null), + })), + new[] + { + new RazorError( + string.Format(errorFormat, "bound-required-string", "input", stringType), + absoluteIndex: 19, + lineIndex: 0, + columnIndex: 19, + length: 21) + } + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "input", + selfClosing: true, + attributes: new List>() + { + new KeyValuePair("bound-required-int", null), + new KeyValuePair("class", factory.Markup("btn")), + })), + new[] + { + new RazorError( + string.Format(errorFormat, "bound-required-int", "input", intType), 7, 0, 7, 18) + } + }, + { + "

", + new MarkupBlock( + new MarkupTagHelperBlock( + "p", + selfClosing: false, + attributes: new List>() + { + new KeyValuePair("bound-int", null), + new KeyValuePair("class", factory.Markup("btn")), + })), + new[] + { + new RazorError(string.Format(errorFormat, "bound-int", "p", intType), 3, 0, 3, 9) + } + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "input", + selfClosing: true, + attributes: new List>() + { + new KeyValuePair("class", factory.Markup("btn")), + new KeyValuePair("bound-required-int", null), + })), + new[] + { + new RazorError(string.Format(errorFormat, "bound-required-int", "input", intType), 19, 0, 19, 18) + } + }, + { + "

", + new MarkupBlock( + new MarkupTagHelperBlock( + "p", + selfClosing: false, + attributes: new List>() + { + new KeyValuePair("class", factory.Markup("btn")), + new KeyValuePair("bound-int", null), + })), + new[] + { + new RazorError(string.Format(errorFormat, "bound-int", "p", intType), 15, 0, 15, 9) + } + }, + { + $"", + new MarkupBlock( + new MarkupTagHelperBlock( + "input", + selfClosing: true, + attributes: new List>() + { + new KeyValuePair("class", expression), + new KeyValuePair("bound-required-int", null), + })), + new[] + { + new RazorError( + string.Format(errorFormat, "bound-required-int", "input", intType), 33, 0, 33, 18) + } + }, + { + $"

", + new MarkupBlock( + new MarkupTagHelperBlock( + "p", + selfClosing: false, + attributes: new List>() + { + new KeyValuePair("class", expression), + new KeyValuePair("bound-int", null), + })), + new[] + { + new RazorError(string.Format(errorFormat, "bound-int", "p", intType), 29, 0, 29, 9) + } + }, + { + $"", + new MarkupBlock( + new MarkupTagHelperBlock( + "input", + selfClosing: true, + attributes: new List>() + { + new KeyValuePair("bound-required-int", null), + new KeyValuePair("class", expression), + new KeyValuePair("bound-required-string", null), + new KeyValuePair("class", expression), + new KeyValuePair("unbound-required", null), + })), + new[] + { + new RazorError( + string.Format(errorFormat, "bound-required-int", "input", intType), 10, 0, 10, 18), + new RazorError( + string.Format(errorFormat, "bound-required-string", "input", stringType), + absoluteIndex: 57, + lineIndex: 0, + columnIndex: 57, + length: 21), + } + }, + { + $"

", + new MarkupBlock( + new MarkupTagHelperBlock( + "p", + selfClosing: false, + attributes: new List>() + { + new KeyValuePair("bound-int", null), + new KeyValuePair("class", expression), + new KeyValuePair("bound-string", null), + new KeyValuePair("class", expression), + new KeyValuePair("bound-string", null), + })), + new[] + { + new RazorError(string.Format(errorFormat, "bound-int", "p", intType), 6, 0, 6, 9), + new RazorError(string.Format(errorFormat, "bound-string", "p", stringType), 44, 0, 44, 12), + new RazorError(string.Format(errorFormat, "bound-string", "p", stringType), 84, 0, 84, 12), + } + }, + }; + } + } + + public static TheoryData MinimizedAttributeData_CSharpBlock + { + get + { + var factory = CreateDefaultSpanFactory(); + var documentData = MinimizedAttributeData_Document; + Func, MarkupBlock> buildStatementBlock = (insideBuilder) => + { + return new MarkupBlock( + factory.EmptyHtml(), + new StatementBlock( + factory.CodeTransition(), + factory.MetaCode("{").Accepts(AcceptedCharacters.None), + insideBuilder(), + factory.EmptyCSharp().AsStatement(), + factory.MetaCode("}").Accepts(AcceptedCharacters.None)), + factory.EmptyHtml()); + }; + + foreach (var data in documentData) + { + data[0] = $"@{{{data[0]}}}"; + data[1] = buildStatementBlock(() => data[1] as MarkupBlock); + + var errors = data[2] as RazorError[]; + + for (var i = 0; i < errors.Length; i++) + { + var error = errors[i]; + error.Location = SourceLocation.Advance(error.Location, "@{"); + } + } + + return documentData; + } + } + + public static TheoryData MinimizedAttributeData_PartialTags + { + get + { + var factory = CreateDefaultSpanFactory(); + var noErrors = new RazorError[0]; + var errorFormatUnclosed = "Found a malformed '{0}' tag helper. Tag helpers must have a start and " + + "end tag or be self closing."; + var errorFormatNoCloseAngle = "Missing close angle for tag helper '{0}'."; + var errorFormatNoValue = "Attribute '{0}' on tag helper element '{1}' requires a value. Tag helper bound " + + "attributes of type '{2}' cannot be empty or contain only whitespace."; + var stringType = typeof(string).FullName; + var intType = typeof(int).FullName; + + // documentContent, expectedOutput, expectedErrors + return new TheoryData + { + { + ">() + { + new KeyValuePair("unbound-required", null), + })), + new[] + { + new RazorError(string.Format(errorFormatNoCloseAngle, "input"), SourceLocation.Zero), + new RazorError(string.Format(errorFormatUnclosed, "input"), SourceLocation.Zero), + } + }, + { + ">() + { + new KeyValuePair("bound-required-string", null), + })), + new[] + { + new RazorError(string.Format(errorFormatNoCloseAngle, "input"), SourceLocation.Zero), + new RazorError(string.Format(errorFormatUnclosed, "input"), SourceLocation.Zero), + new RazorError( + string.Format(errorFormatNoValue, "bound-required-string", "input", stringType), + absoluteIndex: 7, + lineIndex: 0, + columnIndex: 7, + length: 21), + } + }, + { + ">() + { + new KeyValuePair("bound-required-int", null), + })), + new[] + { + new RazorError(string.Format(errorFormatNoCloseAngle, "input"), SourceLocation.Zero), + new RazorError(string.Format(errorFormatUnclosed, "input"), SourceLocation.Zero), + new RazorError( + string.Format(errorFormatNoValue, "bound-required-int", "input", intType), + absoluteIndex: 7, + lineIndex: 0, + columnIndex: 7, + length: 18), + } + }, + { + ">() + { + new KeyValuePair("bound-required-int", null), + new KeyValuePair("unbound-required", null), + new KeyValuePair("bound-required-string", null), + })), + new[] + { + new RazorError(string.Format(errorFormatNoCloseAngle, "input"), SourceLocation.Zero), + new RazorError(string.Format(errorFormatUnclosed, "input"), SourceLocation.Zero), + new RazorError( + string.Format(errorFormatNoValue, "bound-required-int", "input", intType), + absoluteIndex: 7, + lineIndex: 0, + columnIndex: 7, + length: 18), + new RazorError( + string.Format(errorFormatNoValue, "bound-required-string", "input", stringType), + absoluteIndex: 43, + lineIndex: 0, + columnIndex: 43, + length: 21), + } + }, + { + "

>() + { + new KeyValuePair("bound-string", null), + })), + new[] + { + new RazorError(string.Format(errorFormatNoCloseAngle, "p"), SourceLocation.Zero), + new RazorError(string.Format(errorFormatUnclosed, "p"), SourceLocation.Zero), + new RazorError( + string.Format(errorFormatNoValue, "bound-string", "p", stringType), 3, 0, 3, 12), + } + }, + { + "

>() + { + new KeyValuePair("bound-int", null), + })), + new[] + { + new RazorError(string.Format(errorFormatNoCloseAngle, "p"), SourceLocation.Zero), + new RazorError(string.Format(errorFormatUnclosed, "p"), SourceLocation.Zero), + new RazorError(string.Format(errorFormatNoValue, "bound-int", "p", intType), 3, 0, 3, 9), + } + }, + { + "

>() + { + new KeyValuePair("bound-int", null), + new KeyValuePair("bound-string", null), + })), + new[] + { + new RazorError(string.Format(errorFormatNoCloseAngle, "p"), SourceLocation.Zero), + new RazorError(string.Format(errorFormatUnclosed, "p"), SourceLocation.Zero), + new RazorError(string.Format(errorFormatNoValue, "bound-int", "p", intType), 3, 0, 3, 9), + new RazorError( + string.Format(errorFormatNoValue, "bound-string", "p", stringType), 13, 0, 13, 12), + } + }, + { + ">() + { + new KeyValuePair("bound-required-int", null), + new KeyValuePair("unbound-required", null), + new KeyValuePair("bound-required-string", null), + }, + children: new MarkupTagHelperBlock( + "p", + selfClosing: false, + attributes: new List>() + { + new KeyValuePair("bound-int", null), + new KeyValuePair("bound-string", null), + }))), + new[] + { + new RazorError(string.Format(errorFormatNoCloseAngle, "input"), SourceLocation.Zero), + new RazorError(string.Format(errorFormatUnclosed, "input"), SourceLocation.Zero), + new RazorError( + string.Format(errorFormatNoValue, "bound-required-int", "input", intType), 7, 0, 7, 18), + new RazorError( + string.Format(errorFormatNoValue, "bound-required-string", "input", stringType), + absoluteIndex: 43, + lineIndex: 0, + columnIndex: 43, + length: 21), + new RazorError(string.Format(errorFormatNoCloseAngle, "p"), 64, 0, 64), + new RazorError(string.Format(errorFormatUnclosed, "p"), 64, 0, 64), + new RazorError(string.Format(errorFormatNoValue, "bound-int", "p", intType), 67, 0, 67, 9), + new RazorError( + string.Format(errorFormatNoValue, "bound-string", "p", stringType), + absoluteIndex: 77, + lineIndex: 0, + columnIndex: 77, + length: 12), + } + }, + }; + } + } + + [Theory] + [MemberData(nameof(MinimizedAttributeData_Document))] + [MemberData(nameof(MinimizedAttributeData_CSharpBlock))] + [MemberData(nameof(MinimizedAttributeData_PartialTags))] + public void Rewrite_UnderstandsMinimizedAttributes( + string documentContent, + MarkupBlock expectedOutput, + RazorError[] expectedErrors) + { + // Arrange + var descriptors = new TagHelperDescriptor[] + { + new TagHelperDescriptor( + tagName: "input", + typeName: "InputTagHelper", + assemblyName: "SomeAssembly", + attributes: new TagHelperAttributeDescriptor[0], + requiredAttributes: new[] { "unbound-required" }), + new TagHelperDescriptor( + tagName: "input", + typeName: "InputTagHelper", + assemblyName: "SomeAssembly", + attributes: new[] + { + new TagHelperAttributeDescriptor( + "bound-required-string", + "BoundRequiredString", + typeof(string).FullName) + }, + requiredAttributes: new[] { "bound-required-string" }), + new TagHelperDescriptor( + tagName: "input", + typeName: "InputTagHelper", + assemblyName: "SomeAssembly", + attributes: new[] + { + new TagHelperAttributeDescriptor( + "bound-required-int", + "BoundRequiredInt", + typeof(int).FullName) + }, + requiredAttributes: new[] { "bound-required-int" }), + new TagHelperDescriptor( + tagName: "p", + typeName: "PTagHelper", + assemblyName: "SomeAssembly", + attributes: new[] + { + new TagHelperAttributeDescriptor( + "bound-string", + "BoundRequiredString", + typeof(string).FullName), + new TagHelperAttributeDescriptor( + "bound-int", + "BoundRequiredString", + typeof(int).FullName) + }, + requiredAttributes: Enumerable.Empty()), + }; + var descriptorProvider = new TagHelperDescriptorProvider(descriptors); + + // Act & Assert + EvaluateData(descriptorProvider, documentContent, expectedOutput, expectedErrors); + } + } +} diff --git a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs index adafe042d..f67f62959 100644 --- a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs @@ -8,16 +8,14 @@ using Microsoft.AspNet.Razor.Generator; using Microsoft.AspNet.Razor.Parser; using Microsoft.AspNet.Razor.Parser.SyntaxTree; -using Microsoft.AspNet.Razor.Parser.TagHelpers.Internal; using Microsoft.AspNet.Razor.TagHelpers; using Microsoft.AspNet.Razor.Test.Framework; using Microsoft.AspNet.Razor.Text; -using Microsoft.AspNet.Razor.Tokenizer; using Xunit; namespace Microsoft.AspNet.Razor.Test.TagHelpers { - public class TagHelperParseTreeRewriterTest : CsHtmlMarkupParserTestBase + public class TagHelperParseTreeRewriterTest : TagHelperRewritingTestBase { public static TheoryData RequiredAttributeData { @@ -1949,7 +1947,11 @@ public static TheoryData OptOut_WithPartialTextTagData { "@{ new MarkupBlock(blockFactory.EscapedMarkupTagBlock("<", "text /}"))), + () => new MarkupBlock( + blockFactory.EscapedMarkupTagBlock( + "<", + "text /", + new MarkupBlock(factory.Markup("}"))))), new [] { new RazorError( @@ -2036,7 +2038,7 @@ public static TheoryData OptOut_WithPartialTextTagData prefix: new LocationTagged(string.Empty, 16, 0, 16), value: new LocationTagged("btn", 16, 0, 16))), factory.Markup("\"").With(SpanCodeGenerator.Null)), - factory.Markup("}")))), + new MarkupBlock(factory.Markup("}"))))), new [] { new RazorError( @@ -2067,7 +2069,8 @@ public static TheoryData OptOut_WithPartialTextTagData prefix: new LocationTagged(string.Empty, 16, 0, 16), value: new LocationTagged("btn", 16, 0, 16))), factory.Markup("\"").With(SpanCodeGenerator.Null)), - factory.Markup(" /}")))), + factory.Markup(" /"), + new MarkupBlock(factory.Markup("}"))))), new [] { new RazorError( @@ -2152,7 +2155,8 @@ public static TheoryData OptOut_WithPartialData_CSharp { "@{ new MarkupBlock(blockFactory.EscapedMarkupTagBlock("<", "p /}"))), + () => new MarkupBlock( + blockFactory.EscapedMarkupTagBlock("<", "p /", new MarkupBlock(factory.Markup("}"))))), new [] { new RazorError( @@ -2274,7 +2278,7 @@ public static TheoryData OptOut_WithPartialData_CSharp prefix: new LocationTagged(string.Empty, 13, 0, 13), value: new LocationTagged("btn", 13, 0, 13))), factory.Markup("\"").With(SpanCodeGenerator.Null)), - factory.Markup("}")))), + new MarkupBlock(factory.Markup("}"))))), new [] { new RazorError( @@ -2305,7 +2309,9 @@ public static TheoryData OptOut_WithPartialData_CSharp prefix: new LocationTagged(string.Empty, 13, 0, 13), value: new LocationTagged("btn", 13, 0, 13))), factory.Markup("\"").With(SpanCodeGenerator.Null)), - factory.Markup(" /}")))), + factory.Markup(" /"), + new MarkupBlock( + factory.Markup("}"))))), new [] { new RazorError( @@ -3353,6 +3359,11 @@ public static TheoryData MalformedTagHelperAt "

", new MarkupBlock( new MarkupTagHelperBlock("p", + new List> + { + new KeyValuePair("foo", null), + new KeyValuePair("bar", null) + }, new MarkupTagHelperBlock("strong"))), new [] { @@ -3406,7 +3417,7 @@ public static TheoryData MalformedTagHelperAt new KeyValuePair( "class", new MarkupBlock(factory.Markup("btn"), factory.Markup(" bar="))), - new KeyValuePair("foo", factory.Markup(string.Empty)) + new KeyValuePair("foo", null) }, new MarkupTagHelperBlock("strong"))), new [] @@ -3429,6 +3440,7 @@ public static TheoryData MalformedTagHelperAt new List> { new KeyValuePair("class", new MarkupBlock(factory.Markup("btn"), factory.Markup(" bar="))), + new KeyValuePair("foo", null), })), new RazorError[0] }, @@ -4013,7 +4025,11 @@ public static TheoryData InvalidHtmlBlockData { "< p />", new MarkupBlock( - blockFactory.MarkupTagBlock("< p />")) + new MarkupTagBlock( + factory.Markup("<"), + new MarkupBlock( + factory.Markup(" p")), + factory.Markup(" />"))) }, { "", @@ -5076,74 +5092,5 @@ public void TagHelperParseTreeRewriter_RewritesNestedTagHelperTagBlocks( { RunParseTreeRewriterTest(documentContent, expectedOutput, "p", "div"); } - - private void RunParseTreeRewriterTest(string documentContent, - MarkupBlock expectedOutput, - params string[] tagNames) - { - RunParseTreeRewriterTest(documentContent, - expectedOutput, - errors: Enumerable.Empty(), - tagNames: tagNames); - } - - private void RunParseTreeRewriterTest(string documentContent, - MarkupBlock expectedOutput, - IEnumerable errors, - params string[] tagNames) - { - // Arrange - var providerContext = BuildProviderContext(tagNames); - - // Act & Assert - EvaluateData(providerContext, documentContent, expectedOutput, errors); - } - - private TagHelperDescriptorProvider BuildProviderContext(params string[] tagNames) - { - var descriptors = new List(); - - foreach (var tagName in tagNames) - { - descriptors.Add( - new TagHelperDescriptor(tagName, tagName + "taghelper", "SomeAssembly")); - } - - return new TagHelperDescriptorProvider(descriptors); - } - - public override ParserContext CreateParserContext(ITextDocument input, - ParserBase codeParser, - ParserBase markupParser, - ErrorSink errorSink) - { - return base.CreateParserContext(input, codeParser, markupParser, errorSink); - } - - private void EvaluateData(TagHelperDescriptorProvider provider, - string documentContent, - MarkupBlock expectedOutput, - IEnumerable expectedErrors) - { - var errorSink = new ErrorSink(); - var results = ParseDocument(documentContent, errorSink); - var rewritingContext = new RewritingContext(results.Document, errorSink); - new TagHelperParseTreeRewriter(provider).Rewrite(rewritingContext); - var rewritten = rewritingContext.SyntaxTree; - var actualErrors = errorSink.Errors.OrderBy(error => error.Location.AbsoluteIndex) - .ToList(); - - EvaluateRazorErrors(actualErrors, expectedErrors.ToList()); - EvaluateParseTree(rewritten, expectedOutput); - } - - private static SpanFactory CreateDefaultSpanFactory() - { - return new SpanFactory - { - MarkupTokenizerFactory = doc => new HtmlTokenizer(doc), - CodeTokenizerFactory = doc => new CSharpTokenizer(doc) - }; - } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperRewritingTestBase.cs b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperRewritingTestBase.cs new file mode 100644 index 000000000..f0a4db8fc --- /dev/null +++ b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperRewritingTestBase.cs @@ -0,0 +1,89 @@ +// 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.AspNet.Razor.Parser; +using Microsoft.AspNet.Razor.Parser.TagHelpers.Internal; +using Microsoft.AspNet.Razor.TagHelpers; +using Microsoft.AspNet.Razor.Test.Framework; +using Microsoft.AspNet.Razor.Text; +using Microsoft.AspNet.Razor.Tokenizer; + +namespace Microsoft.AspNet.Razor.Test.TagHelpers +{ + public class TagHelperRewritingTestBase : CsHtmlMarkupParserTestBase + { + public void RunParseTreeRewriterTest( + string documentContent, + MarkupBlock expectedOutput, + params string[] tagNames) + { + RunParseTreeRewriterTest( + documentContent, + expectedOutput, + errors: Enumerable.Empty(), + tagNames: tagNames); + } + + public void RunParseTreeRewriterTest( + string documentContent, + MarkupBlock expectedOutput, + IEnumerable errors, + params string[] tagNames) + { + var providerContext = BuildProviderContext(tagNames); + + EvaluateData(providerContext, documentContent, expectedOutput, errors); + } + + public TagHelperDescriptorProvider BuildProviderContext(params string[] tagNames) + { + var descriptors = new List(); + + foreach (var tagName in tagNames) + { + descriptors.Add( + new TagHelperDescriptor(tagName, tagName + "taghelper", "SomeAssembly")); + } + + return new TagHelperDescriptorProvider(descriptors); + } + + public override ParserContext CreateParserContext( + ITextDocument input, + ParserBase codeParser, + ParserBase markupParser, + ErrorSink errorSink) + { + return base.CreateParserContext(input, codeParser, markupParser, errorSink); + } + + public void EvaluateData( + TagHelperDescriptorProvider provider, + string documentContent, + MarkupBlock expectedOutput, + IEnumerable expectedErrors) + { + var errorSink = new ErrorSink(); + var results = ParseDocument(documentContent, errorSink); + var rewritingContext = new RewritingContext(results.Document, errorSink); + new TagHelperParseTreeRewriter(provider).Rewrite(rewritingContext); + var rewritten = rewritingContext.SyntaxTree; + var actualErrors = errorSink.Errors.OrderBy(error => error.Location.AbsoluteIndex) + .ToList(); + + EvaluateRazorErrors(actualErrors, expectedErrors.ToList()); + EvaluateParseTree(rewritten, expectedOutput); + } + + public static SpanFactory CreateDefaultSpanFactory() + { + return new SpanFactory + { + MarkupTokenizerFactory = doc => new HtmlTokenizer(doc), + CodeTokenizerFactory = doc => new CSharpTokenizer(doc) + }; + } + } +} diff --git a/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/MinimizedTagHelpers.DesignTime.cs b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/MinimizedTagHelpers.DesignTime.cs new file mode 100644 index 000000000..dc4e928a6 --- /dev/null +++ b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/MinimizedTagHelpers.DesignTime.cs @@ -0,0 +1,49 @@ +namespace TestOutput +{ + using Microsoft.AspNet.Razor.Runtime.TagHelpers; + using System; + using System.Threading.Tasks; + + public class MinimizedTagHelpers + { + private static object @__o; + private void @__RazorDesignTimeHelpers__() + { + #pragma warning disable 219 + string __tagHelperDirectiveSyntaxHelper = null; + __tagHelperDirectiveSyntaxHelper = +#line 1 "MinimizedTagHelpers.cshtml" + "something, nice" + +#line default +#line hidden + ; + #pragma warning restore 219 + } + #line hidden + private CatchAllTagHelper __CatchAllTagHelper = null; + private InputTagHelper __InputTagHelper = null; + #line hidden + public MinimizedTagHelpers() + { + } + + #pragma warning disable 1998 + public override async Task ExecuteAsync() + { + __CatchAllTagHelper = CreateTagHelper(); + __InputTagHelper = CreateTagHelper(); + __InputTagHelper.BoundRequiredString = "hello"; + __CatchAllTagHelper = CreateTagHelper(); + __InputTagHelper = CreateTagHelper(); + __InputTagHelper.BoundRequiredString = "hello2"; + __CatchAllTagHelper = CreateTagHelper(); + __CatchAllTagHelper.BoundRequiredString = "world"; + __InputTagHelper = CreateTagHelper(); + __InputTagHelper.BoundRequiredString = "world"; + __CatchAllTagHelper = CreateTagHelper(); + __CatchAllTagHelper = CreateTagHelper(); + } + #pragma warning restore 1998 + } +} diff --git a/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/MinimizedTagHelpers.cs b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/MinimizedTagHelpers.cs new file mode 100644 index 000000000..050b8f6b5 --- /dev/null +++ b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/MinimizedTagHelpers.cs @@ -0,0 +1,106 @@ +#pragma checksum "MinimizedTagHelpers.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "07839be4304797e30b19b50b95e2247c93cdff06" +namespace TestOutput +{ + using Microsoft.AspNet.Razor.Runtime.TagHelpers; + using System; + using System.Threading.Tasks; + + public class MinimizedTagHelpers + { + #line hidden + #pragma warning disable 0414 + private TagHelperContent __tagHelperStringValueBuffer = null; + #pragma warning restore 0414 + private TagHelperExecutionContext __tagHelperExecutionContext = null; + private TagHelperRunner __tagHelperRunner = null; + private TagHelperScopeManager __tagHelperScopeManager = new TagHelperScopeManager(); + private CatchAllTagHelper __CatchAllTagHelper = null; + private InputTagHelper __InputTagHelper = null; + #line hidden + public MinimizedTagHelpers() + { + } + + #pragma warning disable 1998 + public override async Task ExecuteAsync() + { + __tagHelperRunner = __tagHelperRunner ?? new TagHelperRunner(); + Instrumentation.BeginContext(33, 2, true); + WriteLiteral("\r\n"); + Instrumentation.EndContext(); + __tagHelperExecutionContext = __tagHelperScopeManager.Begin("p", false, "test", async() => { + WriteLiteral("\r\n \r\n "); + __tagHelperExecutionContext = __tagHelperScopeManager.Begin("input", true, "test", async() => { + } + , StartTagHelperWritingScope, EndTagHelperWritingScope); + __CatchAllTagHelper = CreateTagHelper(); + __tagHelperExecutionContext.Add(__CatchAllTagHelper); + __tagHelperExecutionContext.AddHtmlAttribute("class", Html.Raw("btn")); + __tagHelperExecutionContext.AddMinimizedHtmlAttribute("catchall-unbound-required"); + __tagHelperExecutionContext.Output = await __tagHelperRunner.RunAsync(__tagHelperExecutionContext); + await WriteTagHelperAsync(__tagHelperExecutionContext); + __tagHelperExecutionContext = __tagHelperScopeManager.End(); + WriteLiteral("\r\n "); + __tagHelperExecutionContext = __tagHelperScopeManager.Begin("input", true, "test", async() => { + } + , StartTagHelperWritingScope, EndTagHelperWritingScope); + __InputTagHelper = CreateTagHelper(); + __tagHelperExecutionContext.Add(__InputTagHelper); + __InputTagHelper.BoundRequiredString = "hello"; + __tagHelperExecutionContext.AddTagHelperAttribute("input-bound-required-string", __InputTagHelper.BoundRequiredString); + __CatchAllTagHelper = CreateTagHelper(); + __tagHelperExecutionContext.Add(__CatchAllTagHelper); + __tagHelperExecutionContext.AddHtmlAttribute("class", Html.Raw("btn")); + __tagHelperExecutionContext.AddMinimizedHtmlAttribute("catchall-unbound-required"); + __tagHelperExecutionContext.AddMinimizedHtmlAttribute("input-unbound-required"); + __tagHelperExecutionContext.Output = await __tagHelperRunner.RunAsync(__tagHelperExecutionContext); + await WriteTagHelperAsync(__tagHelperExecutionContext); + __tagHelperExecutionContext = __tagHelperScopeManager.End(); + WriteLiteral("\r\n "); + __tagHelperExecutionContext = __tagHelperScopeManager.Begin("input", true, "test", async() => { + } + , StartTagHelperWritingScope, EndTagHelperWritingScope); + __InputTagHelper = CreateTagHelper(); + __tagHelperExecutionContext.Add(__InputTagHelper); + __InputTagHelper.BoundRequiredString = "hello2"; + __tagHelperExecutionContext.AddTagHelperAttribute("input-bound-required-string", __InputTagHelper.BoundRequiredString); + __CatchAllTagHelper = CreateTagHelper(); + __tagHelperExecutionContext.Add(__CatchAllTagHelper); + __CatchAllTagHelper.BoundRequiredString = "world"; + __tagHelperExecutionContext.AddTagHelperAttribute("catchall-bound-string", __CatchAllTagHelper.BoundRequiredString); + __tagHelperExecutionContext.AddHtmlAttribute("class", Html.Raw("btn")); + __tagHelperExecutionContext.AddMinimizedHtmlAttribute("catchall-unbound-required"); + __tagHelperExecutionContext.AddMinimizedHtmlAttribute("input-unbound-required"); + __tagHelperExecutionContext.Output = await __tagHelperRunner.RunAsync(__tagHelperExecutionContext); + await WriteTagHelperAsync(__tagHelperExecutionContext); + __tagHelperExecutionContext = __tagHelperScopeManager.End(); + WriteLiteral("\r\n "); + __tagHelperExecutionContext = __tagHelperScopeManager.Begin("input", true, "test", async() => { + } + , StartTagHelperWritingScope, EndTagHelperWritingScope); + __InputTagHelper = CreateTagHelper(); + __tagHelperExecutionContext.Add(__InputTagHelper); + __InputTagHelper.BoundRequiredString = "world"; + __tagHelperExecutionContext.AddTagHelperAttribute("input-bound-required-string", __InputTagHelper.BoundRequiredString); + __CatchAllTagHelper = CreateTagHelper(); + __tagHelperExecutionContext.Add(__CatchAllTagHelper); + __tagHelperExecutionContext.AddHtmlAttribute("class", Html.Raw("btn")); + __tagHelperExecutionContext.AddHtmlAttribute("catchall-unbound-required", Html.Raw("hello")); + __tagHelperExecutionContext.AddHtmlAttribute("input-unbound-required", Html.Raw("hello2")); + __tagHelperExecutionContext.AddMinimizedHtmlAttribute("catchall-unbound-required"); + __tagHelperExecutionContext.Output = await __tagHelperRunner.RunAsync(__tagHelperExecutionContext); + await WriteTagHelperAsync(__tagHelperExecutionContext); + __tagHelperExecutionContext = __tagHelperScopeManager.End(); + WriteLiteral("\r\n"); + } + , StartTagHelperWritingScope, EndTagHelperWritingScope); + __CatchAllTagHelper = CreateTagHelper(); + __tagHelperExecutionContext.Add(__CatchAllTagHelper); + __tagHelperExecutionContext.AddMinimizedHtmlAttribute("catchall-unbound-required"); + __tagHelperExecutionContext.Output = await __tagHelperRunner.RunAsync(__tagHelperExecutionContext); + await WriteTagHelperAsync(__tagHelperExecutionContext); + __tagHelperExecutionContext = __tagHelperScopeManager.End(); + } + #pragma warning restore 1998 + } +} diff --git a/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Source/MinimizedTagHelpers.cshtml b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Source/MinimizedTagHelpers.cshtml new file mode 100644 index 000000000..f0c443cc9 --- /dev/null +++ b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Source/MinimizedTagHelpers.cshtml @@ -0,0 +1,18 @@ +@addTagHelper "something, nice" + +

+ + + + + +

\ No newline at end of file