Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement paginate tag #389

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions Fluid.Tests/ParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ public void ShouldFailParseInvalidTemplateWithCorrectLineNumber(string source, s
[InlineData("{% unless true %}")]
[InlineData("{% case a %}")]
[InlineData("{% capture myVar %}")]
[InlineData("{% paginate myVar by 50 %}")]
public void ShouldFailNotClosedBlock(string source)
{
var result = _parser.TryParse(source, out var template, out var errors);
Expand All @@ -303,6 +304,7 @@ public void ShouldFailNotClosedBlock(string source)
[InlineData("{% unless true %} {% endunless %}")]
[InlineData("{% case a %} {% when 'cake' %} blah {% endcase %}")]
[InlineData("{% capture myVar %} capture me! {% endcapture %}")]
[InlineData("{% paginate myVar by 50 %} paginate {% endpaginate %}")]
public void ShouldSucceedClosedBlock(string source)
{
var result = _parser.TryParse(source, out var template, out var error);
Expand Down Expand Up @@ -414,6 +416,7 @@ public void ShouldRegisterModelType()
[InlineData("{% comment %}")]
[InlineData("{% raw %}")]
[InlineData("{% capture %}")]
[InlineData("{% paginate %}")]

public void ShouldThrowParseExceptionMissingTag(string template)
{
Expand Down Expand Up @@ -619,7 +622,7 @@ public Task EmptyShouldEqualToNil(string source, string expected)
{
return CheckAsync(source, expected, t => t.SetValue("e", "").SetValue("f", "hello"));
}

[Theory]
[InlineData("zero == empty", "false")]
[InlineData("empty == zero", "false")]
Expand All @@ -642,7 +645,7 @@ public Task BlankShouldComparesToFalse(string source, string expected)
{
return CheckAsync(source, expected, t => t.SetValue("zero", 0).SetValue("one", 1));
}

[Fact]
public void CycleShouldHandleNumbers()
{
Expand All @@ -657,7 +660,7 @@ public void CycleShouldHandleNumbers()
var rendered = template.Render();

Assert.Equal("1<br />2<br />3<br />1<br />2<br />3<br />1<br />2<br />3<br />", rendered);
}
}

[Fact]
public void ShouldAssignWithLogicalExpression()
Expand Down Expand Up @@ -921,5 +924,23 @@ public async Task ShouldSupportCompactNotation(string source, string expected)
var result = await template.RenderAsync(context);
Assert.Equal(expected, result);
}

[Fact]
public void ShouldParsePaginateTag()
{
var statements = Parse("{% paginate list by 10 %}{% endpaginate %}");

Assert.IsType<PaginateStatement>(statements.ElementAt(0));
}
[Fact]
public void ShouldParsePaginateWithPageSizeTag()
{
var statements = Parse("{% paginate list by 10 %}{% endpaginate %}");

Assert.IsType<PaginateStatement>(statements.ElementAt(0));

var forStatement = statements.ElementAt(0) as PaginateStatement;
Assert.True(forStatement.PageSize == 10);
}
}
}
249 changes: 249 additions & 0 deletions Fluid/Ast/PaginateStatement.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
using Fluid.Values;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;

