From 602b597ca3a3157df3f23eef427b5c9cd23bdf0b Mon Sep 17 00:00:00 2001 From: Dave Glick Date: Wed, 2 Dec 2020 21:37:05 -0500 Subject: [PATCH] Fixed some regressions with layout templates and HTML fragments (#934) --- RELEASE.md | 5 + .../BootstrapperTemplateExtensions.cs | 30 ++ src/Statiq.Web/Templates/Templates.cs | 14 +- .../Pipelines/ContentFixture.cs | 340 ++++++++++++++++++ 4 files changed, 388 insertions(+), 1 deletion(-) diff --git a/RELEASE.md b/RELEASE.md index 93134d88f..bb3ead54f 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,5 +1,10 @@ # 1.0.0-beta.17 +- Fixed a regression in how layouts are applied to Markdown files (#934). +- Changed behavior introduced in 1.0.0-beta.16 regarding HTML files and layouts, now layouts are applied if the HTML file does not contain a `` tag, and are not applied if it does (#934). +- Added a `SetDefaultLayoutTemplate()` bootstrapper extension to change the layout engine applied to HTML fragments to an existing one (if the default of Razor is not wanted). +- Added a `SetDefaultLayoutModule()` bootstrapper extension to change the layout engine applied to HTML fragments to a new module (if the default of Razor is not wanted). + # 1.0.0-beta.16 - Updated Statiq Framework to version [1.0.0-beta.31](https://github.com/statiqdev/Statiq.Framework/releases/tag/v1.0.0-beta.31). diff --git a/src/Statiq.Web/Bootstrapper/BootstrapperTemplateExtensions.cs b/src/Statiq.Web/Bootstrapper/BootstrapperTemplateExtensions.cs index 7c538e631..17357e056 100644 --- a/src/Statiq.Web/Bootstrapper/BootstrapperTemplateExtensions.cs +++ b/src/Statiq.Web/Bootstrapper/BootstrapperTemplateExtensions.cs @@ -72,5 +72,35 @@ public static TBootstrapper ModifyTemplate( throw new Exception($"Template for media type {mediaType} not found"); } }); + + public static TBootstrapper SetDefaultLayoutTemplate( + this TBootstrapper bootstrapper, + string mediaType) + where TBootstrapper : IBootstrapper => + bootstrapper.ConfigureTemplates(templates => + { + if (!templates.TryGetValue(mediaType, out Template template)) + { + throw new Exception($"Template for media type {mediaType} not found"); + } + if (template.ContentType != ContentType.Content) + { + throw new Exception($"Template for media type {mediaType} is not a {ContentType.Content} template"); + } + if (template.Phase != Phase.PostProcess) + { + throw new Exception($"Template for media type {mediaType} is not a {Phase.PostProcess} template"); + } + templates[MediaTypes.HtmlFragment].Module = template.Module; + }); + + public static TBootstrapper SetDefaultLayoutModule( + this TBootstrapper bootstrapper, + IModule module) + where TBootstrapper : IBootstrapper => + bootstrapper.ConfigureTemplates(templates => + { + templates[MediaTypes.HtmlFragment].Module = module; + }); } } diff --git a/src/Statiq.Web/Templates/Templates.cs b/src/Statiq.Web/Templates/Templates.cs index 4cc3adb07..480beda47 100644 --- a/src/Statiq.Web/Templates/Templates.cs +++ b/src/Statiq.Web/Templates/Templates.cs @@ -58,7 +58,19 @@ internal Templates() } return null; // If no layout metadata, revert to default behavior })))); - Add(MediaTypes.HtmlFragment, this[MediaTypes.Razor]); // Set Razor as the default for HTML fragment files, don't process full HTML files as part of a template, they're assumed to be complete + + // Set Razor as the default for HTML and HTML fragment files, but if HTML make sure we don't already have a full before applying layouts + // By changing the module of MediaTypes.HtmlFragment we can change the default layout template + Add(MediaTypes.HtmlFragment, this[MediaTypes.Razor]); + Add( + MediaTypes.Html, + new Template( + ContentType.Content, + Phase.PostProcess, + new ExecuteIf(Config.FromDocument(async doc => !(await doc.GetContentStringAsync()).Replace(" ", string.Empty).Contains(" this[MediaTypes.HtmlFragment].Module)) + })); } /// diff --git a/tests/Statiq.Web.Tests/Pipelines/ContentFixture.cs b/tests/Statiq.Web.Tests/Pipelines/ContentFixture.cs index 359a7f55e..fe413bfec 100644 --- a/tests/Statiq.Web.Tests/Pipelines/ContentFixture.cs +++ b/tests/Statiq.Web.Tests/Pipelines/ContentFixture.cs @@ -5,6 +5,7 @@ using Shouldly; using Statiq.App; using Statiq.Common; +using Statiq.Core; using Statiq.Html; using Statiq.Testing; using Statiq.Web.Pipelines; @@ -101,6 +102,345 @@ public async Task DocumentGatherHeadingsLevelSetting() .ShouldBe(new[] { "1.1", "1.2", "2.1", "2.2", "2.3" }, true); } + [Test] + public async Task LayoutViewStart() + { + // Given + Bootstrapper bootstrapper = Bootstrapper + .Factory + .CreateWeb(Array.Empty()); + TestFileProvider fileProvider = new TestFileProvider + { + { + "/input/Test.md", + @"# Heading + +This is a test" + }, + { + "/input/_Layout.cshtml", + @" +
LAYOUT
+ @RenderBody() +" + }, + { + "/input/_ViewStart.cshtml", + @"@{ + Layout = ""_Layout""; +}" + }, + }; + + // When + BootstrapperTestResult result = await bootstrapper.RunTestAsync(fileProvider); + + // Then + result.ExitCode.ShouldBe((int)ExitCode.Normal); + IDocument document = result.Outputs[nameof(Content)][Phase.Output].ShouldHaveSingleItem(); + (await document.GetContentStringAsync()).ShouldBe( + @" +
LAYOUT
+

Heading

+

This is a test

+ +", + StringCompareShould.IgnoreLineEndings); + } + + [Test] + public async Task HtmlFragmentFileAppliesLayout() + { + // Given + Bootstrapper bootstrapper = Bootstrapper + .Factory + .CreateWeb(Array.Empty()); + TestFileProvider fileProvider = new TestFileProvider + { + { + "/input/Test.fhtml", + @"

This is a test

" + }, + { + "/input/_Layout.cshtml", + @" +
LAYOUT
+ @RenderBody() +" + }, + { + "/input/_ViewStart.cshtml", + @"@{ + Layout = ""_Layout""; +}" + }, + }; + + // When + BootstrapperTestResult result = await bootstrapper.RunTestAsync(fileProvider); + + // Then + result.ExitCode.ShouldBe((int)ExitCode.Normal); + IDocument document = result.Outputs[nameof(Content)][Phase.Output].ShouldHaveSingleItem(); + (await document.GetContentStringAsync()).ShouldBe( + @" +
LAYOUT
+

This is a test

+", + StringCompareShould.IgnoreLineEndings); + } + + [Test] + public async Task HtmlFragmentAppliesLayout() + { + // Given + Bootstrapper bootstrapper = Bootstrapper + .Factory + .CreateWeb(Array.Empty()); + TestFileProvider fileProvider = new TestFileProvider + { + { + "/input/Test.html", + @"

This is a test

" + }, + { + "/input/_Layout.cshtml", + @" +
LAYOUT
+ @RenderBody() +" + }, + { + "/input/_ViewStart.cshtml", + @"@{ + Layout = ""_Layout""; +}" + }, + }; + + // When + BootstrapperTestResult result = await bootstrapper.RunTestAsync(fileProvider); + + // Then + result.ExitCode.ShouldBe((int)ExitCode.Normal); + IDocument document = result.Outputs[nameof(Content)][Phase.Output].ShouldHaveSingleItem(); + (await document.GetContentStringAsync()).ShouldBe( + @" +
LAYOUT
+

This is a test

+", + StringCompareShould.IgnoreLineEndings); + } + + [Test] + public async Task HtmlDoesNotApplyLayout() + { + // Given + Bootstrapper bootstrapper = Bootstrapper + .Factory + .CreateWeb(Array.Empty()); + TestFileProvider fileProvider = new TestFileProvider + { + { + "/input/Test.html", + @"

This is a test

" + }, + { + "/input/_Layout.cshtml", + @" +
LAYOUT
+ @RenderBody() +" + }, + { + "/input/_ViewStart.cshtml", + @"@{ + Layout = ""_Layout""; +}" + }, + }; + + // When + BootstrapperTestResult result = await bootstrapper.RunTestAsync(fileProvider); + + // Then + result.ExitCode.ShouldBe((int)ExitCode.Normal); + IDocument document = result.Outputs[nameof(Content)][Phase.Output].ShouldHaveSingleItem(); + (await document.GetContentStringAsync()).ShouldBe( + @"

This is a test

", + StringCompareShould.IgnoreLineEndings); + } + + [Test] + public async Task HtmlFragmentFileAppliesAlternateLayout() + { + // Given + Bootstrapper bootstrapper = Bootstrapper + .Factory + .CreateWeb(Array.Empty()) + .AddTemplate( + "foobar", + ContentType.Content, + Phase.PostProcess, + new ExecuteConfig(Config.FromDocument(async doc => "start" + (await doc.GetContentStringAsync()) + "end"))) + .SetDefaultLayoutTemplate("foobar"); + TestFileProvider fileProvider = new TestFileProvider + { + { + "/input/Test.fhtml", + @"

This is a test

" + }, + { + "/input/_Layout.cshtml", + @" +
LAYOUT
+ @RenderBody() +" + }, + { + "/input/_ViewStart.cshtml", + @"@{ + Layout = ""_Layout""; +}" + }, + }; + + // When + BootstrapperTestResult result = await bootstrapper.RunTestAsync(fileProvider); + + // Then + result.ExitCode.ShouldBe((int)ExitCode.Normal); + IDocument document = result.Outputs[nameof(Content)][Phase.Output].ShouldHaveSingleItem(); + (await document.GetContentStringAsync()).ShouldBe( + @"start

This is a test

end", + StringCompareShould.IgnoreLineEndings); + } + + [Test] + public async Task HtmlFragmentAppliesAlternateLayout() + { + // Given + Bootstrapper bootstrapper = Bootstrapper + .Factory + .CreateWeb(Array.Empty()) + .SetDefaultLayoutModule(new ExecuteConfig(Config.FromDocument(async doc => "start" + (await doc.GetContentStringAsync()) + "end"))); + TestFileProvider fileProvider = new TestFileProvider + { + { + "/input/Test.html", + @"

This is a test

" + }, + { + "/input/_Layout.cshtml", + @" +
LAYOUT
+ @RenderBody() +" + }, + { + "/input/_ViewStart.cshtml", + @"@{ + Layout = ""_Layout""; +}" + }, + }; + + // When + BootstrapperTestResult result = await bootstrapper.RunTestAsync(fileProvider); + + // Then + result.ExitCode.ShouldBe((int)ExitCode.Normal); + IDocument document = result.Outputs[nameof(Content)][Phase.Output].ShouldHaveSingleItem(); + (await document.GetContentStringAsync()).ShouldBe( + @"start

This is a test

end", + StringCompareShould.IgnoreLineEndings); + } + + [Test] + public async Task HtmlFragmentFileAppliesAlternateLayoutModule() + { + // Given + Bootstrapper bootstrapper = Bootstrapper + .Factory + .CreateWeb(Array.Empty()) + .SetDefaultLayoutModule(new ExecuteConfig(Config.FromDocument(async doc => "start" + (await doc.GetContentStringAsync()) + "end"))); + TestFileProvider fileProvider = new TestFileProvider + { + { + "/input/Test.fhtml", + @"

This is a test

" + }, + { + "/input/_Layout.cshtml", + @" +
LAYOUT
+ @RenderBody() +" + }, + { + "/input/_ViewStart.cshtml", + @"@{ + Layout = ""_Layout""; +}" + }, + }; + + // When + BootstrapperTestResult result = await bootstrapper.RunTestAsync(fileProvider); + + // Then + result.ExitCode.ShouldBe((int)ExitCode.Normal); + IDocument document = result.Outputs[nameof(Content)][Phase.Output].ShouldHaveSingleItem(); + (await document.GetContentStringAsync()).ShouldBe( + @"start

This is a test

end", + StringCompareShould.IgnoreLineEndings); + } + + [Test] + public async Task HtmlFragmentAppliesAlternateLayoutModule() + { + // Given + Bootstrapper bootstrapper = Bootstrapper + .Factory + .CreateWeb(Array.Empty()) + .AddTemplate( + "foobar", + ContentType.Content, + Phase.PostProcess, + new ExecuteConfig(Config.FromDocument(async doc => "start" + (await doc.GetContentStringAsync()) + "end"))) + .SetDefaultLayoutTemplate("foobar"); + TestFileProvider fileProvider = new TestFileProvider + { + { + "/input/Test.html", + @"

This is a test

" + }, + { + "/input/_Layout.cshtml", + @" +
LAYOUT
+ @RenderBody() +" + }, + { + "/input/_ViewStart.cshtml", + @"@{ + Layout = ""_Layout""; +}" + }, + }; + + // When + BootstrapperTestResult result = await bootstrapper.RunTestAsync(fileProvider); + + // Then + result.ExitCode.ShouldBe((int)ExitCode.Normal); + IDocument document = result.Outputs[nameof(Content)][Phase.Output].ShouldHaveSingleItem(); + (await document.GetContentStringAsync()).ShouldBe( + @"start

This is a test

end", + StringCompareShould.IgnoreLineEndings); + } + [Test] public async Task LayoutMetadata() {