diff --git a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorFactory.cs b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorFactory.cs index 33495036f1..b69692ec9b 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorFactory.cs +++ b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorFactory.cs @@ -118,6 +118,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers assemblyName, attributeDescriptors, requiredAttributes: Enumerable.Empty(), + tagStructure: default(TagStructure), designTimeDescriptor: typeDesignTimeDescriptor) }; } @@ -147,6 +148,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers assemblyName, attributeDescriptors, requiredAttributes, + targetElementAttribute.TagStructure, designTimeDescriptor); } @@ -156,6 +158,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers string assemblyName, IEnumerable attributeDescriptors, IEnumerable requiredAttributes, + TagStructure tagStructure, TagHelperDesignTimeDescriptor designTimeDescriptor) { return new TagHelperDescriptor( @@ -165,6 +168,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers assemblyName: assemblyName, attributes: attributeDescriptors, requiredAttributes: requiredAttributes, + tagStructure: tagStructure, designTimeDescriptor: designTimeDescriptor); } diff --git a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorResolver.cs b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorResolver.cs index 7d2e2835fd..4256deb946 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorResolver.cs +++ b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorResolver.cs @@ -154,6 +154,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers descriptor.AssemblyName, descriptor.Attributes, descriptor.RequiredAttributes, + descriptor.TagStructure, descriptor.DesignTimeDescriptor)); } diff --git a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperExecutionContext.cs b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperExecutionContext.cs index 114ed5885e..363b35b4b0 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperExecutionContext.cs +++ b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperExecutionContext.cs @@ -22,9 +22,9 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers /// /// Internal for testing purposes only. /// - internal TagHelperExecutionContext(string tagName, bool selfClosing) + internal TagHelperExecutionContext(string tagName, TagMode tagMode) : this(tagName, - selfClosing, + tagMode, items: new Dictionary(), uniqueId: string.Empty, executeChildContentAsync: async () => await Task.FromResult(result: true), @@ -37,8 +37,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers /// Instantiates a new . /// /// The HTML tag name in the Razor source. - /// - /// indicating whether or not the tag in the Razor source was self-closing. + /// HTML syntax of the element in the Razor source. /// The collection of items used to communicate with other /// s /// An identifier unique to the HTML element this context is for. @@ -47,7 +46,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers /// A delegate used to end a writing scope in a Razor page. public TagHelperExecutionContext( [NotNull] string tagName, - bool selfClosing, + TagMode tagMode, [NotNull] IDictionary items, [NotNull] string uniqueId, [NotNull] Func executeChildContentAsync, @@ -59,7 +58,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers _startTagHelperWritingScope = startTagHelperWritingScope; _endTagHelperWritingScope = endTagHelperWritingScope; - SelfClosing = selfClosing; + TagMode = tagMode; HTMLAttributes = new TagHelperAttributeList(); AllAttributes = new TagHelperAttributeList(); TagName = tagName; @@ -68,9 +67,9 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers } /// - /// Gets a value indicating whether or not the tag in the Razor source was self-closing. + /// Gets the HTML syntax of the element in the Razor source. /// - public bool SelfClosing { get; } + public TagMode TagMode { get; } /// /// Indicates if has been called. diff --git a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperOutput.cs b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperOutput.cs index 70dc786d78..bba143fce4 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperOutput.cs +++ b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperOutput.cs @@ -1,8 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; -using System.Collections.Generic; using Microsoft.Framework.Internal; namespace Microsoft.AspNet.Razor.Runtime.TagHelpers @@ -82,9 +80,9 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers } /// - /// Indicates whether or not the tag is self-closing. + /// Syntax of the element in the generated HTML. /// - public bool SelfClosing { get; set; } + public TagMode TagMode { get; set; } /// /// The HTML element's attributes. @@ -100,7 +98,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers /// Changes to generate nothing. /// /// - /// Sets to null, and clears , , + /// Sets to null, and clears , , /// , , and to suppress output. /// public void SuppressOutput() diff --git a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperRunner.cs b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperRunner.cs index 75ddd6fcb7..fdd5b40f53 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperRunner.cs +++ b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperRunner.cs @@ -30,7 +30,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers executionContext.TagName, executionContext.HTMLAttributes) { - SelfClosing = executionContext.SelfClosing, + TagMode = executionContext.TagMode, }; var orderedTagHelpers = executionContext.TagHelpers.OrderBy(tagHelper => tagHelper.Order); diff --git a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperScopeManager.cs b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperScopeManager.cs index be17ff8eff..4464b83fd9 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperScopeManager.cs +++ b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperScopeManager.cs @@ -27,9 +27,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers /// Starts a scope. /// /// The HTML tag name that the scope is associated with. - /// - /// indicating whether or not the tag of this scope is self-closing. - /// + /// HTML syntax of the element in the Razor source. /// An identifier unique to the HTML element this scope is for. /// A delegate used to execute the child content asynchronously. /// A delegate used to start a writing scope in a Razor page. @@ -37,7 +35,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers /// A to use. public TagHelperExecutionContext Begin( [NotNull] string tagName, - bool selfClosing, + TagMode tagMode, [NotNull] string uniqueId, [NotNull] Func executeChildContentAsync, [NotNull] Action startTagHelperWritingScope, @@ -59,7 +57,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers var executionContext = new TagHelperExecutionContext( tagName, - selfClosing, + tagMode, items, uniqueId, executeChildContentAsync, diff --git a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TargetElementAttribute.cs b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TargetElementAttribute.cs index 116ba65a4a..0f2eb5099c 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TargetElementAttribute.cs +++ b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TargetElementAttribute.cs @@ -53,5 +53,31 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers /// * at the end of an attribute name acts as a prefix match. /// public string Attributes { get; set; } + + /// + /// The expected tag structure. + /// + /// + /// If and no other tag helpers applying to the same element specify + /// their the behavior is used: + /// + /// + /// <my-tag-helper></my-tag-helper> + /// <!-- OR --> + /// <my-tag-helper /> + /// + /// Otherwise, if another tag helper applying to the same element does specify their behavior, that behavior + /// is used. + /// + /// + /// If HTML elements can be written in the following formats: + /// + /// <my-tag-helper> + /// <!-- OR --> + /// <my-tag-helper /> + /// + /// + /// + public TagStructure TagStructure { get; set; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor/Chunks/Generators/TagHelperChunkGenerator.cs b/src/Microsoft.AspNet.Razor/Chunks/Generators/TagHelperChunkGenerator.cs index 1d9b8421fc..3c77a46238 100644 --- a/src/Microsoft.AspNet.Razor/Chunks/Generators/TagHelperChunkGenerator.cs +++ b/src/Microsoft.AspNet.Razor/Chunks/Generators/TagHelperChunkGenerator.cs @@ -1,7 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -84,7 +83,7 @@ namespace Microsoft.AspNet.Razor.Chunks.Generators context.ChunkTreeBuilder.StartParentChunk( new TagHelperChunk( unprefixedTagName, - tagHelperBlock.SelfClosing, + tagHelperBlock.TagMode, attributes, _tagHelperDescriptors), target, diff --git a/src/Microsoft.AspNet.Razor/Chunks/TagHelperChunk.cs b/src/Microsoft.AspNet.Razor/Chunks/TagHelperChunk.cs index ed0f4e19df..5e4d6768de 100644 --- a/src/Microsoft.AspNet.Razor/Chunks/TagHelperChunk.cs +++ b/src/Microsoft.AspNet.Razor/Chunks/TagHelperChunk.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.Generic; +using Microsoft.AspNet.Razor.Runtime.TagHelpers; using Microsoft.AspNet.Razor.TagHelpers; namespace Microsoft.AspNet.Razor.Chunks @@ -15,21 +16,19 @@ namespace Microsoft.AspNet.Razor.Chunks /// Instantiates a new . /// /// The tag name associated with the tag helpers HTML element. - /// - /// indicating whether or not the tag of the tag helpers HTML element is self-closing. - /// + /// HTML syntax of the element in the Razor source. /// The attributes associated with the tag helpers HTML element. /// /// The s associated with this tag helpers HTML element. /// public TagHelperChunk( string tagName, - bool selfClosing, + TagMode tagMode, IList> attributes, IEnumerable descriptors) { TagName = tagName; - SelfClosing = selfClosing; + TagMode = tagMode; Attributes = attributes; Descriptors = descriptors; } @@ -54,8 +53,8 @@ namespace Microsoft.AspNet.Razor.Chunks public string TagName { get; set; } /// - /// Gets a value indicating whether or not the tag of the tag helpers HTML element is self-closing. + /// Gets the HTML syntax of the element in the Razor source. /// - public bool SelfClosing { get; } + public TagMode TagMode { get; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor/CodeGenerators/CSharpTagHelperCodeRenderer.cs b/src/Microsoft.AspNet.Razor/CodeGenerators/CSharpTagHelperCodeRenderer.cs index a75a9d0d33..29447f3c06 100644 --- a/src/Microsoft.AspNet.Razor/CodeGenerators/CSharpTagHelperCodeRenderer.cs +++ b/src/Microsoft.AspNet.Razor/CodeGenerators/CSharpTagHelperCodeRenderer.cs @@ -7,6 +7,7 @@ using System.Globalization; using System.Linq; using Microsoft.AspNet.Razor.Chunks; using Microsoft.AspNet.Razor.CodeGenerators.Visitors; +using Microsoft.AspNet.Razor.Runtime.TagHelpers; using Microsoft.AspNet.Razor.TagHelpers; using Microsoft.Framework.Internal; @@ -65,7 +66,7 @@ namespace Microsoft.AspNet.Razor.CodeGenerators // the same TagHelper X many times (instead of once) over a single HTML element. var tagHelperDescriptors = chunk.Descriptors.Distinct(TypeBasedTagHelperDescriptorComparer.Default); - RenderBeginTagHelperScope(chunk.TagName, chunk.SelfClosing, chunk.Children); + RenderBeginTagHelperScope(chunk.TagName, chunk.TagMode, chunk.Children); RenderTagHelpersCreation(chunk, tagHelperDescriptors); @@ -85,7 +86,7 @@ namespace Microsoft.AspNet.Razor.CodeGenerators return "__" + descriptor.TypeName.Replace('.', '_'); } - private void RenderBeginTagHelperScope(string tagName, bool selfClosing, IList children) + private void RenderBeginTagHelperScope(string tagName, TagMode tagMode, IList children) { // Scopes/execution contexts are a runtime feature. if (_designTimeMode) @@ -105,7 +106,9 @@ namespace Microsoft.AspNet.Razor.CodeGenerators // per call site, e.g. if the tag is on the view twice, there should be two IDs. _writer.WriteStringLiteral(tagName) .WriteParameterSeparator() - .WriteBooleanLiteral(selfClosing) + .Write(nameof(TagMode)) + .Write(".") + .Write(tagMode.ToString()) .WriteParameterSeparator() .WriteStringLiteral(GenerateUniqueId()) .WriteParameterSeparator(); diff --git a/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlock.cs b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlock.cs index feacf85a53..3ec14979a4 100644 --- a/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlock.cs +++ b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlock.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Runtime.TagHelpers; using Microsoft.AspNet.Razor.TagHelpers; using Microsoft.Framework.Internal; @@ -30,7 +31,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers Descriptors = source.Descriptors; Attributes = new List>(source.Attributes); _start = source.Start; - SelfClosing = source.SelfClosing; + TagMode = source.TagMode; SourceStartTag = source.SourceStartTag; SourceEndTag = source.SourceEndTag; @@ -58,9 +59,9 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers public Block SourceEndTag { get; } /// - /// Indicates whether or not the tag is self closing. + /// Gets the HTML syntax of the element in the Razor source. /// - public bool SelfClosing { get; } + public TagMode TagMode { get; } /// /// s for the HTML element. diff --git a/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockBuilder.cs b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockBuilder.cs index ebed3bb2da..5b3ef82b2b 100644 --- a/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockBuilder.cs +++ b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockBuilder.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using Microsoft.AspNet.Razor.Chunks.Generators; using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Runtime.TagHelpers; using Microsoft.AspNet.Razor.TagHelpers; namespace Microsoft.AspNet.Razor.Parser.TagHelpers @@ -32,22 +33,20 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers /// and from the . /// /// An HTML tag name. - /// - /// indicating whether or not the tag in the Razor source was self-closing. - /// + /// HTML syntax of the element in the Razor source. /// Starting location of the . /// Attributes of the . /// The s associated with the current HTML /// tag. public TagHelperBlockBuilder( string tagName, - bool selfClosing, + TagMode tagMode, SourceLocation start, IList> attributes, IEnumerable descriptors) { TagName = tagName; - SelfClosing = selfClosing; + TagMode = tagMode; Start = start; Descriptors = descriptors; Attributes = new List>(attributes); @@ -58,12 +57,12 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers // Internal for testing internal TagHelperBlockBuilder( string tagName, - bool selfClosing, + TagMode tagMode, IList> attributes, IEnumerable children) { TagName = tagName; - SelfClosing = selfClosing; + TagMode = tagMode; Attributes = attributes; Type = BlockType.Tag; ChunkGenerator = new TagHelperChunkGenerator(tagHelperDescriptors: null); @@ -88,9 +87,9 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers public Block SourceEndTag { get; set; } /// - /// Gets a value indicating whether or not the tag in the Razor source was self-closing. + /// Gets the HTML syntax of the element in the Razor source. /// - public bool SelfClosing { get; } + public TagMode TagMode { get; } /// /// s for the HTML element. diff --git a/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockRewriter.cs b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockRewriter.cs index 94b4c234aa..5b710f14b9 100644 --- a/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockRewriter.cs +++ b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockRewriter.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.Linq; using Microsoft.AspNet.Razor.Chunks.Generators; using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Runtime.TagHelpers; using Microsoft.AspNet.Razor.TagHelpers; using Microsoft.AspNet.Razor.Tokenizer.Symbols; @@ -25,9 +26,9 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal // There will always be at least one child for the '<'. var start = tag.Children.First().Start; var attributes = GetTagAttributes(tagName, validStructure, tag, descriptors, errorSink); - var selfClosing = IsSelfClosing(tag); + var tagMode = GetTagMode(tagName, tag, descriptors, errorSink); - return new TagHelperBlockBuilder(tagName, selfClosing, start, attributes, descriptors); + return new TagHelperBlockBuilder(tagName, tagMode, start, attributes, descriptors); } private static IList> GetTagAttributes( @@ -108,11 +109,29 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal return attributes; } - private static bool IsSelfClosing(Block beginTagBlock) + private static TagMode GetTagMode( + string tagName, + Block beginTagBlock, + IEnumerable descriptors, + ErrorSink errorSink) { var childSpan = beginTagBlock.FindLastDescendentSpan(); - return childSpan?.Content.EndsWith("/>") ?? false; + // Self-closing tags are always valid despite descriptors[X].TagStructure. + if (childSpan?.Content.EndsWith("/>") ?? false) + { + return TagMode.SelfClosing; + } + + var baseDescriptor = descriptors.FirstOrDefault( + descriptor => descriptor.TagStructure != TagStructure.Unspecified); + var resolvedTagStructure = baseDescriptor?.TagStructure ?? TagStructure.Unspecified; + if (resolvedTagStructure == TagStructure.WithoutEndTag) + { + return TagMode.StartTagOnly; + } + + return TagMode.StartTagAndEndTag; } // This method handles cases when the attribute is a simple span attribute such as diff --git a/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperParseTreeRewriter.cs b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperParseTreeRewriter.cs index 869969b7bf..4b2641f8c4 100644 --- a/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperParseTreeRewriter.cs +++ b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperParseTreeRewriter.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Runtime.TagHelpers; using Microsoft.AspNet.Razor.TagHelpers; using Microsoft.AspNet.Razor.Tokenizer.Symbols; @@ -70,7 +71,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal // At this point the child is a Span or Block with Type BlockType.Tag that doesn't happen to be a // tag helper. - // Add the child to current block. + // Add the child to current block. _currentBlock.Children.Add(child); } @@ -78,7 +79,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal // it means that there are malformed tag helpers at the top of our stack. if (activeTagHelpers != _trackerStack.Count) { - // Malformed tag helpers built here will be tag helpers that do not have end tags in the current block + // Malformed tag helpers built here will be tag helpers that do not have end tags in the current block // scope. Block scopes are special cases in Razor such as @

