cobalt-org / liquid-rust Goto Github PK
View Code? Open in Web Editor NEWLiquid templating for Rust
Home Page: docs.rs/liquid
License: MIT License
Liquid templating for Rust
Home Page: docs.rs/liquid
License: MIT License
Is there a reason that the Template
type isn't exposed? The parse
function returns a value of that type, but we can't annotate values using that type explicitly since it's not a public type. Is this an oversight, or is it deliberate?
Something like
{{ article.published_at | date: "%a, %b %d, %y", "%b %d, %y" }}
to be able to specify a custom parse format.
When trying to use a variable as an argument to a filter I get an error from the parser.
Consider this example:
let input = "{{ \"test\" | prepend: myvar }}";
let parsed = match liquid::parse(input, Default::default()) {
Ok(t) => t,
Err(e) => { panic!("{}", e) }
};
Results in:
thread '<main>' panicked at 'Parsing error: Expected a comma or a pipe, found myvar'
Feel free to grab any of those and implement! Creating filters is really straightforward, you can find more info in the README and look at the existing filters
According to documentation, divided_by
floors only when the divisor is an integer. Otherwise it should not round.
{{ 20 | divided_by: 7 }}
⇒ 2
{{ 20 | divided_by: 7.0 }}
⇒ 2.857142857142857
{% assign my_integer = 7 %}
{{ 20 | divided_by: my_integer }}
⇒ 2
{% assign my_integer = 7 %}
{% assign my_float = my_integer | times: 1.0 %}
{{ 20 | divided_by: my_float }}
⇒ 2.857142857142857
However, as of 0.9.1, all of these produce 2
as result since it floors unconditionally.
I think you may need to split Value::Num
into two distinct variants to properly fix it.
I think that objects held in a liquid context should be allowed to be more complex than HashMap
.
Perhaps Object
should be a trait that defines
trait Object {
fn get_val(&mut self, name: &str) {
}
}
The reason is, some objects cannot or should not be loaded into memory if they're unused. A good example of this is Collection
in Shopify. They can hold hundreds, if not thousands or millions of products. This makes lazy-loading of collections and products desirable. It also makes memoization desirable.
My Rust (or statically typed language) knowledge fails me here.
It would also be desirable to make it possible for tags to act upon the objects, which would mean that tags would need to check the type of the returned object.
An example of this is the paginate
block.
The documentation for it is pretty poor, but the gist is that it bounds the collection such that for-loops will iterate over only a slice of the data.
ex: Given collection = [1, 2, 3, 4, 5, 6]
and current_page = 2
{% paginate collection by 2 %}
{% for num in collection %}
{{ num }}
{% endfor %}
{% endpaginate %}
The paginate block will bound collection
to (2..4)
(4 excluded). The result will then be
3
4
I'd be super happy to tackle this, but I'd need pointers
In https://github.com/cobalt-org/liquid-rust/blob/master/src/filters.rs#L308 we use a custom date format to parse dates for the date filter if it's a string.
The original liquid has a to_date
function that can take different types of input and parse it. We might even parse different date formats that way https://github.com/Shopify/liquid/blob/master/lib/liquid/utils.rb#L63
This would be helpful for tracking dependency upgrades.
I'm unsure what our options are but it'd at least be nice to look up the result of master builds over time or to see the impact of PRs on performance.
As per #9, the parser has been made public, and needs some docs. We should also add !#deny[missing_docs]
while we're at it.
This issue was automatically generated. Feel free to close without ceremony if
you do not agree with re-licensing or if it is not possible for other reasons.
Respond to @cmr with any questions or concerns, or pop over to
#rust-offtopic
on IRC to discuss.
You're receiving this because someone (perhaps the project maintainer)
published a crates.io package with the license as "MIT" xor "Apache-2.0" and
the repository field pointing here.
TL;DR the Rust ecosystem is largely Apache-2.0. Being available under that
license is good for interoperation. The MIT license as an add-on can be nice
for GPLv2 projects to use your code.
The MIT license requires reproducing countless copies of the same copyright
header with different names in the copyright field, for every MIT library in
use. The Apache license does not have this drawback. However, this is not the
primary motivation for me creating these issues. The Apache license also has
protections from patent trolls and an explicit contribution licensing clause.
However, the Apache license is incompatible with GPLv2. This is why Rust is
dual-licensed as MIT/Apache (the "primary" license being Apache, MIT only for
GPLv2 compat), and doing so would be wise for this project. This also makes
this crate suitable for inclusion and unrestricted sharing in the Rust
standard distribution and other projects using dual MIT/Apache, such as my
personal ulterior motive, the Robigalia project.
Some ask, "Does this really apply to binary redistributions? Does MIT really
require reproducing the whole thing?" I'm not a lawyer, and I can't give legal
advice, but some Google Android apps include open source attributions using
this interpretation. Others also agree with
it.
But, again, the copyright notice redistribution is not the primary motivation
for the dual-licensing. It's stronger protections to licensees and better
interoperation with the wider Rust ecosystem.
To do this, get explicit approval from each contributor of copyrightable work
(as not all contributions qualify for copyright) and then add the following to
your README:
## License
Licensed under either of
* Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
* MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
at your option.
### Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any
additional terms or conditions.
and in your license headers, use the following boilerplate (based on that used in Rust):
// Copyright (c) 2016 liquid-rust developers
//
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0> or the MIT
// license <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. All files in the project carrying such notice may not be copied,
// modified, or distributed except according to those terms.
Be sure to add the relevant LICENSE-{MIT,APACHE}
files. You can copy these
from the Rust repo for a plain-text
version.
And don't forget to update the license
metadata in your Cargo.toml
to:
license = "MIT/Apache-2.0"
I'll be going through projects which agree to be relicensed and have approval
by the necessary contributors and doing this changes, so feel free to leave
the heavy lifting to me!
To agree to relicensing, comment with :
I license past and future contributions under the dual MIT/Apache-2.0 license, allowing licensees to chose either at their option
Or, if you're a contributor, you can check the box in this repo next to your
name. My scripts will pick this exact phrase up and check your checkbox, but
I'll come through and manually review this issue later as well.
A cargo clean
fixes it but that means having to rebuild everything. This also limits our ability for the CI to cache
liquid-rust version: 0.10.0
rust version: rustc 1.18.0 (03fc9d622 2017-06-06)
OS: macOS Sierra / 10.12.4
{% assign tags = "foo,bar" | split: "," %}
tags[0]: {{ tags[0] }}
running the code results in
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Parser("Expected |, found [")', sr
c/libcore/result.rs:859
note: Run with `RUST_BACKTRACE=1` for a backtrace.
{% for tag in tags %}
{{ tag }}
{% endfor %}
Will build and display each tag.
I've tested using another liquid parser and this definitely a problem with liquid-rust.
Hi! You might be interested in going stable-compatible. This means that the project can't use any unstable language features. This is currently using:
#![feature(box_syntax)]
#![feature(unboxed_closures)]
#![feature(plugin)]
#![feature(test)]
#![feature(collections)]
#![feature(core)]
You can easily identify which parts of your code require which features by removing those attributes.
The box syntax feature is an easy one to do away with by replacing box someexpr
with Box::new(someexpr)
.
The plugin
feature is mostly required due to your use of regex_macros
, a very nice package indeed but one which requires syntax extension support, which stable doesn't. This is easily remedied by changing regex!("pattern")
to Regex::new("pattern")
and checking for potential failure, or unwrapping if the pattern isn't dynamically generated.
The test feature is usually needed for benchmarking purposes.
The rest you can figure out by removing the given attribute and seeing what generates an error.
Common strategies for going stable include:
cfg(feature = "nightly")
attribute to conditionally compile functionality that requires nightly features...which is basically just the opposite of if
. You can find the if
code here: https://github.com/cobalt-org/liquid-rust/blob/master/src/tags/if_block.rs
This should be fairly straightforward, however, please do not fully copy-paste the if code into a new file, we should find a way to share some code between the two tags.
The liquid markup documentation at spotify indicates there is an object usable in for loops to get things like the index, test for first/last, etc. to be able to do things like this:
{% for product in collections.frontpage.products %}
{% if forloop.first == true %}
First time through!
{% else %}
Not the first time.
{% endif %}
{% endfor %}
which results in output:
First time through!
Not the first time.
Not the first time.
Not the first time.
Not the first time.
I have a need to do something different depending on which item I'm on and there doesn't seem to be any other way to do this currently implemented.
In Shopify/liquid
the syntax for the include tag is different from my implementation.
# Simply include another template:
#
# {% include 'product' %}
#
# Include a template with a local variable:
#
# {% include 'product' with products[0] %}
#
We should implement the Include tag similar. This issue is to keep track.
Can be witnessed here: https://ci.appveyor.com/project/johannhof/liquid-rust/build/1.0.2/job/w7ph5acbd079v7hf#L244
There might be more.
It would be much nicer if instead of [error] Liquid error: Rendering error: Tried to iterate over None, which is not supported. or something similarly unhelpful, cobalt would directly point to the problem.
This is for the liquid side of the work for cobalt-org/cobalt.rs#136
liquid-rust version: 0.10.1
rust version: rustc 1.21.0 (3b72af97e 2017-10-09)
OS: Arch Linux
I'm using Cobalt (0.7.3) and it appears that the dot notation form of size doesn't work.
With the following in index.liquid
and 10 posts:
{{ posts | size }}
x
{{ posts.size }}
the output is:
10
x
What I'm trying to do is show some text if there are more than 5 posts:
{% if posts.size > 5 %}
more posts
{% endif %}
With the include
tag and the upcoming liquid-reuse, it would be great to have access to some sort of file cache inside Liquid during compile time. I hope we can do this in a way that gives plugin authors flexibility while not adding overhead to "simple" users of the API.
I think they have to do with travis-cargo coverage. Maybe we could add a build step that removes/ignores the skeptic test files when running travis-cargo coveralls
.
There appears to be an issue if you chain more than one filter together as shown in this sample program
extern crate liquid;
use liquid::{Renderable, Context, Value};
fn main() {
println!("Run cargo test instead!");
}
#[test]
fn test_uppercase() {
let template = liquid::parse("{{ s | upcase }}", Default::default()).unwrap();
let mut context = Context::new();
context.set_val("s", Value::Str(String::from("foofoo")));
let output = template.render(&mut context);
assert_eq!(output.unwrap(), Some("FOOFOO".to_string()));
}
#[test]
fn test_replace() {
let template = liquid::parse("{{ s | replace:'foo','bar' }}", Default::default()).unwrap();
let mut context = Context::new();
context.set_val("s", Value::Str(String::from("foofoo")));
let output = template.render(&mut context);
assert_eq!(output.unwrap(), Some("barbar".to_string()));
}
#[test]
fn test_both() {
let template = liquid::parse("{{ s | replace:'foo','bar' | upcase }}", Default::default()).unwrap();
let mut context = Context::new();
context.set_val("s", Value::Str(String::from("foofoo")));
let output = template.render(&mut context);
assert_eq!(output.unwrap(), Some("BARBAR".to_string()));
}
Here is the result:
% cargo test
Running target/debug/loops-43680f964937768f
running 3 tests
test test_uppercase ... ok
test test_replace ... ok
test test_both ... FAILED
failures:
---- test_both stdout ----
thread 'test_both' panicked at 'assertion failed: `(left == right)` (left: `Some("FOOFOO")`, right: `Some("BARBAR")`)', src/main.rs:37
note: Run with `RUST_BACKTRACE=1` for a backtrace.
failures:
test_both
test result: FAILED. 2 passed; 1 failed; 0 ignored; 0 measured
The intermediate filter result isn't passed as input to the next filter in the chain. Should have gotten BARBAR
but instead for FOOFOO
. It seems like the replace
filter wasn't called, but I added some debugging println!
and is IS being called, but its result isn't input to the next block in the chain.
Quick scan of the code seems that the issue is around in this area
Originally cobalt-org/cobalt.rs#141 and reported by @ebkalderon
When extending from a layout template, the resulting HTML has inconsistent indentation. Imagine you have an example layout file default.liquid
which looks like this:
<!doctype html>
<html>
<head> ... </head>
<body>
{% if is_post %}
{% include '_layouts/post.liquid' %}
{% else %}
{{ content }}
{% endif %}
</body>
</html>
and also a page which extends the layout:
extends: default.liquid
---
<section id="hero">
<!-- blah blah -->
</section>
One would expect the resulting HTML generated by Cobalt to look like:
<!doctype html>
<html>
<head> ... </head>
<body>
<section id="hero">
<!-- blah blah -->
</section>
</body>
</html>
But instead, Cobalt generates this:
<!doctype html>
<html>
<head> ... </head>
<body>
<section id="hero">
<!-- blah blah -->
</section>
</body>
</html>
Not only is the page content pasted directly into the layout it extends from without any indentation whatsoever, there are also extra newlines around where the Liquid markup used to be. This doesn't break the functionality of the page in any way, but it still looks pretty ugly.
The cobalt website has this line:
{% raw %} <a href="{{post.path}}">{{ post.title }}</a>{% endraw %}
See https://github.com/cobalt-org/cobalt-org.github.io/blob/source/docs/pages.liquid#L78
That is being rendered as
</a>{{ post.title }}">{{post.path}} <a href="
See https://github.com/cobalt-org/cobalt-org.github.io/blob/master/docs/pages.html#L110
@jespino suggested
Currently the layouts are on liquid have to been rewritten various times when you want to define different layouts. Could be nice to have a "extends" tag like in django templates or jinja. or something that allow to build a template from a base template.
base.liquid
<html>
<body>
{{include_block}}
</body>
</html>
default.liquid
{% include "base.liquid" block_var="include_block" %}
<h1>Posts</h1>
<div>{{ content }}</div>
{% endinclude %}
It might be better to do something like {% include "base.liquid" with include_block %}
.
I wanted to parse many templates into Template
s and store them in an Arc<Mutex<HashMap<String, Template>>>
so that multiple threads could access the template to render various Context
s, to avoid re-parsing a given template each time it needs to be rendered. However, many things seem to be tangled up in local, non-'static
references, which means that I can't simply specify Template<'static>
or Box<Renderable + Sync + Send + 'static>
. The parser in particular seems to revolve entirely around local references.
Would you guys be open to creating a template type that is "self-contained," so that it is implicitly Sync + Send + 'static
, to allow us to send and share them between threads? I can do this with the handlebars
crate, for example, because the Template
type is like this. They also have a Registry
type that makes it very easy to render a given template "by name," so I simply stick this type in an Arc<Mutex
after I parse my templates and then various threads can use it to render separate documents.
The only alternative I have now, that I can think of, is to re-parse every template at the time and context that it needs to be rendered, which is a shame.
I try to compile the following liquid template:
{{ title | replace:"hello world","hello, world" }}
I get the following error:
Liquid error: Parsing error: parse_output: Identifier("\"hello") not implemented
When I remove spaces and leave out comma:
{{ title | replace:"helloworld","hello,world" }}
I get the same error.
When I remove both spaces and comma:
{{ title | replace:"helloworld","helloWorld" }}
I get my example compiled.
It seems like filter arguments parser doesn't parse strings in quotes correctly.
I also tried to replace double quotes with single quotes to no avail.
Having clippy break the build is great for reviewers since we don't have to run it manually or check the logs. The problem is building clippy can sometimes fail.
I'd suggest making clippy builds not gate the CI but clippy runs would but then how do we know when we have coverage with clippy?
Following trust
means that if someone improves things, we call get the improvement, right?
According to documentation, round
filter accepts an optional argument, the number of decimal places to round to
{{ 183.357 | round: 2 }}
⇒ 183.36
As of 0.9.1, this argument is ignored and the above is incorrectly rounded to 183
.
Would be nice to allow loading template files directly from the file system.
The tests in the tests
folder are not run.
Because of the [[test]]
in Cargo.toml
cargo only run the test specified here. And this test is just an empty file. Because you want to use a non-default feature in a test, this means you cannot just remove this section.
I see multiple ways to solve this issue:
Cargo.toml
liquid.rs
(via mod
) (this could be done automatically with code generation in build.rs
)Cargo.toml
but specify the feature on the command line each time (cargo test --features "serde"
)Using current cobalt (cobalt-org/cobalt.rs@1c7fb5b) I get this error:
Liquid error: Parsing error: Expected String Literal, found Some(Identifier("_layouts/head.liquid"))
head.liquid
looks like this:
<head>
<meta charset="utf-8">
<title>{{ title }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!--[if lte IE 8]><script src="/assets/js/ie/html5shiv.js"></script><![endif]-->
<link rel="stylesheet" href="/assets/css/main.css" />
<!--[if lte IE 8]><link rel="stylesheet" href="/assets/css/ie8.css" /><![endif]-->
<!--[if lte IE 9]><link rel="stylesheet" href="/assets/css/ie9.css" /><![endif]-->
</head>
And default.liquid
like this:
<!DOCTYPE html>
<html>
{% include _layouts/head.liquid %}
<body>
<div id="page-wrapper">
{% include _layouts/header.liquid %}
<section class="wrapper style1">
<div class="container">
{% include _layouts/no_sidebar.liquid %}
</div>
</section>
{% include _layouts/footer.liquid %}
</div>
{% include _layouts/scripts.liquid %}
</body>
</html>
Now where is the error?
This is tracking the progress with thoroughly documenting the complete API so that we may add the #![deny(missing_docs)]
label.
I attempted making a release process. I remember tagging the commit. For some reason the tag is not on github.
Not sure when they added that but we should support this: https://shopify.github.io/liquid/basics/whitespace/
Goals
Secondary goals
globals
Context
be a trait when different tags (cycle, include, break, etc) need special support in it?trait FilterValue
to advertise the required/optional params
Value
should be an enum of Object
, Array
, Scalar
, and Nil
Scalar
is a struct wrapping an enum to hide implementation detailsScalar
can coerce between different data typesLiquidOption
owns
parse
takes
LiquidOptions
Template
render
takes
Context
Context
Concerns
parse
while filters are on-render
Tera
is a template collection
new
parses a directory (can add files to it one at a time)register_filter
for adding them to the template collectionregister_tester
for adding new conditional evaluationsrender
takes a template identifier and a Context
So this is a pretty large change I think, but as far as I understand the original Liquid HTML escapes everything that goes into filters by default, or something. Someone would have to check what exactly gets escaped in Liquid and do the same escaping in liquid-rust. It just states
It needs to be non evaling and secure. Liquid templates are made so that users can edit them. You don't want to run code on your server which your users wrote.
which is pretty vague.
Could use https://github.com/skade/escapade
Currently it is required but should be made optional.
As the guidelines point out, error types are a way to leak out unintended dependencies in a public API, possibly breaking people as you evolve it.
Types of errors
Programmaticaly relevant information
https://github.com/Shopify/liquid/wiki/Liquid-for-Designers#tags
So I started working on an implmentation of split
here.
The unit test passes just fine, but the resulting Array
causes a problem downstream as you can see from the failed test:
---- split stdout ----
thread 'split' panicked at 'called `Result::unwrap()` on an `Err` value: Lexer("\' is not a valid identifier")', src/libcore/result.rs:746
stack backtrace:
1: 0x10bd959f8 - std::sys::backtrace::tracing::imp::write::hf68f1a220b61702c
2: 0x10bd97c95 - std::panicking::default_hook::_$u7b$$u7b$closure$u7d$$u7d$::hb638acea7c29901b
3: 0x10bd9781f - std::panicking::default_hook::h508c3dab3df347d6
4: 0x10bd8c4d6 - std::sys_common::unwind::begin_unwind_inner::h17f9e42de6d55309
5: 0x10bd8d28e - std::sys_common::unwind::begin_unwind_fmt::h039d18bd8498e1d0
6: 0x10bd94cc7 - rust_begin_unwind
7: 0x10bdbe510 - core::panicking::panic_fmt::h813eaa27a5810609
8: 0x10bbc1ee8 - core::result::unwrap_failed::he9d1212db5110052
9: 0x10bbc1828 - _<std..result..Result<T, E>>::unwrap::h7493e55a0acc9844
10: 0x10bbcafcb - filters::split::h885acea6ebf1f10e
11: 0x10bbe82cb - _<F as std..boxed..FnBox<A>>::call_box::h48dd10267ee7767d
12: 0x10bbea79d - std::sys_common::unwind::try::try_fn::hc72e80a3e6bb322c
13: 0x10bd94c5b - __rust_try
14: 0x10bd94be3 - std::sys_common::unwind::inner_try::h74a189ca1fbe8e07
15: 0x10bbeab84 - _<F as std..boxed..FnBox<A>>::call_box::h8f9b07ad31279db5
16: 0x10bd96ee8 - std::sys::thread::Thread::new::thread_start::h40a33956f4cb7596
17: 0x7fff9be2899c - _pthread_body
18: 0x7fff9be28919 - _pthread_start
Since there are no other implementations of filters that return Value::Array
, this may have just been overlooked in the filter scaffolding... Need some help here. Thanks!
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.