diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/LinkTagHelper.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/LinkTagHelper.cs index f4342c0411..28d21b585f 100644 --- a/src/Microsoft.AspNet.Mvc.TagHelpers/LinkTagHelper.cs +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/LinkTagHelper.cs @@ -221,23 +221,35 @@ namespace Microsoft.AspNet.Mvc.TagHelpers return; } - // Get the highest matched mode - var mode = modeResult.FullMatches.Select(match => match.Mode).Max(); - // NOTE: Values in TagHelperOutput.Attributes may already be HTML-encoded. var attributes = new TagHelperAttributeList(output.Attributes); + if (AppendVersion == true) + { + EnsureFileVersionProvider(); + + var attributeStringValue = output.Attributes[HrefAttributeName]?.Value as string; + if (attributeStringValue != null) + { + output.Attributes[HrefAttributeName].Value = + _fileVersionProvider.AddFileVersionToPath(attributeStringValue); + } + } + var builder = new DefaultTagHelperContent(); - if (mode == Mode.Fallback && string.IsNullOrEmpty(HrefInclude) || mode == Mode.AppendVersion) - { - // No globbing to do, just build a tag to match the original one in the source file. - // Or just add file version to the link tag. - BuildLinkTag(attributes, builder); - } - else + // Get the highest matched mode + var mode = modeResult.FullMatches.Select(match => match.Mode).Max(); + + if (mode == Mode.GlobbedHref || mode == Mode.Fallback && !string.IsNullOrEmpty(HrefInclude)) { BuildGlobbedLinkTags(attributes, builder); + if (string.IsNullOrEmpty(Href)) + { + // Only HrefInclude is specified. Don't render the original tag. + output.TagName = null; + output.Content.SetContent(string.Empty); + } } if (mode == Mode.Fallback) @@ -245,22 +257,26 @@ namespace Microsoft.AspNet.Mvc.TagHelpers BuildFallbackBlock(builder); } - // We've taken over tag rendering, so prevent rendering the outer tag - output.TagName = null; - output.Content.SetContent(builder); + output.PostElement.SetContent(builder); } private void BuildGlobbedLinkTags(TagHelperAttributeList attributes, TagHelperContent builder) { EnsureGlobbingUrlBuilder(); - // Build a tag for each matched href as well as the original one in the source file - var urls = GlobbingUrlBuilder.BuildUrlList(Href, HrefInclude, HrefExclude); + // Build a tag for each matched href. + var urls = GlobbingUrlBuilder.BuildUrlList(null, HrefInclude, HrefExclude); foreach (var url in urls) { // "url" values come from bound attributes and globbing. Must always be non-null. Debug.Assert(url != null); + if (string.Equals(Href, url, StringComparison.OrdinalIgnoreCase)) + { + // Don't build duplicate link tag for the original href url. + continue; + } + attributes[HrefAttributeName] = url; BuildLinkTag(attributes, builder); } @@ -332,7 +348,6 @@ namespace Microsoft.AspNet.Mvc.TagHelpers private void BuildLinkTag(TagHelperAttributeList attributes, TagHelperContent builder) { - EnsureFileVersionProvider(); builder.Append(" - public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) + public override void Process(TagHelperContext context, TagHelperOutput output) { // Pass through attribute that is also a well-known HTML attribute. if (Src != null) @@ -190,24 +190,35 @@ namespace Microsoft.AspNet.Mvc.TagHelpers return; } - // Get the highest matched mode - var mode = modeResult.FullMatches.Select(match => match.Mode).Max(); - // NOTE: Values in TagHelperOutput.Attributes may already be HTML-encoded. var attributes = new TagHelperAttributeList(output.Attributes); - var builder = new DefaultTagHelperContent(); - var originalContent = await context.GetChildContentAsync(); + if (AppendVersion == true) + { + EnsureFileVersionProvider(); - if (mode == Mode.Fallback && string.IsNullOrEmpty(SrcInclude) || mode == Mode.AppendVersion) - { - // No globbing to do, just build a "); + builder.Append(">"); } private void AppendAttribute(TagHelperContent content, string key, object value, bool escapeQuotes) diff --git a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/LinkTagHelperTest.cs b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/LinkTagHelperTest.cs index 29406a39aa..804313b2e9 100644 --- a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/LinkTagHelperTest.cs +++ b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/LinkTagHelperTest.cs @@ -32,16 +32,15 @@ namespace Microsoft.AspNet.Mvc.TagHelpers { get { - // outputAttributes, expectedAttributeString - return new TheoryData + // outputAttributes + return new TheoryData { { new TagHelperAttributeList { { "hello", "world" }, { "hello", "world2" } - }, - "hello=\"HtmlEncode[[world]]\" hello=\"HtmlEncode[[world2]]\"" + } }, { new TagHelperAttributeList @@ -49,16 +48,14 @@ namespace Microsoft.AspNet.Mvc.TagHelpers { "hello", "world" }, { "hello", "world2" }, { "hello", "world3" } - }, - "hello=\"HtmlEncode[[world]]\" hello=\"HtmlEncode[[world2]]\" hello=\"HtmlEncode[[world3]]\"" + } }, { new TagHelperAttributeList { { "HelLO", "world" }, { "HELLO", "world2" } - }, - "HelLO=\"HtmlEncode[[world]]\" HELLO=\"HtmlEncode[[world2]]\"" + } }, { new TagHelperAttributeList @@ -66,16 +63,14 @@ namespace Microsoft.AspNet.Mvc.TagHelpers { "Hello", "world" }, { "HELLO", "world2" }, { "hello", "world3" } - }, - "Hello=\"HtmlEncode[[world]]\" HELLO=\"HtmlEncode[[world2]]\" hello=\"HtmlEncode[[world3]]\"" + } }, { new TagHelperAttributeList { { "HeLlO", "world" }, { "hello", "world2" } - }, - "HeLlO=\"HtmlEncode[[world]]\" hello=\"HtmlEncode[[world2]]\"" + } }, }; } @@ -83,9 +78,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers [Theory] [MemberData(nameof(MultiAttributeSameNameData))] - public void HandlesMultipleAttributesSameNameCorrectly( - TagHelperAttributeList outputAttributes, - string expectedAttributeString) + public void HandlesMultipleAttributesSameNameCorrectly(TagHelperAttributeList outputAttributes) { // Arrange var allAttributes = new TagHelperAttributeList( @@ -125,12 +118,14 @@ namespace Microsoft.AspNet.Mvc.TagHelpers FallbackTestValue = "hidden", Href = "test.css", }; + var expectedAttributes = new TagHelperAttributeList(output.Attributes); + expectedAttributes.Add(new TagHelperAttribute("href", "test.css")); // Act helper.Process(context, output); // Assert - Assert.StartsWith("> { - { - new TagHelperAttributeList - { - ["asp-href-include"] = "*.css" - }, - tagHelper => - { - tagHelper.HrefInclude = "*.css"; - } - }, - { - new TagHelperAttributeList - { - ["asp-href-include"] = "*.css", - ["asp-href-exclude"] = "*.min.css" - }, - tagHelper => - { - tagHelper.HrefInclude = "*.css"; - tagHelper.HrefExclude = "*.min.css"; - } - }, { new TagHelperAttributeList { @@ -194,42 +167,6 @@ namespace Microsoft.AspNet.Mvc.TagHelpers } }, // File Version - { - new TagHelperAttributeList - { - ["asp-append-version"] = "true" - }, - tagHelper => - { - tagHelper.AppendVersion = true; - } - }, - { - new TagHelperAttributeList - { - ["asp-href-include"] = "*.css", - ["asp-append-version"] = "true" - }, - tagHelper => - { - tagHelper.HrefInclude = "*.css"; - tagHelper.AppendVersion = true; - } - }, - { - new TagHelperAttributeList - { - ["asp-href-include"] = "*.css", - ["asp-href-exclude"] = "*.min.css", - ["asp-append-version"] = "true" - }, - tagHelper => - { - tagHelper.HrefInclude = "*.css"; - tagHelper.HrefExclude = "*.min.css"; - tagHelper.AppendVersion = true; - } - }, { new TagHelperAttributeList { @@ -282,6 +219,9 @@ namespace Microsoft.AspNet.Mvc.TagHelpers var logger = new Mock>(); var hostingEnvironment = MakeHostingEnvironment(); var viewContext = MakeViewContext(); + var globbingUrlBuilder = new Mock(); + globbingUrlBuilder.Setup(g => g.BuildUrlList(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(new[] { "/common.css" }); var helper = new LinkTagHelper( logger.Object, @@ -291,6 +231,102 @@ namespace Microsoft.AspNet.Mvc.TagHelpers new CommonTestEncoder()) { ViewContext = viewContext, + GlobbingUrlBuilder = globbingUrlBuilder.Object + }; + setProperties(helper); + + // Act + helper.Process(context, output); + + // Assert + Assert.NotNull(output.TagName); + Assert.False(output.IsContentModified); + Assert.True(output.PostElement.IsModified); + } + + public static TheoryData RunsWhenRequiredAttributesArePresent_NoHref_Data + { + get + { + return new TheoryData> + { + { + new TagHelperAttributeList + { + ["asp-href-include"] = "*.css" + }, + tagHelper => + { + tagHelper.HrefInclude = "*.css"; + } + }, + { + new TagHelperAttributeList + { + ["asp-href-include"] = "*.css", + ["asp-href-exclude"] = "*.min.css" + }, + tagHelper => + { + tagHelper.HrefInclude = "*.css"; + tagHelper.HrefExclude = "*.min.css"; + } + }, + { + new TagHelperAttributeList + { + ["asp-href-include"] = "*.css", + ["asp-append-version"] = "true" + }, + tagHelper => + { + tagHelper.HrefInclude = "*.css"; + tagHelper.AppendVersion = true; + } + }, + { + new TagHelperAttributeList + { + ["asp-href-include"] = "*.css", + ["asp-href-exclude"] = "*.min.css", + ["asp-append-version"] = "true" + }, + tagHelper => + { + tagHelper.HrefInclude = "*.css"; + tagHelper.HrefExclude = "*.min.css"; + tagHelper.AppendVersion = true; + } + } + }; + } + } + + [Theory] + [MemberData(nameof(RunsWhenRequiredAttributesArePresent_NoHref_Data))] + public void RunsWhenRequiredAttributesArePresent_NoHref( + TagHelperAttributeList attributes, + Action setProperties) + { + // Arrange + var context = MakeTagHelperContext(attributes); + var output = MakeTagHelperOutput("link"); + var logger = new Mock>(); + var hostingEnvironment = MakeHostingEnvironment(); + var viewContext = MakeViewContext(); + var globbingUrlBuilder = new Mock(); + globbingUrlBuilder.Setup(g => g.BuildUrlList(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(new[] { "/common.css" }); + + var helper = new LinkTagHelper( + logger.Object, + hostingEnvironment, + MakeCache(), + new CommonTestEncoder(), + new CommonTestEncoder()) + { + ViewContext = viewContext, + GlobbingUrlBuilder = globbingUrlBuilder.Object }; setProperties(helper); @@ -299,20 +335,20 @@ namespace Microsoft.AspNet.Mvc.TagHelpers // Assert Assert.Null(output.TagName); - Assert.NotNull(output.Content); Assert.True(output.IsContentModified); + Assert.True(output.PostElement.IsModified); } [Fact] - public void PreservesOrderOfSourceAttributesWhenRun() + public void PreservesOrderOfNonHrefAttributes() { // Arrange var context = MakeTagHelperContext( attributes: new TagHelperAttributeList { { "rel", new HtmlString("stylesheet") }, - { "data-extra", new HtmlString("something") }, { "href", "test.css" }, + { "data-extra", new HtmlString("something") }, { "asp-fallback-href", "test.css" }, { "asp-fallback-test-class", "hidden" }, { "asp-fallback-test-property", "visibility" }, @@ -347,8 +383,9 @@ namespace Microsoft.AspNet.Mvc.TagHelpers helper.Process(context, output); // Assert - Assert.StartsWith( - "(); - globbingUrlBuilder.Setup(g => g.BuildUrlList("/css/site.css", "**/*.css", null)) - .Returns(new[] { "/css/site.css", "/base.css" }); + globbingUrlBuilder.Setup(g => g.BuildUrlList(null, "**/*.css", null)) + .Returns(new[] { "/base.css" }); var helper = new LinkTagHelper( logger.Object, @@ -522,10 +563,9 @@ namespace Microsoft.AspNet.Mvc.TagHelpers helper.Process(context, output); // Assert - Assert.Equal( - "" + - "", - output.Content.GetContent()); + Assert.Equal("link", output.TagName); + Assert.Equal("/css/site.css", output.Attributes["href"].Value); + Assert.Equal("", output.PostElement.GetContent()); } [Fact] @@ -547,8 +587,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers var hostingEnvironment = MakeHostingEnvironment(); var viewContext = MakeViewContext(); var globbingUrlBuilder = new Mock(); - globbingUrlBuilder.Setup(g => g.BuildUrlList("/css/site.css", "**/*.css", null)) - .Returns(new[] { "/css/site.css", "/base.css" }); + globbingUrlBuilder.Setup(g => g.BuildUrlList(null, "**/*.css", null)) + .Returns(new[] { "/base.css" }); var helper = new LinkTagHelper( logger.Object, @@ -567,10 +607,10 @@ namespace Microsoft.AspNet.Mvc.TagHelpers helper.Process(context, output); // Assert - Assert.Equal( - "" + - "", - output.Content.GetContent()); + Assert.Equal("link", output.TagName); + Assert.Equal("/css/site.css", output.Attributes["href"].Value); + Assert.Equal("", + output.PostElement.GetContent()); } [ConditionalTheory] @@ -603,7 +643,6 @@ namespace Microsoft.AspNet.Mvc.TagHelpers { ViewContext = viewContext, Href = "/css/site.css", - HrefInclude = "**/*.css", AppendVersion = true }; @@ -611,9 +650,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers helper.Process(context, output); // Assert - Assert.Equal( - "", - output.Content.GetContent()); + Assert.Equal("link", output.TagName); + Assert.Equal("/css/site.css?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk", output.Attributes["href"].Value); } [ConditionalTheory] @@ -646,7 +684,6 @@ namespace Microsoft.AspNet.Mvc.TagHelpers { ViewContext = viewContext, Href = "/bar/css/site.css", - HrefInclude = "**/*.css", AppendVersion = true }; @@ -654,9 +691,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers helper.Process(context, output); // Assert - Assert.Equal( - "", - output.Content.GetContent()); + Assert.Equal("link", output.TagName); + Assert.Equal("/bar/css/site.css?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk", output.Attributes["href"].Value); } [ConditionalTheory] @@ -681,8 +717,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers var hostingEnvironment = MakeHostingEnvironment(); var viewContext = MakeViewContext(); var globbingUrlBuilder = new Mock(); - globbingUrlBuilder.Setup(g => g.BuildUrlList("/css/site.css", "**/*.css", null)) - .Returns(new[] { "/css/site.css", "/base.css" }); + globbingUrlBuilder.Setup(g => g.BuildUrlList(null, "**/*.css", null)) + .Returns(new[] { "/base.css" }); var helper = new LinkTagHelper( logger.Object, @@ -702,10 +738,10 @@ namespace Microsoft.AspNet.Mvc.TagHelpers helper.Process(context, output); // Assert - Assert.Equal( - "" + - "", - output.Content.GetContent()); + Assert.Equal("link", output.TagName); + Assert.Equal("/css/site.css?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk", output.Attributes["href"].Value); + Assert.Equal("", + output.PostElement.GetContent()); } private static ViewContext MakeViewContext(string requestPathBase = null) diff --git a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/ScriptTagHelperTest.cs b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/ScriptTagHelperTest.cs index 34238c53be..446d2de471 100644 --- a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/ScriptTagHelperTest.cs +++ b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/ScriptTagHelperTest.cs @@ -31,9 +31,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers { [Theory] [MemberData(nameof(LinkTagHelperTest.MultiAttributeSameNameData), MemberType = typeof(LinkTagHelperTest))] - public async Task HandlesMultipleAttributesSameNameCorrectly( - TagHelperAttributeList outputAttributes, - string expectedAttributeString) + public async Task HandlesMultipleAttributesSameNameCorrectly(TagHelperAttributeList outputAttributes) { // Arrange var allAttributes = new TagHelperAttributeList( @@ -68,15 +66,14 @@ namespace Microsoft.AspNet.Mvc.TagHelpers FallbackTestExpression = "http://www.example.com/blank.js", Src = "/blank.js", }; + var expectedAttributes = new TagHelperAttributeList(output.Attributes); + expectedAttributes.Add(new TagHelperAttribute("src", "/blank.js")); // Act await helper.ProcessAsync(tagHelperContext, output); // Assert - Assert.StartsWith( - "" + - "", output.Content.GetContent()); + Assert.Equal("script", output.TagName); + Assert.Equal("/js/site.js", output.Attributes["src"].Value); + Assert.Equal("", output.PostElement.GetContent()); } [Fact] @@ -594,8 +641,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers var hostingEnvironment = MakeHostingEnvironment(); var viewContext = MakeViewContext(); var globbingUrlBuilder = new Mock(); - globbingUrlBuilder.Setup(g => g.BuildUrlList("/js/site.js", "**/*.js", null)) - .Returns(new[] { "/js/site.js", "/common.js" }); + globbingUrlBuilder.Setup(g => g.BuildUrlList(null, "**/*.js", null)) + .Returns(new[] { "/common.js" }); var helper = new ScriptTagHelper( logger.Object, @@ -614,8 +661,9 @@ namespace Microsoft.AspNet.Mvc.TagHelpers await helper.ProcessAsync(context, output); // Assert - Assert.Equal("" + - "", output.Content.GetContent()); + Assert.Equal("script", output.TagName); + Assert.Equal("/js/site.js", output.Attributes["src"].Value); + Assert.Equal("", output.PostElement.GetContent()); } [ConditionalTheory] @@ -652,9 +700,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers await helper.ProcessAsync(context, output); // Assert - Assert.Equal( - "", output.Content.GetContent()); + Assert.Equal("script", output.TagName); + Assert.Equal("/js/site.js?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk", output.Attributes["src"].Value); } [ConditionalTheory] @@ -691,9 +738,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers await helper.ProcessAsync(context, output); // Assert - Assert.Equal( - "", output.Content.GetContent()); + Assert.Equal("script", output.TagName); + Assert.Equal("/bar/js/site.js?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk", output.Attributes["src"].Value); } [ConditionalTheory] @@ -734,12 +780,11 @@ namespace Microsoft.AspNet.Mvc.TagHelpers await helper.ProcessAsync(context, output); // Assert - Assert.Equal( - "{Environment.NewLine}" + - "", - output.Content.GetContent()); + Assert.Equal("script", output.TagName); + Assert.Equal("/js/site.js?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk", output.Attributes["src"].Value); + Assert.Equal(Environment.NewLine + "", output.PostElement.GetContent()); } [ConditionalTheory] @@ -760,8 +805,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers var hostingEnvironment = MakeHostingEnvironment(); var viewContext = MakeViewContext(); var globbingUrlBuilder = new Mock(); - globbingUrlBuilder.Setup(g => g.BuildUrlList("/js/site.js", "*.js", null)) - .Returns(new[] { "/js/site.js", "/common.js" }); + globbingUrlBuilder.Setup(g => g.BuildUrlList(null, "*.js", null)) + .Returns(new[] { "/common.js" }); var helper = new ScriptTagHelper( logger.Object, @@ -781,9 +826,10 @@ namespace Microsoft.AspNet.Mvc.TagHelpers await helper.ProcessAsync(context, output); // Assert - Assert.Equal("", output.Content.GetContent()); + Assert.Equal("script", output.TagName); + Assert.Equal("/js/site.js?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk", output.Attributes["src"].Value); + Assert.Equal("", output.PostElement.GetContent()); } private TagHelperContext MakeTagHelperContext(