would cause an error because there's no // matching end

tag in the template block scope and therefore doesn't make sense as a tag helper. BuildMalformedTagHelpers(_trackerStack.Count - activeTagHelpers, context); @@ -117,11 +118,12 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal descriptors = _provider.GetDescriptors(tagName, providedAttributes); + // If there aren't any TagHelperDescriptors registered then we aren't a TagHelper if (!descriptors.Any()) { // If the current tag matches the current TagHelper scope it means the parent TagHelper matched - // all the required attributes but the current one did not; therefore, we need to increment the + // all the required attributes but the current one did not; therefore, we need to increment the // OpenMatchingTags counter for current the TagHelperBlock so we don't end it too early. // ex: We don't want the first myth to close on the inside // tag. @@ -133,8 +135,10 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal return false; } + ValidateDescriptors(descriptors, tagName, tagBlock, context.ErrorSink); + // We're in a start TagHelper block. - var validTagStructure = ValidateTagStructure(tagName, tagBlock, context); + var validTagStructure = ValidateTagSyntax(tagName, tagBlock, context); var builder = TagHelperBlockRewriter.Rewrite( tagName, @@ -143,16 +147,16 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal descriptors, context.ErrorSink); - // Track the original start tag so the editor knows where each piece of the TagHelperBlock lies + // Track the original start tag so the editor knows where each piece of the TagHelperBlock lies // for formatting. builder.SourceStartTag = tagBlock; // Found a new tag helper block TrackTagHelperBlock(builder); - // If it's a self closing block then we don't have to worry about nested children - // within the tag... complete it. - if (builder.SelfClosing) + // If it's a non-content expecting block then we don't have to worry about nested children within the + // tag. Complete it. + if (builder.TagMode == TagMode.SelfClosing || builder.TagMode == TagMode.StartTagOnly) { BuildCurrentlyTrackedTagHelperBlock(endTag: null); } @@ -171,25 +175,43 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal return false; } - ValidateTagStructure(tagName, tagBlock, context); + ValidateTagSyntax(tagName, tagBlock, context); BuildCurrentlyTrackedTagHelperBlock(tagBlock); } else { - // If there are not TagHelperDescriptors associated with the end tag block that also have no + descriptors = _provider.GetDescriptors(tagName, attributeNames: Enumerable.Empty()); + + // If there are not TagHelperDescriptors associated with the end tag block that also have no // required attributes then it means we can't be a TagHelper, bail out. - if (!_provider.GetDescriptors(tagName, attributeNames: Enumerable.Empty()).Any()) + if (!descriptors.Any()) { return false; } - // Current tag helper scope does not match the end tag. Attempt to recover the tag - // helper by looking up the previous tag helper scopes for a matching tag. If we + var invalidDescriptor = descriptors.FirstOrDefault( + descriptor => descriptor.TagStructure == TagStructure.WithoutEndTag); + if (invalidDescriptor != null) + { + // End tag TagHelper that states it shouldn't have an end tag. + context.ErrorSink.OnError( + tagBlock.Start, + RazorResources.FormatTagHelperParseTreeRewriter_EndTagTagHelperMustNotHaveAnEndTag( + tagName, + invalidDescriptor.TypeName, + invalidDescriptor.TagStructure), + tagBlock.Length); + + return false; + } + + // Current tag helper scope does not match the end tag. Attempt to recover the tag + // helper by looking up the previous tag helper scopes for a matching tag. If we // can't recover it means there was no corresponding tag helper start tag. if (TryRecoverTagHelper(tagName, tagBlock, context)) { - ValidateTagStructure(tagName, tagBlock, context); + ValidateTagSyntax(tagName, tagBlock, context); // Successfully recovered, move onto the next element. } @@ -245,9 +267,40 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal return attributeNames; } - private static bool ValidateTagStructure(string tagName, Block tag, RewritingContext context) + private static void ValidateDescriptors( + IEnumerable descriptors, + string tagName, + Block tagBlock, + ErrorSink errorSink) { - // We assume an invalid structure until we verify that the tag meets all of our "valid structure" criteria. + // Ensure that all descriptors associated with this tag have appropriate TagStructures. Cannot have + // multiple descriptors that expect different TagStructures (other than TagStructure.Unspecified). + TagHelperDescriptor baseDescriptor = null; + foreach (var descriptor in descriptors) + { + if (descriptor.TagStructure != TagStructure.Unspecified) + { + // Can't have a set of TagHelpers that expect different structures. + if (baseDescriptor != null && baseDescriptor.TagStructure != descriptor.TagStructure) + { + errorSink.OnError( + tagBlock.Start, + RazorResources.FormatTagHelperParseTreeRewriter_InconsistentTagStructure( + baseDescriptor.TypeName, + descriptor.TypeName, + tagName, + nameof(TagHelperDescriptor.TagStructure)), + tagBlock.Length); + } + + baseDescriptor = descriptor; + } + } + } + + private static bool ValidateTagSyntax(string tagName, Block tag, RewritingContext context) + { + // We assume an invalid syntax until we verify that the tag meets all of our "valid syntax" criteria. if (IsPartialTag(tag)) { context.ErrorSink.OnError( @@ -304,7 +357,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal private void BuildCurrentlyTrackedTagHelperBlock(Block endTag) { - // Track the original end tag so the editor knows where each piece of the TagHelperBlock lies + // Track the original end tag so the editor knows where each piece of the TagHelperBlock lies // for formatting. _trackerStack.Pop().Builder.SourceEndTag = endTag; @@ -351,7 +404,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal malformedTagHelperCount++; } - // If the malformedTagHelperCount == _tagStack.Count it means we couldn't find a start tag for the tag + // If the malformedTagHelperCount == _tagStack.Count it means we couldn't find a start tag for the tag // helper, can't recover. if (malformedTagHelperCount != _trackerStack.Count) { diff --git a/src/Microsoft.AspNet.Razor/Properties/RazorResources.Designer.cs b/src/Microsoft.AspNet.Razor/Properties/RazorResources.Designer.cs index b717e43638..3677d1371c 100644 --- a/src/Microsoft.AspNet.Razor/Properties/RazorResources.Designer.cs +++ b/src/Microsoft.AspNet.Razor/Properties/RazorResources.Designer.cs @@ -1482,6 +1482,38 @@ namespace Microsoft.AspNet.Razor return string.Format(CultureInfo.CurrentCulture, GetString("SourceLocationFilePathDoesNotMatch"), p0, p1); } + /// + /// Tag helpers '{0}' and '{1}' targeting element '{2}' must not expect different {3} values. + /// + internal static string TagHelperParseTreeRewriter_InconsistentTagStructure + { + get { return GetString("TagHelperParseTreeRewriter_InconsistentTagStructure"); } + } + + /// + /// Tag helpers '{0}' and '{1}' targeting element '{2}' must not expect different {3} values. + /// + internal static string FormatTagHelperParseTreeRewriter_InconsistentTagStructure(object p0, object p1, object p2, object p3) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperParseTreeRewriter_InconsistentTagStructure"), p0, p1, p2, p3); + } + + /// + /// Found an end tag (&lt;/{0}&gt;) for tag helper '{1}' with tag structure that disallows an end tag ('{2}'). + /// + internal static string TagHelperParseTreeRewriter_EndTagTagHelperMustNotHaveAnEndTag + { + get { return GetString("TagHelperParseTreeRewriter_EndTagTagHelperMustNotHaveAnEndTag"); } + } + + /// + /// Found an end tag (&lt;/{0}&gt;) for tag helper '{1}' with tag structure that disallows an end tag ('{2}'). + /// + internal static string FormatTagHelperParseTreeRewriter_EndTagTagHelperMustNotHaveAnEndTag(object p0, object p1, object p2) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperParseTreeRewriter_EndTagTagHelperMustNotHaveAnEndTag"), p0, p1, p2); + } + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNet.Razor/RazorResources.resx b/src/Microsoft.AspNet.Razor/RazorResources.resx index 2c40a9ee7c..d0680dfa81 100644 --- a/src/Microsoft.AspNet.Razor/RazorResources.resx +++ b/src/Microsoft.AspNet.Razor/RazorResources.resx @@ -413,4 +413,10 @@ Instead, wrap the contents of the block in "{{}}": Cannot perform '{1}' operations on '{0}' instances with different file paths. + + Tag helpers '{0}' and '{1}' targeting element '{2}' must not expect different {3} values. + + + Found an end tag (&lt;/{0}&gt;) for tag helper '{1}' with tag structure that disallows an end tag ('{2}'). + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor/Runtime/TagHelpers/TagMode.cs b/src/Microsoft.AspNet.Razor/Runtime/TagHelpers/TagMode.cs new file mode 100644 index 0000000000..4f1232df17 --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Runtime/TagHelpers/TagMode.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Razor.Runtime.TagHelpers +{ + /// + /// The mode in which an element should render. + /// + public enum TagMode + { + /// + /// Include both start and end tags. + /// + StartTagAndEndTag, + + /// + /// A self-closed tag. + /// + SelfClosing, + + /// + /// Only a start tag. + /// + StartTagOnly + } +} diff --git a/src/Microsoft.AspNet.Razor/Runtime/TagHelpers/TagStructure.cs b/src/Microsoft.AspNet.Razor/Runtime/TagHelpers/TagStructure.cs new file mode 100644 index 0000000000..b7866c220d --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Runtime/TagHelpers/TagStructure.cs @@ -0,0 +1,28 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Razor.Runtime.TagHelpers +{ + /// + /// The structure the element should be written in. + /// + public enum TagStructure + { + /// + /// If no other tag helper applies to the same element and specifies a , + /// will be used. + /// + Unspecified, + + /// + /// Element can be written as <my-tag-helper></my-tag-helper> or <my-tag-helper />. + /// + NormalOrSelfClosing, + + /// + /// Element can be written as <my-tag-helper> or <my-tag-helper />. + /// + /// Elements with a structure will never have any content. + WithoutEndTag + } +} diff --git a/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptor.cs b/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptor.cs index bea47ddab7..4f2ebd329b 100644 --- a/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptor.cs +++ b/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptor.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using Microsoft.AspNet.Razor.Runtime.TagHelpers; using Microsoft.Framework.Internal; namespace Microsoft.AspNet.Razor.TagHelpers @@ -60,6 +61,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers assemblyName: assemblyName, attributes: attributes, requiredAttributes: requiredAttributes, + tagStructure: TagStructure.Unspecified, designTimeDescriptor: null) { } @@ -91,6 +93,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers [NotNull] string assemblyName, [NotNull] IEnumerable attributes, [NotNull] IEnumerable requiredAttributes, + TagStructure tagStructure, TagHelperDesignTimeDescriptor designTimeDescriptor) { Prefix = prefix ?? string.Empty; @@ -100,6 +103,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers AssemblyName = assemblyName; Attributes = new List(attributes); RequiredAttributes = new List(requiredAttributes); + TagStructure = tagStructure; DesignTimeDescriptor = designTimeDescriptor; } @@ -143,6 +147,32 @@ namespace Microsoft.AspNet.Razor.TagHelpers /// public IReadOnlyList RequiredAttributes { get; } + /// + /// The expected tag structure. + /// + /// + /// If and no other tag helpers applying to the same element specify + /// their the behavior is used: + /// + /// + /// <my-tag-helper></my-tag-helper> + /// <!-- OR --> + /// <my-tag-helper /> + /// + /// Otherwise, if another tag helper applying to the same element does specify their behavior, that behavior + /// is used. + /// + /// + /// If HTML elements can be written in the following formats: + /// + /// <my-tag-helper> + /// <!-- OR --> + /// <my-tag-helper /> + /// + /// + /// + public TagStructure TagStructure { get; } + /// /// The that contains design time information about this /// tag helper. diff --git a/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptorComparer.cs b/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptorComparer.cs index 3c8092decd..88cb6bcb6f 100644 --- a/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptorComparer.cs +++ b/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptorComparer.cs @@ -30,8 +30,8 @@ namespace Microsoft.AspNet.Razor.TagHelpers /// /// Determines equality based on , /// , , - /// and . Ignores - /// because it can be inferred directly from + /// , and . + /// Ignores because it can be inferred directly from /// and . /// public virtual bool Equals(TagHelperDescriptor descriptorX, TagHelperDescriptor descriptorY) @@ -48,7 +48,8 @@ namespace Microsoft.AspNet.Razor.TagHelpers Enumerable.SequenceEqual( descriptorX.RequiredAttributes.OrderBy(attribute => attribute, StringComparer.OrdinalIgnoreCase), descriptorY.RequiredAttributes.OrderBy(attribute => attribute, StringComparer.OrdinalIgnoreCase), - StringComparer.OrdinalIgnoreCase); + StringComparer.OrdinalIgnoreCase) && + descriptorX.TagStructure == descriptorY.TagStructure; } /// @@ -57,7 +58,8 @@ namespace Microsoft.AspNet.Razor.TagHelpers var hashCodeCombiner = HashCodeCombiner.Start() .Add(descriptor.TypeName, StringComparer.Ordinal) .Add(descriptor.TagName, StringComparer.OrdinalIgnoreCase) - .Add(descriptor.AssemblyName, StringComparer.Ordinal); + .Add(descriptor.AssemblyName, StringComparer.Ordinal) + .Add(descriptor.TagStructure); var attributes = descriptor.RequiredAttributes.OrderBy( attribute => attribute, diff --git a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorFactoryTest.cs b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorFactoryTest.cs index 270c29c34b..0d68e6aab6 100644 --- a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorFactoryTest.cs +++ b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorFactoryTest.cs @@ -17,6 +17,106 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers private static readonly string AssemblyName = typeof(TagHelperDescriptorFactoryTest).GetTypeInfo().Assembly.GetName().Name; + public static TheoryData TagStructureData + { + get + { + // tagHelperType, expectedDescriptors + return new TheoryData + { + { + typeof(TagStructureTagHelper), + new[] + { + new TagHelperDescriptor( + prefix: string.Empty, + tagName: "input", + typeName: typeof(TagStructureTagHelper).FullName, + assemblyName: AssemblyName, + attributes: Enumerable.Empty(), + requiredAttributes: Enumerable.Empty(), + tagStructure: TagStructure.WithoutEndTag, + designTimeDescriptor: null) + } + }, + { + typeof(MultiSpecifiedTagStructureTagHelper), + new[] + { + new TagHelperDescriptor( + prefix: string.Empty, + tagName: "input", + typeName: typeof(MultiSpecifiedTagStructureTagHelper).FullName, + assemblyName: AssemblyName, + attributes: Enumerable.Empty(), + requiredAttributes: Enumerable.Empty(), + tagStructure: TagStructure.WithoutEndTag, + designTimeDescriptor: null), + new TagHelperDescriptor( + prefix: string.Empty, + tagName: "p", + typeName: typeof(MultiSpecifiedTagStructureTagHelper).FullName, + assemblyName: AssemblyName, + attributes: Enumerable.Empty(), + requiredAttributes: Enumerable.Empty(), + tagStructure: TagStructure.NormalOrSelfClosing, + designTimeDescriptor: null), + } + }, + { + typeof(MultiWithUnspecifiedTagStructureTagHelper), + new[] + { + new TagHelperDescriptor( + prefix: string.Empty, + tagName: "input", + typeName: typeof(MultiWithUnspecifiedTagStructureTagHelper).FullName, + assemblyName: AssemblyName, + attributes: Enumerable.Empty(), + requiredAttributes: Enumerable.Empty(), + tagStructure: TagStructure.WithoutEndTag, + designTimeDescriptor: null), + new TagHelperDescriptor( + prefix: string.Empty, + tagName: "p", + typeName: typeof(MultiWithUnspecifiedTagStructureTagHelper).FullName, + assemblyName: AssemblyName, + attributes: Enumerable.Empty(), + requiredAttributes: Enumerable.Empty(), + tagStructure: TagStructure.Unspecified, + designTimeDescriptor: null), + } + }, + }; + } + } + + [Theory] + [MemberData(nameof(TagStructureData))] + public void CreateDescriptors_CreatesDesignTimeDescriptorsWithTagStructure( + Type tagHelperType, + TagHelperDescriptor[] expectedDescriptors) + { + // Arrange + var errorSink = new ErrorSink(); + + // Act + var descriptors = TagHelperDescriptorFactory.CreateDescriptors( + AssemblyName, + tagHelperType, + designTime: false, + errorSink: errorSink); + + // Assert + Assert.Empty(errorSink.Errors); + + // We don't care about order. Mono returns reflected attributes differently so we need to ensure order + // doesn't matter by sorting. + descriptors = descriptors.OrderBy(descriptor => descriptor.TagName); + + Assert.Equal(expectedDescriptors, descriptors, CaseSensitiveTagHelperDescriptorComparer.Default); + } + // TagHelperDesignTimeDescriptors are not created in CoreCLR. #if !DNXCORE50 public static TheoryData OutputElementHintData @@ -37,6 +137,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers assemblyName: AssemblyName, attributes: Enumerable.Empty(), requiredAttributes: Enumerable.Empty(), + tagStructure: default(TagStructure), designTimeDescriptor: new TagHelperDesignTimeDescriptor( summary: null, remarks: null, @@ -54,6 +155,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers assemblyName: AssemblyName, attributes: Enumerable.Empty(), requiredAttributes: Enumerable.Empty(), + tagStructure: default(TagStructure), designTimeDescriptor: new TagHelperDesignTimeDescriptor( summary: null, remarks: null, @@ -65,6 +167,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers assemblyName: AssemblyName, attributes: Enumerable.Empty(), requiredAttributes: Enumerable.Empty(), + tagStructure: default(TagStructure), designTimeDescriptor: new TagHelperDesignTimeDescriptor( summary: null, remarks: null, @@ -1870,6 +1973,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers assemblyName: assemblyName, attributes: attributes ?? Enumerable.Empty(), requiredAttributes: requiredAttributes ?? Enumerable.Empty(), + tagStructure: default(TagStructure), designTimeDescriptor: null); } @@ -1885,6 +1989,23 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers designTimeDescriptor: null); } + [TargetElement("input", TagStructure = TagStructure.WithoutEndTag)] + private class TagStructureTagHelper : TagHelper + { + } + + [TargetElement("p", TagStructure = TagStructure.NormalOrSelfClosing)] + [TargetElement("input", TagStructure = TagStructure.WithoutEndTag)] + private class MultiSpecifiedTagStructureTagHelper : TagHelper + { + } + + [TargetElement("p")] + [TargetElement("input", TagStructure = TagStructure.WithoutEndTag)] + private class MultiWithUnspecifiedTagStructureTagHelper : TagHelper + { + } + [EditorBrowsable(EditorBrowsableState.Always)] private class DefaultEditorBrowsableTagHelper : TagHelper { diff --git a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorResolverTest.cs b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorResolverTest.cs index 06ac46da7f..3266d6cf69 100644 --- a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorResolverTest.cs +++ b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorResolverTest.cs @@ -32,6 +32,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers assemblyName: AssemblyName, attributes: Enumerable.Empty(), requiredAttributes: Enumerable.Empty(), + tagStructure: default(TagStructure), designTimeDescriptor: null); } } @@ -47,6 +48,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers assemblyName: AssemblyName, attributes: Enumerable.Empty(), requiredAttributes: Enumerable.Empty(), + tagStructure: default(TagStructure), designTimeDescriptor: null); } } @@ -573,6 +575,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers assemblyName: assemblyB, attributes: Enumerable.Empty(), requiredAttributes: Enumerable.Empty(), + tagStructure: default(TagStructure), designTimeDescriptor: null); return new TheoryData>, // descriptorAssemblyLookups @@ -1008,6 +1011,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers assemblyName: assemblyB, attributes: Enumerable.Empty(), requiredAttributes: Enumerable.Empty(), + tagStructure: default(TagStructure), designTimeDescriptor: null); return new TheoryData>, // descriptorAssemblyLookups @@ -1390,6 +1394,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers assemblyName, attributes: Enumerable.Empty(), requiredAttributes: Enumerable.Empty(), + tagStructure: default(TagStructure), designTimeDescriptor: null); } diff --git a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperExecutionContextTest.cs b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperExecutionContextTest.cs index d6599dda07..d8f0dcf94e 100644 --- a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperExecutionContextTest.cs +++ b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperExecutionContextTest.cs @@ -13,15 +13,16 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers { [Theory] - [InlineData(true)] - [InlineData(false)] - public void SelfClosing_ReturnsTrueOrFalseAsExpected(bool selfClosing) + [InlineData(TagMode.SelfClosing)] + [InlineData(TagMode.StartTagAndEndTag)] + [InlineData(TagMode.StartTagOnly)] + public void TagMode_ReturnsExpectedValue(TagMode tagMode) { // Arrange & Act - var executionContext = new TagHelperExecutionContext("p", selfClosing); + var executionContext = new TagHelperExecutionContext("p", tagMode); // Assert - Assert.Equal(selfClosing, executionContext.SelfClosing); + Assert.Equal(tagMode, executionContext.TagMode); } [Fact] @@ -36,7 +37,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers // Act var executionContext = new TagHelperExecutionContext( "p", - selfClosing: false, + tagMode: TagMode.StartTagAndEndTag, items: expectedItems, uniqueId: string.Empty, executeChildContentAsync: async () => await Task.FromResult(result: true), @@ -56,7 +57,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers var expectedContent = string.Empty; var executionContext = new TagHelperExecutionContext( "p", - selfClosing: false, + tagMode: TagMode.StartTagAndEndTag, items: null, uniqueId: string.Empty, executeChildContentAsync: () => @@ -89,7 +90,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers var executionCount = 0; var executionContext = new TagHelperExecutionContext( "p", - selfClosing: false, + tagMode: TagMode.StartTagAndEndTag, items: null, uniqueId: string.Empty, executeChildContentAsync: () => @@ -118,7 +119,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers var defaultTagHelperContent = new DefaultTagHelperContent(); var executionContext = new TagHelperExecutionContext( "p", - selfClosing: false, + tagMode: TagMode.StartTagAndEndTag, items: null, uniqueId: string.Empty, executeChildContentAsync: () => { return Task.FromResult(result: true); }, @@ -143,7 +144,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers var childContentExecutionCount = 0; var executionContext = new TagHelperExecutionContext( "p", - selfClosing: false, + tagMode: TagMode.StartTagAndEndTag, items: null, uniqueId: string.Empty, executeChildContentAsync: () => @@ -182,7 +183,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers public void HtmlAttributes_IgnoresCase(string originalName, string updatedName) { // Arrange - var executionContext = new TagHelperExecutionContext("p", selfClosing: false); + var executionContext = new TagHelperExecutionContext("p", TagMode.StartTagAndEndTag); executionContext.HTMLAttributes[originalName] = "hello"; // Act @@ -198,7 +199,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers public void AllAttributes_IgnoresCase(string originalName, string updatedName) { // Arrange - var executionContext = new TagHelperExecutionContext("p", selfClosing: false); + var executionContext = new TagHelperExecutionContext("p", tagMode: TagMode.StartTagAndEndTag); executionContext.AllAttributes.Add(originalName, value: false); // Act @@ -213,7 +214,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers public void AddHtmlAttribute_MaintainsHTMLAttributes() { // Arrange - var executionContext = new TagHelperExecutionContext("p", selfClosing: false); + var executionContext = new TagHelperExecutionContext("p", TagMode.StartTagAndEndTag); var expectedAttributes = new TagHelperAttributeList { { "class", "btn" }, @@ -235,7 +236,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers public void AddMinimizedHtmlAttribute_MaintainsHTMLAttributes() { // Arrange - var executionContext = new TagHelperExecutionContext("input", selfClosing: true); + var executionContext = new TagHelperExecutionContext("input", tagMode: TagMode.StartTagOnly); var expectedAttributes = new TagHelperAttributeList { ["checked"] = new TagHelperAttribute { Name = "checked", Minimized = true }, @@ -257,7 +258,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers public void AddMinimizedHtmlAttribute_MaintainsHTMLAttributes_SomeMinimized() { // Arrange - var executionContext = new TagHelperExecutionContext("input", selfClosing: true); + var executionContext = new TagHelperExecutionContext("input", tagMode: TagMode.SelfClosing); var expectedAttributes = new TagHelperAttributeList { { "class", "btn" }, @@ -283,7 +284,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers public void TagHelperExecutionContext_MaintainsAllAttributes() { // Arrange - var executionContext = new TagHelperExecutionContext("p", selfClosing: false); + var executionContext = new TagHelperExecutionContext("p", TagMode.StartTagAndEndTag); var expectedAttributes = new TagHelperAttributeList { { "class", "btn" }, @@ -307,7 +308,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers public void Add_MaintainsTagHelpers() { // Arrange - var executionContext = new TagHelperExecutionContext("p", selfClosing: false); + var executionContext = new TagHelperExecutionContext("p", TagMode.StartTagAndEndTag); var tagHelper = new PTagHelper(); // Act @@ -322,7 +323,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers public void Add_MaintainsMultipleTagHelpers() { // Arrange - var executionContext = new TagHelperExecutionContext("p", selfClosing: false); + var executionContext = new TagHelperExecutionContext("p", TagMode.StartTagAndEndTag); var tagHelper1 = new PTagHelper(); var tagHelper2 = new PTagHelper(); diff --git a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperRunnerTest.cs b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperRunnerTest.cs index 04b9bdc5f6..7fe662a710 100644 --- a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperRunnerTest.cs +++ b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperRunnerTest.cs @@ -62,7 +62,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers { // Arrange var runner = new TagHelperRunner(); - var executionContext = new TagHelperExecutionContext("p", selfClosing: false); + var executionContext = new TagHelperExecutionContext("p", TagMode.StartTagAndEndTag); var processOrder = new List(); foreach (var order in tagHelperOrders) @@ -82,13 +82,14 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers } [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task RunAsync_SetTagHelperOutputSelfClosing(bool selfClosing) + [InlineData(TagMode.SelfClosing)] + [InlineData(TagMode.StartTagAndEndTag)] + [InlineData(TagMode.StartTagOnly)] + public async Task RunAsync_SetsTagHelperOutputTagMode(TagMode tagMode) { // Arrange var runner = new TagHelperRunner(); - var executionContext = new TagHelperExecutionContext("p", selfClosing); + var executionContext = new TagHelperExecutionContext("p", tagMode); var tagHelper = new TagHelperContextTouchingTagHelper(); executionContext.Add(tagHelper); @@ -98,7 +99,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers var output = await runner.RunAsync(executionContext); // Assert - Assert.Equal(selfClosing, output.SelfClosing); + Assert.Equal(tagMode, output.TagMode); } [Fact] @@ -106,7 +107,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers { // Arrange var runner = new TagHelperRunner(); - var executionContext = new TagHelperExecutionContext("p", selfClosing: false); + var executionContext = new TagHelperExecutionContext("p", TagMode.StartTagAndEndTag); var executableTagHelper1 = new ExecutableTagHelper(); var executableTagHelper2 = new ExecutableTagHelper(); @@ -125,7 +126,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers { // Arrange var runner = new TagHelperRunner(); - var executionContext = new TagHelperExecutionContext("p", selfClosing: false); + var executionContext = new TagHelperExecutionContext("p", TagMode.StartTagAndEndTag); var executableTagHelper = new ExecutableTagHelper(); // Act @@ -137,7 +138,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers Assert.Equal("foo", output.TagName); Assert.Equal("somethingelse", output.Attributes["class"].Value); Assert.Equal("world", output.Attributes["hello"].Value); - Assert.Equal(true, output.SelfClosing); + Assert.Equal(TagMode.SelfClosing, output.TagMode); } [Fact] @@ -145,7 +146,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers { // Arrange var runner = new TagHelperRunner(); - var executionContext = new TagHelperExecutionContext("p", selfClosing: false); + var executionContext = new TagHelperExecutionContext("p", TagMode.StartTagAndEndTag); var tagHelper = new TagHelperContextTouchingTagHelper(); // Act @@ -162,7 +163,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers { // Arrange var runner = new TagHelperRunner(); - var executionContext = new TagHelperExecutionContext("p", selfClosing: false); + var executionContext = new TagHelperExecutionContext("p", TagMode.StartTagAndEndTag); var tagHelper = new ContextInspectingTagHelper(); executionContext.Add(tagHelper); @@ -191,7 +192,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers } output.Attributes.Add("hello", "world"); - output.SelfClosing = true; + output.TagMode = TagMode.SelfClosing; } } diff --git a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperScopeManagerTest.cs b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperScopeManagerTest.cs index a582c8f9ab..fefba884b4 100644 --- a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperScopeManagerTest.cs +++ b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperScopeManagerTest.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNet.Razor.Runtime.TagHelpers; +using Microsoft.AspNet.Razor.TagHelpers; using Xunit; namespace Microsoft.AspNet.Razor.Runtime.Test.TagHelpers @@ -152,18 +153,19 @@ namespace Microsoft.AspNet.Razor.Runtime.Test.TagHelpers } [Theory] - [InlineData("true")] - [InlineData("false")] - public void Begin_SetExecutionContextSelfClosing(bool selfClosing) + [InlineData(TagMode.SelfClosing)] + [InlineData(TagMode.StartTagAndEndTag)] + [InlineData(TagMode.StartTagOnly)] + public void Begin_SetsExecutionContextTagMode(TagMode tagMode) { // Arrange var scopeManager = new TagHelperScopeManager(); // Act - var executionContext = BeginDefaultScope(scopeManager, "p", selfClosing); + var executionContext = BeginDefaultScope(scopeManager, "p", tagMode); // Assert - Assert.Equal(selfClosing, executionContext.SelfClosing); + Assert.Equal(tagMode, executionContext.TagMode); } [Fact] @@ -218,13 +220,13 @@ namespace Microsoft.AspNet.Razor.Runtime.Test.TagHelpers } private static TagHelperExecutionContext BeginDefaultScope( - TagHelperScopeManager scopeManager, + TagHelperScopeManager scopeManager, string tagName, - bool selfClosing = false) + TagMode tagMode = TagMode.StartTagAndEndTag) { return scopeManager.Begin( tagName, - selfClosing, + tagMode, uniqueId: string.Empty, executeChildContentAsync: async () => await Task.FromResult(result: true), startTagHelperWritingScope: () => { }, diff --git a/test/Microsoft.AspNet.Razor.Test/CodeGenerators/CSharpTagHelperRenderingTest.cs b/test/Microsoft.AspNet.Razor.Test/CodeGenerators/CSharpTagHelperRenderingTest.cs index e456e5774d..6a3ce59a3f 100644 --- a/test/Microsoft.AspNet.Razor.Test/CodeGenerators/CSharpTagHelperRenderingTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/CodeGenerators/CSharpTagHelperRenderingTest.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Reflection; #endif using Microsoft.AspNet.Razor.CodeGenerators; +using Microsoft.AspNet.Razor.Runtime.TagHelpers; using Microsoft.AspNet.Razor.TagHelpers; using Xunit; @@ -448,7 +449,7 @@ namespace Microsoft.AspNet.Razor.Test.Generator generatedCharacterOffsetIndex: 6, contentLength: 23), BuildLineMapping( - documentAbsoluteIndex: 287, + documentAbsoluteIndex: 285, documentLineIndex: 6, generatedAbsoluteIndex: 1677, generatedLineIndex: 49, @@ -462,24 +463,27 @@ namespace Microsoft.AspNet.Razor.Test.Generator PrefixedPAndInputTagHelperDescriptors, new List { - BuildLineMapping(documentAbsoluteIndex: 17, - documentLineIndex: 0, - generatedAbsoluteIndex: 496, - generatedLineIndex: 15, - characterOffsetIndex: 17, - contentLength: 5), - BuildLineMapping(documentAbsoluteIndex: 38, - documentLineIndex: 1, - generatedAbsoluteIndex: 655, - generatedLineIndex: 22, - characterOffsetIndex: 14, - contentLength: 17), - BuildLineMapping(documentAbsoluteIndex: 228, - documentLineIndex: 7, - generatedAbsoluteIndex: 1480, - generatedLineIndex: 46, - characterOffsetIndex: 43, - contentLength: 4) + BuildLineMapping( + documentAbsoluteIndex: 17, + documentLineIndex: 0, + generatedAbsoluteIndex: 496, + generatedLineIndex: 15, + characterOffsetIndex: 17, + contentLength: 5), + BuildLineMapping( + documentAbsoluteIndex: 38, + documentLineIndex: 1, + generatedAbsoluteIndex: 655, + generatedLineIndex: 22, + characterOffsetIndex: 14, + contentLength: 17), + BuildLineMapping( + documentAbsoluteIndex: 226, + documentLineIndex: 7, + generatedAbsoluteIndex: 1480, + generatedLineIndex: 46, + characterOffsetIndex: 43, + contentLength: 4), } }, { @@ -589,7 +593,7 @@ namespace Microsoft.AspNet.Razor.Test.Generator generatedCharacterOffsetIndex: 0, contentLength: 1), BuildLineMapping( - documentAbsoluteIndex: 645, + documentAbsoluteIndex: 643, documentLineIndex: 18, generatedAbsoluteIndex: 3245, generatedLineIndex: 128, @@ -604,21 +608,21 @@ namespace Microsoft.AspNet.Razor.Test.Generator generatedCharacterOffsetIndex: 6, contentLength: 12), BuildLineMapping( - documentAbsoluteIndex: 771, + documentAbsoluteIndex: 769, documentLineIndex: 21, generatedAbsoluteIndex: 3477, generatedLineIndex: 140, characterOffsetIndex: 0, contentLength: 12), BuildLineMapping( - documentAbsoluteIndex: 785, + documentAbsoluteIndex: 783, documentLineIndex: 21, generatedAbsoluteIndex: 3575, generatedLineIndex: 146, characterOffsetIndex: 14, contentLength: 21), BuildLineMapping( - documentAbsoluteIndex: 839, + documentAbsoluteIndex: 837, documentLineIndex: 22, documentCharacterOffsetIndex: 30, generatedAbsoluteIndex: 3832, @@ -626,7 +630,7 @@ namespace Microsoft.AspNet.Razor.Test.Generator generatedCharacterOffsetIndex: 28, contentLength: 7), BuildLineMapping( - documentAbsoluteIndex: 713, + documentAbsoluteIndex: 711, documentLineIndex: 20, documentCharacterOffsetIndex: 39, generatedAbsoluteIndex: 4007, @@ -634,7 +638,7 @@ namespace Microsoft.AspNet.Razor.Test.Generator generatedCharacterOffsetIndex: 38, contentLength: 23), BuildLineMapping( - documentAbsoluteIndex: 736, + documentAbsoluteIndex: 734, documentLineIndex: 20, documentCharacterOffsetIndex: 62, generatedAbsoluteIndex: 4030, @@ -642,7 +646,7 @@ namespace Microsoft.AspNet.Razor.Test.Generator generatedCharacterOffsetIndex: 61, contentLength: 7), BuildLineMapping( - documentAbsoluteIndex: 981, + documentAbsoluteIndex: 977, documentLineIndex: 25, documentCharacterOffsetIndex: 62, generatedAbsoluteIndex: 4304, @@ -650,7 +654,7 @@ namespace Microsoft.AspNet.Razor.Test.Generator generatedCharacterOffsetIndex: 60, contentLength: 30), BuildLineMapping( - documentAbsoluteIndex: 883, + documentAbsoluteIndex: 879, documentLineIndex: 24, documentCharacterOffsetIndex: 16, generatedAbsoluteIndex: 4483, @@ -658,7 +662,7 @@ namespace Microsoft.AspNet.Razor.Test.Generator generatedCharacterOffsetIndex: 19, contentLength: 8), BuildLineMapping( - documentAbsoluteIndex: 892, + documentAbsoluteIndex: 888, documentLineIndex: 24, documentCharacterOffsetIndex: 25, generatedAbsoluteIndex: 4491, @@ -666,14 +670,14 @@ namespace Microsoft.AspNet.Razor.Test.Generator generatedCharacterOffsetIndex: 27, contentLength: 23), BuildLineMapping( - documentAbsoluteIndex: 1110, + documentAbsoluteIndex: 1106, documentLineIndex: 28, generatedAbsoluteIndex: 4749, generatedLineIndex: 180, characterOffsetIndex: 28, contentLength: 30), BuildLineMapping( - documentAbsoluteIndex: 1048, + documentAbsoluteIndex: 1044, documentLineIndex: 27, documentCharacterOffsetIndex: 16, generatedAbsoluteIndex: 4928, @@ -681,14 +685,14 @@ namespace Microsoft.AspNet.Razor.Test.Generator generatedCharacterOffsetIndex: 19, contentLength: 30), BuildLineMapping( - documentAbsoluteIndex: 1240, + documentAbsoluteIndex: 1234, documentLineIndex: 31, generatedAbsoluteIndex: 5193, generatedLineIndex: 193, characterOffsetIndex: 28, contentLength: 3), BuildLineMapping( - documentAbsoluteIndex: 1245, + documentAbsoluteIndex: 1239, documentLineIndex: 31, documentCharacterOffsetIndex: 33, generatedAbsoluteIndex: 5196, @@ -696,7 +700,7 @@ namespace Microsoft.AspNet.Razor.Test.Generator generatedCharacterOffsetIndex: 31, contentLength: 27), BuildLineMapping( - documentAbsoluteIndex: 1273, + documentAbsoluteIndex: 1267, documentLineIndex: 31, documentCharacterOffsetIndex: 61, generatedAbsoluteIndex: 5223, @@ -704,7 +708,7 @@ namespace Microsoft.AspNet.Razor.Test.Generator generatedCharacterOffsetIndex: 58, contentLength: 10), BuildLineMapping( - documentAbsoluteIndex: 1178, + documentAbsoluteIndex: 1172, documentLineIndex: 30, documentCharacterOffsetIndex: 18, generatedAbsoluteIndex: 5382, @@ -712,7 +716,7 @@ namespace Microsoft.AspNet.Razor.Test.Generator generatedCharacterOffsetIndex: 19, contentLength: 29), BuildLineMapping( - documentAbsoluteIndex: 1315, + documentAbsoluteIndex: 1309, documentLineIndex: 34, generatedAbsoluteIndex: 5482, generatedLineIndex: 204, @@ -1344,6 +1348,7 @@ namespace Microsoft.AspNet.Razor.Test.Generator new TagHelperAttributeDescriptor("age", pAgePropertyInfo) }, requiredAttributes: Enumerable.Empty(), + tagStructure: TagStructure.NormalOrSelfClosing, designTimeDescriptor: null), new TagHelperDescriptor( prefix, @@ -1355,6 +1360,7 @@ namespace Microsoft.AspNet.Razor.Test.Generator new TagHelperAttributeDescriptor("type", inputTypePropertyInfo) }, requiredAttributes: Enumerable.Empty(), + tagStructure: TagStructure.WithoutEndTag, designTimeDescriptor: null), new TagHelperDescriptor( prefix, @@ -1367,6 +1373,7 @@ namespace Microsoft.AspNet.Razor.Test.Generator new TagHelperAttributeDescriptor("checked", checkedPropertyInfo) }, requiredAttributes: Enumerable.Empty(), + tagStructure: TagStructure.Unspecified, designTimeDescriptor: null) }; } diff --git a/test/Microsoft.AspNet.Razor.Test/CodeGenerators/CSharpTagHelperRenderingUnitTest.cs b/test/Microsoft.AspNet.Razor.Test/CodeGenerators/CSharpTagHelperRenderingUnitTest.cs index 6817d68861..238ece84d5 100644 --- a/test/Microsoft.AspNet.Razor.Test/CodeGenerators/CSharpTagHelperRenderingUnitTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/CodeGenerators/CSharpTagHelperRenderingUnitTest.cs @@ -6,6 +6,7 @@ using Microsoft.AspNet.Razor.CodeGenerators; using Microsoft.AspNet.Razor.CodeGenerators.Visitors; using Microsoft.AspNet.Razor.Parser.SyntaxTree; using Microsoft.AspNet.Razor.Parser.TagHelpers; +using Microsoft.AspNet.Razor.Runtime.TagHelpers; using Microsoft.AspNet.Razor.TagHelpers; using Xunit; @@ -169,14 +170,14 @@ namespace Microsoft.AspNet.Razor.Test.Generator { return new TagHelperChunk( tagName, - selfClosing: false, + tagMode: TagMode.StartTagAndEndTag, attributes: new List>(), descriptors: tagHelperDescriptors) { Association = new TagHelperBlock( new TagHelperBlockBuilder( tagName, - selfClosing: false, + tagMode: TagMode.StartTagAndEndTag, attributes: new List>(), children: Enumerable.Empty())), Children = new List(), diff --git a/test/Microsoft.AspNet.Razor.Test/CodeGenerators/TagHelperAttributeValueCodeRendererTest.cs b/test/Microsoft.AspNet.Razor.Test/CodeGenerators/TagHelperAttributeValueCodeRendererTest.cs index 3468b8f679..c5ee79ba70 100644 --- a/test/Microsoft.AspNet.Razor.Test/CodeGenerators/TagHelperAttributeValueCodeRendererTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/CodeGenerators/TagHelperAttributeValueCodeRendererTest.cs @@ -7,6 +7,7 @@ using System.Reflection; #endif using Microsoft.AspNet.Razor.CodeGenerators; using Microsoft.AspNet.Razor.CodeGenerators.Visitors; +using Microsoft.AspNet.Razor.Runtime.TagHelpers; using Microsoft.AspNet.Razor.TagHelpers; using Xunit; @@ -23,21 +24,27 @@ namespace Microsoft.AspNet.Razor.Test.Generator var tagHelperDescriptors = new TagHelperDescriptor[] { new TagHelperDescriptor("p", "PTagHelper", "SomeAssembly"), - new TagHelperDescriptor("input", - "InputTagHelper", - "SomeAssembly", - new TagHelperAttributeDescriptor[] - { - new TagHelperAttributeDescriptor("type", inputTypePropertyInfo) - }), - new TagHelperDescriptor("input", - "InputTagHelper2", - "SomeAssembly", - new TagHelperAttributeDescriptor[] - { - new TagHelperAttributeDescriptor("type", inputTypePropertyInfo), - new TagHelperAttributeDescriptor("checked", checkedPropertyInfo) - }) + new TagHelperDescriptor( + prefix: string.Empty, + tagName: "input", + typeName: "InputTagHelper", + assemblyName: "SomeAssembly", + attributes: new TagHelperAttributeDescriptor[] + { + new TagHelperAttributeDescriptor("type", inputTypePropertyInfo) + }, + requiredAttributes: new string[0], + tagStructure: TagStructure.WithoutEndTag, + designTimeDescriptor: null), + new TagHelperDescriptor( + tagName: "input", + typeName: "InputTagHelper2", + assemblyName: "SomeAssembly", + attributes: new TagHelperAttributeDescriptor[] + { + new TagHelperAttributeDescriptor("type", inputTypePropertyInfo), + new TagHelperAttributeDescriptor("checked", checkedPropertyInfo) + }) }; // Act & Assert diff --git a/test/Microsoft.AspNet.Razor.Test/Framework/BlockTypes.cs b/test/Microsoft.AspNet.Razor.Test/Framework/BlockTypes.cs index 99418bc3b7..454b451f75 100644 --- a/test/Microsoft.AspNet.Razor.Test/Framework/BlockTypes.cs +++ b/test/Microsoft.AspNet.Razor.Test/Framework/BlockTypes.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using Microsoft.AspNet.Razor.Chunks.Generators; using Microsoft.AspNet.Razor.Parser.SyntaxTree; using Microsoft.AspNet.Razor.Parser.TagHelpers; +using Microsoft.AspNet.Razor.Runtime.TagHelpers; namespace Microsoft.AspNet.Razor.Test.Framework { @@ -156,41 +157,41 @@ namespace Microsoft.AspNet.Razor.Test.Framework public class MarkupTagHelperBlock : TagHelperBlock { public MarkupTagHelperBlock(string tagName) - : this(tagName, selfClosing: false, attributes: new List>()) + : this(tagName, tagMode: TagMode.StartTagAndEndTag, attributes: new List>()) { } - public MarkupTagHelperBlock(string tagName, bool selfClosing) - : this(tagName, selfClosing, new List>()) + public MarkupTagHelperBlock(string tagName, TagMode tagMode) + : this(tagName, tagMode, new List>()) { } public MarkupTagHelperBlock( string tagName, IList> attributes) - : this(tagName, selfClosing: false, attributes: attributes, children: new SyntaxTreeNode[0]) + : this(tagName, TagMode.StartTagAndEndTag, attributes, children: new SyntaxTreeNode[0]) { } public MarkupTagHelperBlock( string tagName, - bool selfClosing, + TagMode tagMode, IList> attributes) - : this(tagName, selfClosing, attributes, new SyntaxTreeNode[0]) + : this(tagName, tagMode, attributes, new SyntaxTreeNode[0]) { } public MarkupTagHelperBlock(string tagName, params SyntaxTreeNode[] children) : this( tagName, - selfClosing: false, + TagMode.StartTagAndEndTag, attributes: new List>(), children: children) { } - public MarkupTagHelperBlock(string tagName, bool selfClosing, params SyntaxTreeNode[] children) - : this(tagName, selfClosing, new List>(), children) + public MarkupTagHelperBlock(string tagName, TagMode tagMode, params SyntaxTreeNode[] children) + : this(tagName, tagMode, new List>(), children) { } @@ -198,16 +199,20 @@ namespace Microsoft.AspNet.Razor.Test.Framework string tagName, IList> attributes, params SyntaxTreeNode[] children) - : base(new TagHelperBlockBuilder(tagName, selfClosing: false, attributes: attributes, children: children)) + : base(new TagHelperBlockBuilder( + tagName, + TagMode.StartTagAndEndTag, + attributes: attributes, + children: children)) { } public MarkupTagHelperBlock( string tagName, - bool selfClosing, + TagMode tagMode, IList> attributes, params SyntaxTreeNode[] children) - : base(new TagHelperBlockBuilder(tagName, selfClosing, attributes, children)) + : base(new TagHelperBlockBuilder(tagName, tagMode, attributes, children)) { } } diff --git a/test/Microsoft.AspNet.Razor.Test/Framework/ParserTestBase.cs b/test/Microsoft.AspNet.Razor.Test/Framework/ParserTestBase.cs index a3d8ddd1fc..a0670603ed 100644 --- a/test/Microsoft.AspNet.Razor.Test/Framework/ParserTestBase.cs +++ b/test/Microsoft.AspNet.Razor.Test/Framework/ParserTestBase.cs @@ -10,6 +10,7 @@ using Microsoft.AspNet.Razor.Chunks.Generators; using Microsoft.AspNet.Razor.Parser; using Microsoft.AspNet.Razor.Parser.SyntaxTree; using Microsoft.AspNet.Razor.Parser.TagHelpers; +using Microsoft.AspNet.Razor.Runtime.TagHelpers; using Microsoft.AspNet.Razor.Text; using Xunit; @@ -411,13 +412,11 @@ namespace Microsoft.AspNet.Razor.Test.Framework actual.TagName); } - if (expected.SelfClosing != actual.SelfClosing) + if (expected.TagMode != actual.TagMode) { collector.AddError( - "{0} - FAILED :: SelfClosing for TagHelperBlock {1} :: ACTUAL: {2}", - expected.SelfClosing, - actual.TagName, - actual.SelfClosing); + $"{expected.TagMode} - FAILED :: {nameof(TagMode)} for {nameof(TagHelperBlock)} " + + $"{actual.TagName} :: ACTUAL: {actual.TagMode}"); } var expectedAttributes = expected.Attributes.GetEnumerator(); diff --git a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperBlockRewriterTest.cs b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperBlockRewriterTest.cs index 95e2262733..908fa4140f 100644 --- a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperBlockRewriterTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperBlockRewriterTest.cs @@ -8,6 +8,7 @@ using System.Linq; using Microsoft.AspNet.Razor.Chunks.Generators; using Microsoft.AspNet.Razor.Parser; using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Runtime.TagHelpers; using Microsoft.AspNet.Razor.Test.Framework; using Microsoft.AspNet.Razor.Test.TagHelpers; using Microsoft.AspNet.Razor.Text; @@ -17,6 +18,195 @@ namespace Microsoft.AspNet.Razor.TagHelpers { public class TagHelperBlockRewriterTest : TagHelperRewritingTestBase { + public static TheoryData WithoutEndTagElementData + { + get + { + var factory = CreateDefaultSpanFactory(); + var blockFactory = new BlockFactory(factory); + + // documentContent, expectedOutput + return new TheoryData + { + { + "", + new MarkupBlock(new MarkupTagHelperBlock("input", TagMode.StartTagOnly)) + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "input", + TagMode.StartTagOnly, + attributes: new List> + { + new KeyValuePair("type", factory.Markup("text")) + })) + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock("input", TagMode.StartTagOnly), + new MarkupTagHelperBlock("input", TagMode.StartTagOnly)) + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "input", + TagMode.StartTagOnly, + attributes: new List> + { + new KeyValuePair("type", factory.Markup("text")) + }), + new MarkupTagHelperBlock("input", TagMode.StartTagOnly)) + }, + { + "
", + new MarkupBlock( + blockFactory.MarkupTagBlock("
"), + new MarkupTagHelperBlock("input", TagMode.StartTagOnly), + new MarkupTagHelperBlock("input", TagMode.StartTagOnly), + blockFactory.MarkupTagBlock("
")) + }, + }; + } + } + + [Theory] + [MemberData(nameof(WithoutEndTagElementData))] + public void Rewrite_CanHandleWithoutEndTagTagStructure(string documentContent, MarkupBlock expectedOutput) + { + // Arrange + var descriptors = new TagHelperDescriptor[] + { + new TagHelperDescriptor( + prefix: string.Empty, + tagName: "input", + typeName: "InputTagHelper", + assemblyName: "SomeAssembly", + attributes: new TagHelperAttributeDescriptor[0], + requiredAttributes: Enumerable.Empty(), + tagStructure: TagStructure.WithoutEndTag, + designTimeDescriptor: null) + }; + var descriptorProvider = new TagHelperDescriptorProvider(descriptors); + + // Act & Assert + EvaluateData(descriptorProvider, documentContent, expectedOutput, expectedErrors: new RazorError[0]); + } + + public static TheoryData TagStructureCompatibilityData + { + get + { + var factory = CreateDefaultSpanFactory(); + var blockFactory = new BlockFactory(factory); + + // documentContent, structure1, structure2, expectedOutput + return new TheoryData + { + { + "", + TagStructure.Unspecified, + TagStructure.Unspecified, + new MarkupBlock(new MarkupTagHelperBlock("input", TagMode.StartTagAndEndTag)) + }, + { + "", + TagStructure.Unspecified, + TagStructure.Unspecified, + new MarkupBlock(new MarkupTagHelperBlock("input", TagMode.SelfClosing)) + }, + { + "", + TagStructure.Unspecified, + TagStructure.WithoutEndTag, + new MarkupBlock( + new MarkupTagHelperBlock( + "input", + TagMode.StartTagOnly, + attributes: new List> + { + new KeyValuePair("type", factory.Markup("text")) + })) + }, + { + "", + TagStructure.WithoutEndTag, + TagStructure.WithoutEndTag, + new MarkupBlock( + new MarkupTagHelperBlock("input", TagMode.StartTagOnly), + new MarkupTagHelperBlock("input", TagMode.StartTagOnly)) + }, + { + "", + TagStructure.Unspecified, + TagStructure.NormalOrSelfClosing, + new MarkupBlock( + new MarkupTagHelperBlock( + "input", + TagMode.StartTagAndEndTag, + attributes: new List> + { + new KeyValuePair("type", factory.Markup("text")) + })) + }, + { + "", + TagStructure.Unspecified, + TagStructure.WithoutEndTag, + new MarkupBlock(new MarkupTagHelperBlock("input", TagMode.SelfClosing)) + }, + + { + "", + TagStructure.NormalOrSelfClosing, + TagStructure.Unspecified, + new MarkupBlock(new MarkupTagHelperBlock("input", TagMode.SelfClosing)) + }, + }; + } + } + + [Theory] + [MemberData(nameof(TagStructureCompatibilityData))] + public void Rewrite_AllowsCompatibleTagStructures( + string documentContent, + TagStructure structure1, + TagStructure structure2, + MarkupBlock expectedOutput) + { + // Arrange + var factory = CreateDefaultSpanFactory(); + var blockFactory = new BlockFactory(factory); + var descriptors = new TagHelperDescriptor[] + { + new TagHelperDescriptor( + prefix: string.Empty, + tagName: "input", + typeName: "InputTagHelper1", + assemblyName: "SomeAssembly", + attributes: new TagHelperAttributeDescriptor[0], + requiredAttributes: Enumerable.Empty(), + tagStructure: structure1, + designTimeDescriptor: null), + new TagHelperDescriptor( + prefix: string.Empty, + tagName: "input", + typeName: "InputTagHelper2", + assemblyName: "SomeAssembly", + attributes: new TagHelperAttributeDescriptor[0], + requiredAttributes: Enumerable.Empty(), + tagStructure: structure2, + designTimeDescriptor: null) + }; + var descriptorProvider = new TagHelperDescriptorProvider(descriptors); + + // Act & Assert + EvaluateData(descriptorProvider, documentContent, expectedOutput, expectedErrors: new RazorError[0]); + } + public static TheoryData MalformedTagHelperAttributeBlockData { get @@ -574,7 +764,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers "", new MarkupBlock( new MarkupTagHelperBlock("person", - selfClosing: true, + TagMode.SelfClosing, attributes: new List> { new KeyValuePair("age", factory.CodeMarkup("12")) @@ -584,7 +774,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers "", new MarkupBlock( new MarkupTagHelperBlock("person", - selfClosing: true, + TagMode.SelfClosing, attributes: new List> { new KeyValuePair( @@ -596,7 +786,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers "", new MarkupBlock( new MarkupTagHelperBlock("person", - selfClosing: true, + TagMode.SelfClosing, attributes: new List> { new KeyValuePair("name", factory.Markup("John")) @@ -606,7 +796,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers "", new MarkupBlock( new MarkupTagHelperBlock("person", - selfClosing: true, + TagMode.SelfClosing, attributes: new List> { new KeyValuePair( @@ -618,7 +808,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers "", new MarkupBlock( new MarkupTagHelperBlock("person", - selfClosing: true, + TagMode.SelfClosing, attributes: new List> { new KeyValuePair( @@ -670,7 +860,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers "", new MarkupBlock( new MarkupTagHelperBlock("person", - selfClosing: true, + TagMode.SelfClosing, attributes: new List> { new KeyValuePair("age", factory.CodeMarkup("12")), @@ -686,7 +876,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers "", new MarkupBlock( new MarkupTagHelperBlock("person", - selfClosing: true, + TagMode.SelfClosing, attributes: new List> { new KeyValuePair("age", factory.CodeMarkup("12")), @@ -709,7 +899,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers "", new MarkupBlock( new MarkupTagHelperBlock("person", - selfClosing: true, + TagMode.SelfClosing, attributes: new List> { new KeyValuePair("age", factory.CodeMarkup("12")), @@ -731,7 +921,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers "", new MarkupBlock( new MarkupTagHelperBlock("person", - selfClosing: true, + TagMode.SelfClosing, attributes: new List> { new KeyValuePair( @@ -1324,7 +1514,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers "<

