Skip to content

Commit

Permalink
Separate Includes and Partials in View Engine (sebastienros#377)
Browse files Browse the repository at this point in the history
  • Loading branch information
sebastienros authored Oct 12, 2021
1 parent aa9e948 commit ed65c47
Show file tree
Hide file tree
Showing 22 changed files with 353 additions and 78 deletions.
2 changes: 1 addition & 1 deletion Fluid.MvcSample/Views/Home/Index.liquid
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Hello World from Liquid 2
{% endfor %}

{% assign invoker = 'Index' %}
{% include 'Home/_Partial' %}
{% partial 'Home/_Partial' %}

{% section footer %}
This is the footer
Expand Down
2 changes: 1 addition & 1 deletion Fluid.MvcSample/Views/_Layout.liquid
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
{{ ViewData['Title'] }}

{% assign invoker = '_Layout' %}
{% include '_Partial' %}
{% partial '_Partial' %}

{% renderbody %}

Expand Down
15 changes: 1 addition & 14 deletions Fluid.MvcViewEngine/FluidMvcViewOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,6 @@ namespace Fluid.MvcViewEngine
{
public class FluidMvcViewOptions : FluidViewEngineOptions
{
/// <summary>
/// Gets les list of view location formats.
/// </summary>
/// <remarks>
/// The first argument '{0}' is the view name.
/// The second argument '{1}' is the controller name.
/// The third argument '{2}' is the area name.
/// </remarks>
/// <example>
/// "Views/{1}/{0}"
/// "Views/Shared/{0}"
/// </example>
public IList<string> ViewLocationFormats { get; } = new List<string>();

// Placeholder for future options specific to the MVC view engine
}
}
2 changes: 1 addition & 1 deletion Fluid.MvcViewEngine/FluidRendering.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public FluidRendering(

_options.TemplateOptions.MemberAccessStrategy.Register<ViewDataDictionary>();
_options.TemplateOptions.MemberAccessStrategy.Register<ModelStateDictionary>();
_options.TemplateOptions.FileProvider = new FileProviderMapper(_options.IncludesFileProvider ?? _hostingEnvironment.ContentRootFileProvider, _options.ViewsPath);
_options.TemplateOptions.FileProvider = _options.PartialsFileProvider ?? _hostingEnvironment.ContentRootFileProvider;

_fluidViewRenderer = new FluidViewRenderer(_options);

Expand Down
10 changes: 5 additions & 5 deletions Fluid.MvcViewEngine/FluidViewEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Fluid.ViewEngine;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewEngines;
Expand All @@ -13,7 +14,6 @@ public class FluidViewEngine : IFluidViewEngine
{
private FluidRendering _fluidRendering;
private readonly IWebHostEnvironment _hostingEnvironment;
public static readonly string ViewExtension = ".liquid";
private const string ControllerKey = "controller";
private const string AreaKey = "area";
private FluidMvcViewOptions _options;
Expand Down Expand Up @@ -41,13 +41,13 @@ private ViewEngineResult LocatePageFromViewLocations(ActionContext actionContext

var checkedLocations = new List<string>();

foreach (var location in _options.ViewLocationFormats)
foreach (var location in _options.ViewsLocationFormats)
{
var view = string.Format(location, viewName, controllerName, areaName);
var view = String.Format(location, viewName, controllerName, areaName);

if (fileProvider.GetFileInfo(view).Exists)
{
return ViewEngineResult.Found("Default", new FluidView(view, _fluidRendering));
return ViewEngineResult.Found(viewName, new FluidView(view, _fluidRendering));
}

checkedLocations.Add(view);
Expand Down Expand Up @@ -116,7 +116,7 @@ private static bool IsRelativePath(string name)
Debug.Assert(!string.IsNullOrEmpty(name));

// Though ./ViewName looks like a relative path, framework searches for that view using view locations.
return name.EndsWith(ViewExtension, StringComparison.OrdinalIgnoreCase);
return name.EndsWith(Constants.ViewExtension, StringComparison.OrdinalIgnoreCase);
}

public static string GetNormalizedRouteValue(ActionContext context, string key)
Expand Down
19 changes: 14 additions & 5 deletions Fluid.MvcViewEngine/FluidViewEngineOptionsSetup.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Hosting;
using Fluid.ViewEngine;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Options;

namespace Fluid.MvcViewEngine
Expand All @@ -11,10 +12,18 @@ internal class FluidViewEngineOptionsSetup : ConfigureOptions<FluidMvcViewOption
public FluidViewEngineOptionsSetup(IWebHostEnvironment webHostEnvironment)
: base(options =>
{
options.IncludesFileProvider = webHostEnvironment.ContentRootFileProvider;
options.ViewsFileProvider = webHostEnvironment.ContentRootFileProvider;
options.ViewLocationFormats.Add("Views/{1}/{0}" + FluidViewEngine.ViewExtension);
options.ViewLocationFormats.Add("Views/Shared/{0}" + FluidViewEngine.ViewExtension);
options.PartialsFileProvider = new FileProviderMapper(webHostEnvironment.ContentRootFileProvider, "Views");
options.ViewsFileProvider = new FileProviderMapper(webHostEnvironment.ContentRootFileProvider, "Views");

options.ViewsLocationFormats.Clear();
options.ViewsLocationFormats.Add("/{1}/{0}" + Constants.ViewExtension);
options.ViewsLocationFormats.Add("/Shared/{0}" + Constants.ViewExtension);

options.PartialsLocationFormats.Clear();
options.PartialsLocationFormats.Add("{0}" + Constants.ViewExtension);
options.PartialsLocationFormats.Add("/Partials/{0}" + Constants.ViewExtension);
options.PartialsLocationFormats.Add("/Partials/{1}/{0}" + Constants.ViewExtension);
options.PartialsLocationFormats.Add("/Shared/Partials/{0}" + Constants.ViewExtension);
})
{
}
Expand Down
1 change: 1 addition & 0 deletions Fluid.Tests/Fluid.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Fluid.ViewEngine\Fluid.ViewEngine.csproj" />
<ProjectReference Include="..\Fluid\Fluid.csproj" />
</ItemGroup>

Expand Down
6 changes: 5 additions & 1 deletion Fluid.Tests/Mocks/MockFileInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,18 @@ namespace Fluid.Tests.Mocks
{
public class MockFileInfo : IFileInfo
{
public static readonly MockFileInfo Null = new MockFileInfo("", "") { _exists = false };

private bool _exists = true;

public MockFileInfo(string name, string content)
{
Name = name;
Content = content;
}

public string Content { get; set; }
public bool Exists => true;
public bool Exists => _exists;

public bool IsDirectory => false;

Expand Down
24 changes: 22 additions & 2 deletions Fluid.Tests/Mocks/MockFileProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@
using Microsoft.Extensions.Primitives;
using System;
using System.Collections.Generic;
using System.IO;

namespace Fluid.Tests.Mocks
{
public class MockFileProvider : IFileProvider
{
private Dictionary<string, MockFileInfo> _files = new Dictionary<string, MockFileInfo>();
private readonly bool _caseSensitive;

public MockFileProvider()
public MockFileProvider(bool caseSensitive = false)
{
_caseSensitive = caseSensitive;
}

public IDirectoryContents GetDirectoryContents(string subpath)
Expand All @@ -20,18 +23,22 @@ public IDirectoryContents GetDirectoryContents(string subpath)

public IFileInfo GetFileInfo(string path)
{
path = NormalizePath(path);

if (_files.ContainsKey(path))
{
return _files[path];
}
else
{
return null;
return MockFileInfo.Null;
}
}

public MockFileProvider Add(string path, string content)
{
path = NormalizePath(path);

_files[path] = new MockFileInfo(path, content);
return this;
}
Expand All @@ -40,5 +47,18 @@ public IChangeToken Watch(string filter)
{
throw new NotImplementedException();
}

private string NormalizePath(string path)
{
path = path.Replace('\\', '/');
path = path.Replace('/', Path.DirectorySeparatorChar);

if (!_caseSensitive)
{
return path.ToLowerInvariant();
}

return path;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

namespace Fluid.Tests.MvcViewEngine
{
public class MvcViewEngineTests
public class SampleTests
{
[Fact]
public void ShouldParseIndex()
Expand Down
135 changes: 135 additions & 0 deletions Fluid.Tests/MvcViewEngine/ViewEngineTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
using Fluid.Tests.Mocks;
using Fluid.ViewEngine;
using System.IO;
using System.Threading.Tasks;
using Xunit;

namespace Fluid.Tests.MvcViewEngine
{
public class ViewEngineTests
{
FluidViewEngineOptions _options = new ();
FluidViewRenderer _renderer;
MockFileProvider _mockFileProvider = new ();

public ViewEngineTests()
{
_options.PartialsFileProvider = new FileProviderMapper(_mockFileProvider, "Partials");
_options.ViewsFileProvider = new FileProviderMapper(_mockFileProvider, "Views");

_options.TemplateOptions.MemberAccessStrategy = UnsafeMemberAccessStrategy.Instance;

_options.ViewsLocationFormats.Clear();
_options.ViewsLocationFormats.Add("/{0}" + Constants.ViewExtension);

_options.PartialsLocationFormats.Clear();
_options.PartialsLocationFormats.Add("{0}" + Constants.ViewExtension);
_options.PartialsLocationFormats.Add("/Partials/{0}" + Constants.ViewExtension);

_renderer = new FluidViewRenderer(_options);
}

[Fact]
public async Task ShouldRenderView()
{
_mockFileProvider.Add("Views/Index.liquid", "Hello World");

var sw = new StringWriter();
await _renderer.RenderViewAsync(sw, "Index.liquid", new TemplateContext());
await sw.FlushAsync();

Assert.Equal("Hello World", sw.ToString());
}

[Fact]
public async Task ShouldImportViewStart()
{
_mockFileProvider.Add("Views/Index.liquid", "Hello World");
_mockFileProvider.Add("Views/_ViewStart.liquid", "ViewStart");

var sw = new StringWriter();
await _renderer.RenderViewAsync(sw, "Index.liquid", new TemplateContext());
await sw.FlushAsync();

Assert.Equal("ViewStartHello World", sw.ToString());
}

[Fact]
public async Task ShouldImportViewStartRecursively()
{
_mockFileProvider.Add("Views/Home/Index.liquid", "Hello World");
_mockFileProvider.Add("Views/Home/_ViewStart.liquid", "ViewStart1");
_mockFileProvider.Add("Views/_ViewStart.liquid", "ViewStart2");

var sw = new StringWriter();
await _renderer.RenderViewAsync(sw, "Home/Index.liquid", new TemplateContext());
await sw.FlushAsync();

Assert.Equal("ViewStart1ViewStart2Hello World", sw.ToString());
}

[Fact]
public async Task ShouldIncludePartialsUsingSpecifiFileProvider()
{
_mockFileProvider.Add("Views/Index.liquid", "Hello {% partial 'world' %}");
_mockFileProvider.Add("Partials/World.liquid", "World");

var sw = new StringWriter();
await _renderer.RenderViewAsync(sw, "Index.liquid", new TemplateContext());
await sw.FlushAsync();

Assert.Equal("Hello World", sw.ToString());
}

[Fact]
public async Task ShouldIncludePartialsWithArguments()
{
_mockFileProvider.Add("Views/Index.liquid", "Hello {% partial 'world', x: 1 %}");
_mockFileProvider.Add("Partials/World.liquid", "World {{ x }}");

var sw = new StringWriter();
await _renderer.RenderViewAsync(sw, "Index.liquid", new TemplateContext());
await sw.FlushAsync();

Assert.Equal("Hello World 1", sw.ToString());
}

[Fact]
public async Task ShouldApplyLayout()
{
_mockFileProvider.Add("Views/Index.liquid", "{% layout '_Layout' %}Hi");
_mockFileProvider.Add("Views/_Layout.liquid", "A {% renderbody %} B");

var sw = new StringWriter();
await _renderer.RenderViewAsync(sw, "Index.liquid", new TemplateContext());
await sw.FlushAsync();

Assert.Equal("A Hi B", sw.ToString());
}

[Fact]
public async Task ShouldRenderSection()
{
_mockFileProvider.Add("Views/Index.liquid", "{% section s %}S1{% endsection %}A {% rendersection s %} B");

var sw = new StringWriter();
await _renderer.RenderViewAsync(sw, "Index.liquid", new TemplateContext());
await sw.FlushAsync();

Assert.Equal("A S1 B", sw.ToString());
}

[Fact]
public async Task ShouldRenderSectionInLayout()
{
_mockFileProvider.Add("Views/Index.liquid", "{% layout '_Layout' %}Hi{% section s %}S1{% endsection %}");
_mockFileProvider.Add("Views/_Layout.liquid", "A {% rendersection s %} {% renderbody %} B");

var sw = new StringWriter();
await _renderer.RenderViewAsync(sw, "Index.liquid", new TemplateContext());
await sw.FlushAsync();

Assert.Equal("A S1 Hi B", sw.ToString());
}
}
}
1 change: 1 addition & 0 deletions Fluid.ViewEngine/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ public static class Constants
public const string SectionsIndex = "$$sections";
public const string BodyIndex = "$$body";
public const string LayoutIndex = "$$layout";
public const string RendererIndex = "$$renderer";

public const string ViewStartFilename = "_ViewStart.liquid";
public const string ViewExtension = ".liquid";
Expand Down
Loading

0 comments on commit ed65c47

Please sign in to comment.