namespace Fluid.Ast
{
public class PaginateStatement : Statement
{
private readonly Expression _expression;
private readonly long _pageSize;
private readonly List<Statement> _statements;

public PaginateStatement(Expression expression, long pageSize, List<Statement> statements)
{
_expression = expression ?? throw new ArgumentNullException(nameof(expression));
_pageSize = pageSize;
_statements = statements ?? new List<Statement>();
}

public long PageSize => _pageSize;

public override async ValueTask<Completion> WriteToAsync(TextWriter writer, TextEncoder encoder, TemplateContext context)
{
var value = await _expression.EvaluateAsync(context);
if (value == null || value is not PaginateableValue paginateableValue) return Completion.Normal;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should work with standard collection values (Array, Dictionary, even strings) but it should handle a custom IPaginate if necessary to customize the results (somehow like you did here).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IPaginate needs to return the current page, but the standard collection does not provide this value (unless we agree on a value in the context). It also requires a method to generate a URL to populate the paginate object.

context.EnterChildScope();
try
{
paginateableValue.PageSize = (int)PageSize;

var data = paginateableValue.GetPaginatedData();
var paginate = CreatePaginate(paginateableValue, data);

context.SetValue("paginate", paginate);

await _statements.RenderStatementsAsync(writer, encoder, context);
}
finally
{
context.ReleaseScope();
}
return Completion.Normal;
}

private PaginateValue CreatePaginate(PaginateableValue value, PaginatedData data)
{
var ret = new PaginateValue
{
Items = data.Total,
CurrentOffset = (value.CurrentPage - 1) * value.PageSize,
CurrentPage = value.CurrentPage,
PageSize = value.PageSize,
Pages = data.Total / value.PageSize
};

if (data.Total % value.PageSize > 0) ret.Pages++;

if (ret.Pages <= 1) return ret;

if (ret.CurrentPage > 1)
{
ret.Previous = new PartValue
{
IsLink = true,
Title = "«",
Url = value.GetUrl(ret.CurrentPage - 1)
};
}

if (ret.CurrentPage < ret.Pages)
{
ret.Next = new PartValue
{
IsLink = true,
Title = "»",
Url = value.GetUrl(ret.CurrentPage + 1)
};
}

var min = ret.CurrentPage - 2;
var max = ret.CurrentPage + 2;

if (min <= 1) min = 2;
if (max >= ret.Pages) max = ret.Pages - 1;

var last = 0;
for (var page = 1; page <= ret.Pages; page++)
{
var add = false;
if (page == 1)
{
add = true;
}
else if (page == ret.Pages)
{
add = true;
}
else if (page >= min && page <= max)
{
add = true;
}

if (!add) continue;

if (last + 1 != page)
{
ret.Parts.Add(new PartValue
{
IsLink = false,
Title = "…"
});
}

last = page;

var item = new PartValue
{
Title = page.ToString(),
IsLink = page != ret.CurrentPage
};

if (item.IsLink) item.Url = value.GetUrl(page);

ret.Parts.Add(item);
}

return ret;
}

/// <summary>
/// https://shopify.dev/api/liquid/objects/part
/// </summary>
internal sealed class PartValue : FluidValue
{
public bool IsLink { get; set; }
public string Title { get; set; }
public string Url { get; set; }

public override FluidValues Type => FluidValues.Dictionary;

public override bool Equals(FluidValue other)
{
return false;
}

public override bool ToBooleanValue()
{
return false;
}

public override decimal ToNumberValue()
{
return 0;
}

public override object ToObjectValue()
{
return null;
}

public override string ToStringValue()
{
return "part";
}

public override ValueTask<FluidValue> GetValueAsync(string name, TemplateContext context)
{
return name switch
{
"is_link" => new ValueTask<FluidValue>(BooleanValue.Create(IsLink)),
"title" => new ValueTask<FluidValue>(StringValue.Create(Title)),
"url" => new ValueTask<FluidValue>(StringValue.Create(Url)),
_ => new ValueTask<FluidValue>(NilValue.Instance),
};
}

public override void WriteTo(TextWriter writer, TextEncoder encoder, CultureInfo cultureInfo)
{
}
}

/// <summary>
/// https://shopify.dev/api/liquid/objects/paginate
/// </summary>
internal sealed class PaginateValue : FluidValue
{
public int CurrentOffset { get; set; }
public int CurrentPage { get; set; }
public int Items { get; set; }
public List<PartValue> Parts { get; } = new();
public PartValue Previous { get; set; }
public PartValue Next { get; set; }
public int PageSize { get; set; }
public int Pages { get; set; }

public override FluidValues Type => FluidValues.Dictionary;

public override bool Equals(FluidValue other)
{
return false;
}

public override bool ToBooleanValue()
{
return false;
}

public override decimal ToNumberValue()
{
return 0;
}

public override object ToObjectValue()
{
return null;
}

public override string ToStringValue()
{
return "paginate";
}

public override ValueTask<FluidValue> GetValueAsync(string name, TemplateContext context)
{
return name switch
{
"current_offset" => new ValueTask<FluidValue>(NumberValue.Create(CurrentOffset)),
"current_page" => new ValueTask<FluidValue>(NumberValue.Create(CurrentPage)),
"items" => new ValueTask<FluidValue>(NumberValue.Create(Items)),
"parts" => new ValueTask<FluidValue>(Create(Parts, context.Options)),
"previous" => new ValueTask<FluidValue>(Previous),
"next" => new ValueTask<FluidValue>(Next),
"page_size" => new ValueTask<FluidValue>(NumberValue.Create(PageSize)),
"pages" => new ValueTask<FluidValue>(NumberValue.Create(Pages)),
_ => new ValueTask<FluidValue>(NilValue.Instance),
};
}

public override void WriteTo(TextWriter writer, TextEncoder encoder, CultureInfo cultureInfo)
{
}
}
}
}
Loading