", new MarkupBlock( blockFactory.MarkupTagBlock("<"), - new MarkupTagHelperBlock("p", selfClosing: true)) + new MarkupTagHelperBlock("p", TagMode.SelfClosing)) }, { "< p />", @@ -1339,7 +1529,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers "", new MarkupBlock( blockFactory.MarkupTagBlock("", @@ -1357,7 +1547,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers value: new LocationTagged("foo", 9, 0, 9))), factory.Markup("\"").With(SpanChunkGenerator.Null)), factory.Markup(" ")), - new MarkupTagHelperBlock("p", selfClosing: true)) + new MarkupTagHelperBlock("p", TagMode.SelfClosing)) }, { "/>

>", @@ -1468,7 +1658,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers "

", new MarkupBlock( new MarkupTagHelperBlock("p", - selfClosing: true, + TagMode.SelfClosing, attributes: new List> { new KeyValuePair("class1", new MarkupBlock()), @@ -1482,7 +1672,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers "

", new MarkupBlock( new MarkupTagHelperBlock("p", - selfClosing: true, + TagMode.SelfClosing, attributes: new List> { new KeyValuePair("class1", new MarkupBlock()), @@ -1521,7 +1711,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers new MarkupBlock( new MarkupTagHelperBlock( "myth", - selfClosing: true, + TagMode.SelfClosing, attributes: new List> { new KeyValuePair("bound", new MarkupBlock()) @@ -1538,7 +1728,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers new MarkupBlock( new MarkupTagHelperBlock( "myth", - selfClosing: true, + TagMode.SelfClosing, attributes: new List> { new KeyValuePair("bound", factory.CodeMarkup(" true")) @@ -1550,7 +1740,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers new MarkupBlock( new MarkupTagHelperBlock( "myth", - selfClosing: true, + TagMode.SelfClosing, attributes: new List> { new KeyValuePair("bound", factory.CodeMarkup(" ")) @@ -1567,7 +1757,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers new MarkupBlock( new MarkupTagHelperBlock( "myth", - selfClosing: true, + TagMode.SelfClosing, attributes: new List> { new KeyValuePair("bound", new MarkupBlock()), @@ -1588,7 +1778,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers new MarkupBlock( new MarkupTagHelperBlock( "myth", - selfClosing: true, + TagMode.SelfClosing, attributes: new List> { new KeyValuePair("bound", factory.CodeMarkup(" ")), @@ -1609,7 +1799,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers new MarkupBlock( new MarkupTagHelperBlock( "myth", - selfClosing: true, + TagMode.SelfClosing, attributes: new List> { new KeyValuePair("bound", factory.CodeMarkup("true")), @@ -1629,7 +1819,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers new MarkupBlock( new MarkupTagHelperBlock( "myth", - selfClosing: true, + TagMode.SelfClosing, attributes: new List> { new KeyValuePair( @@ -1649,7 +1839,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers new MarkupBlock( new MarkupTagHelperBlock( "myth", - selfClosing: true, + TagMode.SelfClosing, attributes: new List> { new KeyValuePair( @@ -1669,7 +1859,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers new MarkupBlock( new MarkupTagHelperBlock( "myth", - selfClosing: true, + TagMode.SelfClosing, attributes: new List> { new KeyValuePair("bound", factory.CodeMarkup("true")), @@ -1693,7 +1883,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers new MarkupBlock( new MarkupTagHelperBlock( "myth", - selfClosing: true, + TagMode.SelfClosing, attributes: new List> { new KeyValuePair("BouND", new MarkupBlock()) @@ -1710,7 +1900,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers new MarkupBlock( new MarkupTagHelperBlock( "myth", - selfClosing: true, + TagMode.SelfClosing, attributes: new List> { new KeyValuePair("BOUND", new MarkupBlock()), @@ -1750,7 +1940,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers new MarkupBlock( new MarkupTagHelperBlock( "myth", - selfClosing: true, + TagMode.SelfClosing, attributes: new List> { { @@ -1774,7 +1964,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers new MarkupBlock( new MarkupTagHelperBlock( "myth", - selfClosing: true, + TagMode.SelfClosing, attributes: new List> { { @@ -1883,7 +2073,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers "