edgurgel / solid Goto Github PK
View Code? Open in Web Editor NEWLiquid template engine in Elixir
Home Page: https://hexdocs.pm/solid
License: MIT License
Liquid template engine in Elixir
Home Page: https://hexdocs.pm/solid
License: MIT License
@edgurgel How do you feel about the following:
Instead of the parser returning {:field, String.t()}
it returns {:field [atom()]}
. This would remove the need to split the string during render/runtime and also play nicely with structs without the need for any coercion.
I feel enforcing keys as atoms is more beneficial than strings, due to the fact you gain the ability to use structs for your render values.
The parser would become:
field <- [0-9a-zA-Z\._]* access* `
case Node of
[FieldName | [[]]] -> {field, lists:map(fun(Field) -> binary_to_existing_atom(Field, utf8) end, string:split(iolist_to_binary(FieldName), "."))};
[FieldName | [Accesses]] ->
{field, lists:map(fun(Field) -> binary_to_existing_atom(Field, utf8) end, string:split(iolist_to_binary(FieldName), ".")), Accesses}
end
`;
And the argument:
@spec get({:field, [atom()]} | {:field, [atom()], [{:access, non_neg_integer}]} | {:value, term}, Context.t) :: term
def get({:value, val}, _hash), do: val
def get({:field, key}, %Context{vars: vars}) do
get_in(vars, key)
end
Render result was different!
Input:
########################
this should print
{%- break -%}
this should not print
########################
code: liquid_output == solid_output
left: "########################\n\nthis should print\n"
right: "########################\n\nthis should print"
Render result was different!
Input:
########################
this should print
{%- continue %}
this should not print
########################
code: liquid_output == solid_output
left: "########################\n\nthis should print\n"
right: "########################\n\nthis should print"
https://github.com/edgurgel/solid/blob/master/test/integration/whitespace_control_cases/break_tag_test.exs#L3-L15
& https://github.com/edgurgel/solid/blob/master/test/integration/whitespace_control_cases/continue_tag_test.exs#L3-L15
Add replace_last
and remove_last
filters
Reference: Shopify/liquid#1422
Example:
{% assign myvariable = 5 | plus: 10 %}
{{ myvariable }}
# => 15
input.liquid
{% increment my_number %}
{% increment my_number %}
{% increment my_number %}
{{ my_number }}
input.json
{
"my_number": 4
}
Render result was different!
Input:
{% increment my_number %}
{% increment my_number %}
{% increment my_number %}
{{ my_number }}
code: liquid_output == solid_output
left: "4\n5\n6\n\n7\n"
right: "0\n1\n2\n\n4\n"
There's an issue if you use the contains
operator due to the use of :erlang.binary_to_existing_atom
.
I'm opening this issue instead of a PR because I'm not sure what the best route would be to solve this. There's an easy workaround that makes this a non-blocking issue, for me:
Simply defining :contains
somewhere "fixes" this for me, but I think ideally this module should somehow ensure that :contains
already exists.
I've seen @before_compile
used for this or just as a no-op line proceeding the usage of :erlang.binary_to_existing_atom
{% navigation %}
{% link About %}
{% link Services %}
{% link Contact %}
{% endnavigation %}
Hey there, I was building a Liquid template that required a couple of render
statements which would include template files from the local file system. I noticed that this had quite an impact on performance, and my suspicion is that it might be because Solid keeps re-reading and re-parsing each included template file from the filesystem (https://github.com/edgurgel/solid/blob/main/lib/solid/tag/render.ex#L54).
Would you be open to a PR that changes the signature of the FileSystem.read_template_file/2
callback from read_template_file(binary(), options :: any()) :: String.t()
to read_template_file(binary(), options :: any()) :: String.t() | Template.t()
? Iโd also adjust the render tag so that it handles this correctly.
Add base64_encode
, base64_decode
, base64_url_safe_encode
, and base64_url_safe_decode
filters
Reference: Shopify/liquid#1450
We want to limit how deep a liquid template can get to just like the Liquid gem does: https://github.com/Shopify/liquid/blob/efef03d944157db323f1aed5e19861bf66fe256f/test/integration/security_test.rb#L82-L88
Is it possible to pass arguments into a custom tag? For example: Shopify's render tag allows for this syntax:
{% render 'name', my_variable: my_variable, my_other_variable: 'oranges' %}
I've tried this out and it doesn't seem possible today, although I could be missing something.
{% assign variants = product.variants %}
{% render "product_variant" for variants as variant %}
This is a super strange one that gets into some stuff I'm not sure about. I am seeing filters not being applied, sometimes, which made me look into root cause.
I see the input to my filter appear when I restart the server. The filter is not applied.
Running MyCustomFilters.the_filter("x")
properly returns the filter result. The solid filter is now applied every time.
I tracked it down to String.to_existing_atom
raising an error for my function:
iex(9)> String.to_existing_atom("asset_url")
** (ArgumentError) argument error
:erlang.binary_to_existing_atom("asset_url", :utf8)
After I manually run the function, it becomes an existing atom. TBH, I'm not entirely sure what causes a function to be registered as an atom, and I'm not sure why it's not an existing atom when the app first boots.
question about basic Filesystem usage
what am I missing for filesystem usage in a simple Phoenix index action below?
def index(conn, _params) do
template_path = "/lib/app_web/templates/liquid/"
file_system = Solid.LocalFileSystem.new(template_path)
text = Solid.LocalFileSystem.full_path(file_system, "hello")
render(conn, text)
end
/lib/app_web/templates/liquid/_hello.liquid
hello
gives
** (FunctionClauseError) no function clause matching in String.match?/2
(elixir 1.12.2) lib/string.ex:2259: String.match?(%Solid.LocalFileSystem{pattern: "_%s.liquid", root: "/lib/app_web/templates/liquid/"}, ~r/^[^.\/][a-zA-Z0-9_\/]+$/)
(solid 0.10.0) lib/solid/file_system.ex:84: Solid.LocalFileSystem.full_path/2
no doubt missing some basic config
Issue
Use filter in if condition raise exception
{% for item in user.hobbies %}
{% if item | upcase == "COOKING" %}
<b>{{item}}</b>
{% else %}
{{ item }}
{% endif %}
{% endfor %}
defmodule Parser do
use Solid.Parser.Base, excluded_tags: [
Solid.Tag.Break,
Solid.Tag.Continue,
Solid.Tag.Counter,
Solid.Tag.Comment,
Solid.Tag.Assign,
Solid.Tag.Capture,
#Solid.Tag.If,
Solid.Tag.Case,
Solid.Tag.For,
Solid.Tag.Raw,
Solid.Tag.Cycle,
Solid.Tag.Render
]
end
gives the following error:
** (FunctionClauseError) no function clause matching in NimbleParsec.choice/3
If I comment out (enable) a second tag, the error disappears.
In the README you mention
Solid.render/3 doesn't raise or return errors unless strict_variables: true or strict_filters: true are passed as options.
But the function doc doesn't mention it. IMO, README should not be mandatory read, code docs always take precedence as the source of truth
I read code which render custom_tag
and I see that the options is not passed to custom tag render function.
Is there any reason not to do that?
In my case I want to add custom tag render
to render another template, and I think it would be better to pass via options
instead of variables.
For example:
template
|> Solid.parse!()
|> Solid.render(my_variables, tags: %{"include" => MyIncludeTemplate}, lookup_dir: "templates/")
or we can support tag options tuple
template
|> Solid.parse!()
|> Solid.render(my_variables, tags: %{"include" => {MyIncludeTemplate, [lookup_dir: "templates/"]})
Hi @edgurgel - thanks for creating this library for the community! What are your thoughts on passing the options
passed to render down to the custom filters.
Use case:
In a multi-tenant environment, if I create a custom filter like asset_url
(similar to the one Shopify offers), I need to know the tenant in order to return the correct URL. Additionally, filename passed to asset_url
might have a hash appended to the name.
{{ "logo.png" | asset_url }}
returns https://cdn.com/logo-123abc-hash.png
.
Thanks!
I'm seeing something really odd and wanted to raise it up as a potential bug. I haven't ruled out me doing something stupid, but everything double-checks out.
I have a {% for %} tag which has a custom parsed tag inside of it. In this case, it's something like:
{% for foo in bars %}
{% render "a_template" %}
{% endfor %}
If I put the render tag outside of the loop, it executes. If I put it in the loop, it seems to never get called.
More info:
https://shopify.dev/docs/themes/liquid/reference/tags/theme-tags#liquid
{% liquid
case section.blocks.size
when 1
assign column_size = ''
when 2
assign column_size = 'one-half'
when 3
assign column_size = 'one-third'
else
assign column_size = 'one-quarter'
endcase %}
New tag!
I see some reference to include tags however, are they actually implemented?
Also not sure if this is actually feasible using a custom tag because ideally we'd want to do the template inclusion in parse time and not in runtime?
Missing filters:
The date filter should accept parameters from the Unix strftime, which includes %s
. %s
outputs the number of seconds since Epoch (Jan 1, 1970_. This does not appear to be supported in this library. Are there plans to support this?
More info here: https://help.shopify.com/en/themes/liquid/tags/theme-tags#raw
{% if site.pages.size > 10 %}
This is a big website!
{% endif %}
Steps to reproduce: Run Solid.render, passing in a custom filters module
Expected: Custom filters are recognized in the template
Behavior: Solid.UndefinedFilterError is returned
I define a new custom parser with only 1 custom tag
defmodule CustomPaddingParser do
use Solid.Parser.Base, custom_tags: ["padding"]
end
And compiler throw this message. If number of custom tag >= 2, it works.
** (FunctionClauseError) no function clause matching in NimbleParsec.choice/2
The following arguments were given to NimbleParsec.choice/2:
# 1
[]
# 2
[[string: "padding"]]
Attempted function clauses (showing 1 out of 1):
def choice(combinator, [_, _ | _] = choices) when is_list(combinator)
(nimble_parsec 1.1.0) lib/nimble_parsec.ex:1437: NimbleParsec.choice/2
test/support/custom_parsers.ex:10: (module)
(stdlib 3.14.1) erl_eval.erl:680: :erl_eval.do_apply/6
Hello,
Could you please publish v0.13? Has some nice improvements that I'd like to use.
Thanks.
Currently if parsing failed, solid the return message
Reason: expected end of string, line: xxx
It does not provide much detail so you don't know where to fix or what to fix.
It will be better if the error message can provide more details:
{% if 1 == 1 %}
Hi
how are you {{name
{% endif %}
and the line which error return is line 1, not line 3
We have added support to strict_variables
(#105). We should also support to the strict_filters
option that Liquid supports:
template = Liquid::Template.parse("{{x | filter1 | upcase}}")
template.render({ 'x' => 'foo' }, { strict_filters: true })
#=> '' # when at least one filter in the filter chain is undefined, a whole expression is rendered as nil
template.errors
#=> [#<Liquid::UndefinedFilter: Liquid error: undefined filter filter1>]
https://www.rubydoc.info/gems/liquid/4.0.1#undefined-variables-and-filters
I have a simple template that uses a custom tag inside of a for loop:
{% for file in files.items %}
{% render_file file %}
{% endfor %}
If I inspect the vars
present in the custom tag, I have files
available, but file
is not defined. Is it expected that the iteration variable is available to the custom tag?
I see that iteration_vars
exists, so I'm wondering if the order for grabbing a variable should be: context.iteration_vars
fallback to context.vars
Hey there! I've been working to move https://koype.net/ to use this library over the unmaintained https://github.com/bettyblocks/liquid-elixir. It did work well but I had support for providing custom tags. It looks like this uses nimble-parsec so I'm not sure what would be required to extend this so custom tag support could be a tag. My use case was allowing templates to look up routes in my Phoenix application.
When rendering using {% render arg %}
, Solid.Tag.Render
is unaware of what parser the parent template used to render it. This causes an issue where, if the child template uses a custom tag, rendering fails.
I've created a test repo to demonstrate this.
I think the solution would be to add a :parser
field to Solid.Template
, but wanted your take on this.
Thank you for this awesome library, we love it ๐
We've noticed that when we add custom tags (of which we've added 8) it slows down our compiles tremendously when processing those tags. So just the tags take about 20 seconds to compile.
Do you know if this is this something that's just a limitation of nimble_parsec
and some kind of Big(O) issue with the underlying algorithms or potentially how we're constructing our tags that's resulting in way more parser combinations than needed?
Here's an example of a custom tag:
In Liquid it is:
{% image file_id %}
The elixir:
@impl true
def spec(_parser) do
space = Literal.whitespace(min: 0)
ignore(BaseTag.opening_tag())
|> ignore(space)
|> ignore(string("image"))
|> ignore(space)
|> tag(Argument.argument(), :file_id)
|> ignore(space)
|> ignore(BaseTag.closing_tag())
end
Apologies for using the issue tracker for help.
I am wondering if you see any way to detect whether a tag results in no value? I have a use case where I want to display an error (or even the {{ name }}
text) if a value isn't available.
I attempted to look down the path of providing my own Solid.Context struct, but I don't think there is a place that I could hook in (even hacky) to detect an empty value.
https://shopify.dev/docs/themes/liquid/reference/tags/theme-tags#render
{% render 'snippet-name' %}
New tag!
It would be nice to have a basic benchmark suite so we could analyse changes on the parser.
{% if priceUSD && price %} - not work
{% if priceUSD || price %} - not work
render with
{% assign featured_product = all_products["product_handle"] %}
{% render "product" with featured_product as product %}
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.