Compare commits

..

500 Commits

Author SHA1 Message Date
Samuel
fd5e3e87c7 Optimize range iteration
For loops support a range syntax for iterating a fixed number of times,
which looks like this.

```liquid
{% for i in range (1..100) %}
        ...
{% endfor %}
```

Previously, we converted these ranges to arrays using `to_a`, which
initialized an array containing each number in the range. Since all we
use these ranges for is iteration, this is far less efficient than
using a range iterator.

Doing this means that iterating over ranges now takes O(1) rather than O(n)
memory. See the PR for more benchmarks.

* Remove to_a cast on ranges
* Add ReversableRange iterator
* Add custom range-specific slicing logic
2019-07-09 10:57:39 -04:00
Florian Weingarten
b3b63a683f Merge pull request #1097 from ashmaroli/stackprof-no-jruby
Don't attempt to install stackprof gem on JRuby
2019-04-24 09:11:44 -04:00
Ashwin Maroli
1c577c5b62 Don't attempt to install stackprof gem on JRuby 2019-04-24 11:31:20 +05:30
David Cornu
755d2821f3 Merge pull request #1094 from Shopify/for-tag/invalid-limit-offset
Make sure the for tag's limit and offset are integers
2019-04-23 17:20:54 -04:00
David Cornu
495b3d312f Merge pull request #1095 from Shopify/travis/remove-rainbow-gem
Stop installing the rainbow gem on Travis
2019-04-23 17:20:38 -04:00
David Cornu
453f6348c2 Stop installing the rainbow gem on Travis 2019-04-23 16:55:37 -04:00
David Cornu
70ed1fc86d Make sure the limit and offset values are integers 2019-04-23 16:44:37 -04:00
Florian Weingarten
c2ef247be5 Merge pull request #1092 from Shopify/rake-memory-profiler-task
rake memory_profile:run
2019-04-22 16:33:32 -04:00
Florian Weingarten
1518d3f6f9 Merge pull request #1093 from Shopify/bytesize-not-length
use bytesize, not length
2019-04-18 18:39:21 +01:00
Florian Weingarten
c67b77709d rake memory_profile:run 2019-04-17 19:09:26 +01:00
Florian Weingarten
c89ce9c2ed use bytesize, not length 2019-04-17 18:55:13 +01:00
Richard Monette
b0629f17f7 Merge pull request #1073 from Shopify/defer-alloc-hash
defer hash allocation until needed for unparsed_args
2019-03-20 13:34:48 -04:00
Richard Monette
274f078806 defer hash allocation in parse_filter_expressions
add exploration of GC object allocation

remove performance test

can actually remove one more if branch

use named locals to improve readability
2019-03-20 13:20:31 -04:00
Richard Monette
d7171aa084 Merge pull request #1077 from Shopify/update-cops-for-trailing-comma
update Rubocop for trailing comma styles
2019-03-19 16:02:26 -04:00
Richard Monette
06c4789dc5 update Rubocop for trailing comma styles 2019-03-19 11:05:05 -04:00
Justin Li
f2f467bdbc v4.0.3 2019-03-12 12:43:48 -04:00
Justin Li
ff99d92c18 Merge pull request #1072 from Shopify/fix-interrupts
Fix interrupts through includes
2019-03-12 12:26:12 -04:00
Justin Li
39fecd06db Fix interrupts through includes 2019-03-12 12:18:22 -04:00
Justin Li
8013df8ca2 v4.0.2 2019-03-08 15:43:46 -05:00
Clayton Smith
14cd011cb5 Merge pull request #1070 from Shopify/url-decode-validation
Validate the character encoding in url_decode.
2019-03-08 11:09:40 -05:00
Clayton Smith
e2d9907df2 Validate the character encoding in url_decode. 2019-03-07 14:01:10 -05:00
Justin Li
23d669f5e6 Merge pull request #1032 from printercu/patch-1
Single regexp for strip_html
2019-02-22 13:04:04 -05:00
Justin Li
ed73794f82 Preserve existing strip_html behaviour for weird inputs 2019-02-22 13:00:36 -05:00
Ashwin Maroli
f59f6dea83 Fix simple RuboCop offenses and update TODO file (#1062)
* Fix Layout/EmptyLineAfterMagicComment offense

* Fix Layout/ExtraSpacing offense

* Fix Layout/ClosingParenthesisIndentation offenses

* Fix Style/MutableConstant offense

* Fix Style/UnneededInterpolation offenses

* Fix Style/RedundantParentheses offenses

* Update TODO config for RuboCop

* Add executable bit to test/test_helper.rb

ref: https://travis-ci.org/Shopify/liquid/jobs/488169512#L578
2019-02-22 12:32:56 -05:00
Garland Zhang
7a81fb821a Merge pull request #1059 from Shopify/map_error_checking
Apply error-checking to sort, sort_natural, where, uniq, map, compact filter(s)
2019-02-22 10:42:16 -05:00
Garland Zhang
cec27ea326 Extract raise error line and some filters with begin/rescue blocks 2019-02-21 17:00:20 -05:00
Justin Li
14999e8f7c Merge pull request #1053 from er1/update-changelog-v4.0.1
Updated changelog for v4.0.1 for (#1038)
2019-02-10 10:37:31 -05:00
Eric Chan
b41fc10d8e Updated changelog for v4.0.1 2018-12-03 23:54:00 -05:00
David Cornu
2b3c81cfd0 Merge pull request #1046 from Shopify/make-builds-green
Make builds green
2018-10-24 10:46:01 -04:00
David Cornu
2a2376bfd9 Run :test before :rubocop in the default Rake task 2018-10-19 15:06:36 -04:00
David Cornu
ca9e75db53 Reduce perceived complexity for #sort and #sort_natural 2018-10-19 14:57:33 -04:00
David Cornu
407c8abf30 Use TrailingCommaInLiteral
TrailingCommaInArrayLiteral and TrailingCommaInHashLiteral were introduced in v0.53.0 and we're running v0.49.0.

https://github.com/rubocop-hq/rubocop/blob/master/CHANGELOG.md#0530-2018-03-05
2018-10-19 14:52:16 -04:00
Justin Li
43f181e211 Merge pull request #1044 from Shopify/enable-cla-bot
Enable CLA bot
2018-10-19 09:34:30 -04:00
Tim Layton
7c613e87cb Enable CLA bot 2018-10-18 23:10:56 -07:00
Stephen Paul Weber (Work)
fe4034ccf9 Merge pull request #1025 from Shopify/traverse-ast
Liquid::ParseTreeVisitor
2018-10-18 09:42:56 -04:00
Stephen Paul Weber
52ee303a36 s/block.call/yield 2018-10-18 09:41:53 -04:00
Stephen Paul Weber
8217a8d86c Add test for the full array structure 2018-10-18 09:39:05 -04:00
Stephen Paul Weber
7d13d88258 s/Traversal/ParseTreeVisitor 2018-10-18 09:38:33 -04:00
Stephen Paul Weber
ff727016ef s/callback_for/add_callback_for 2018-10-18 09:37:48 -04:00
Stephen Paul Weber
c11fc656cf Colocate Traversal classes with classes they traverse
This puts all knowledge of the traversal in the same file, and removes
the need for a CASES registry.
2018-10-18 09:37:48 -04:00
Stephen Paul Weber
d789ec4175 Liquid::Traversal
This enables traversal over whole document tree.
2018-10-15 10:11:58 -04:00
Samuel Doiron
fd09f049b0 Merge pull request #1026 from Shopify/where-filter
Add `where` filter to standard filters
2018-10-11 17:45:30 -04:00
Samuel
842986a972 Add where filter to standard filters
Users of Liquid will often wish to filter an array to only those items that match a certain criteria. For example, showing "pinned" messages at the top of a list.

Example usage:

`{{ comments | where: "pinned" | first }}`

or

`{{ products | where: "category", "kitchen" }}`

* Add where filter to standard filters
* Add tests for new where functionality
2018-10-11 16:52:32 -04:00
Florian Weingarten
4661700a97 bump to v4.0.1 2018-10-09 11:13:19 +02:00
Justin Li
cd5a6dd225 Merge pull request #930 from er1/fix-sort-natural-on-nil
Fix sort and sort_natural on sorting with non-string and nil values
2018-10-04 22:32:37 -04:00
printercu
89c1ba2b0e Fix rubocop warning 2018-09-27 17:24:01 +03:00
printercu
479d8fb4a4 Single regexp for strip_html 2018-09-27 17:13:35 +03:00
Justin Li
53b8babf52 Merge pull request #1027 from Shopify/rubocop-fix
Update deprecated rubocop name
2018-09-13 17:16:36 -04:00
Justin Li
76b4920d3e Update deprecated rubocop name 2018-09-13 17:15:32 -04:00
Justin Li
8dcc319128 Merge pull request #1024 from koic/suppress_warning_bigdecimal_new
Suppress warning: `BigDecimal.new` is deprecated
2018-09-09 08:28:24 -04:00
Koichi ITO
0b36461d80 Suppress warning: BigDecimal.new is deprecated
## Summary

`BigDecimal.new` is deprecated since BigDecimal 1.3.3 for Ruby 2.5.

This PR suppresses the following warnings.

```console
% ruby -v
ruby 2.6.0dev (2018-09-06 trunk 64648) [x86_64-darwin17]
% RUBYOPT=-w bundle exec rake
(snip)
/Users/koic/src/github.com/Shopify/liquid/lib/liquid/utils.rb:49:
warning: BigDecimal.new is deprecated; use Kernel.BigDecimal method
instead.
/Users/koic/src/github.com/Shopify/liquid/lib/liquid/utils.rb:53:
warning: BigDecimal.new is deprecated; use Kernel.BigDecimal method
instead.
```

## Other Information

The following is a change of BigDecimal 1.3.3 for Ruby 2.5 related to this PR.

- 533737338d
- 16738ad0ac
2018-09-09 21:10:20 +09:00
Justin Li
70e75719de Merge pull request #1010 from Shopify/circle-ci-remove-38409b
Goodbye CircleCI 👋
2018-05-15 10:53:51 -04:00
shopify-admins
b037b19688 Removing CircleCI 1.0 [ci skip] 2018-05-15 10:35:38 -04:00
Florian Weingarten
d0f77f6cf4 Merge pull request #1006 from Benhgift/master
add installation instruction
2018-04-26 18:20:02 +01:00
Ben Gift
0be260bc97 add installation instruction 2018-04-26 08:12:47 -07:00
Dylan Thacker-Smith
5f0b64cebc Merge pull request #1005 from christopheraue/render_refactor
Refactored and optimized rendering
2018-04-19 16:44:57 -04:00
Christopher Aue
c086017bc9 refactored and optimized rendering
Measures:
1) A while loop is faster than iterating with #each.
2) Check string, variable and block tokens first. They are far more
   frequent than interrupt tokens. In their case, checking for an
   interrupt can be avoided.
3) String tokens just map to themselves and don't need the special
   treatment of BlockBody#render_node (except the resource limit
   check).

Benchmark
=========

$ bundle exec rake benchmark:run

Before
------

Run 1)
              parse:     41.630  (± 0.0%) i/s -    420.000  in  10.089309s
             render:     75.962  (± 3.9%) i/s -    763.000  in  10.066823s
     parse & render:     25.497  (± 0.0%) i/s -    256.000  in  10.040862s

Run 2)
              parse:     42.130  (± 0.0%) i/s -    424.000  in  10.064738s
             render:     77.003  (± 1.3%) i/s -    777.000  in  10.093524s
     parse & render:     25.739  (± 0.0%) i/s -    258.000  in  10.024581s

Run 3)
              parse:     41.976  (± 2.4%) i/s -    420.000  in  10.021406s
             render:     76.184  (± 1.3%) i/s -    763.000  in  10.018104s
     parse & render:     25.641  (± 0.0%) i/s -    258.000  in  10.062549s

After
-----

Run 1)
              parse:     42.283  (± 0.0%) i/s -    424.000  in  10.028306s
             render:     83.158  (± 2.4%) i/s -    832.000  in  10.009201s
     parse & render:     26.417  (± 0.0%) i/s -    266.000  in  10.069718s

Run 2)
              parse:     41.159  (± 4.9%) i/s -    412.000  in  10.031297s
             render:     81.591  (± 3.7%) i/s -    816.000  in  10.018225s
     parse & render:     25.924  (± 3.9%) i/s -    260.000  in  10.035653s

Run 3)
              parse:     42.418  (± 2.4%) i/s -    424.000  in  10.003100s
             render:     84.183  (± 2.4%) i/s -    847.000  in  10.069781s
     parse & render:     26.726  (± 0.0%) i/s -    268.000  in  10.029857s
2018-04-19 12:10:15 +02:00
Dylan Thacker-Smith
4369fe6c85 Improve the unexpected end delimiter message for block tags. (#1003) 2018-04-05 11:18:13 -04:00
Justin Li
c118e6b435 Merge pull request #992 from ashmaroli/each-without-index
Replace unnecessary Array#each_with_index with Array#each
2018-03-16 14:28:05 -04:00
Ashwin Maroli
0fbaf873d9 replace unnecessary #each_with_index with #each 2018-03-16 14:31:43 +05:30
Justin Li
5980ddbfae Merge pull request #988 from ashmaroli/regex-to-constant
Assign regexps to constants
2018-03-14 16:49:17 -04:00
Ashwin Maroli
193fc0fb7a revert to earlier regex for matching floats 2018-03-14 07:02:04 +05:30
Ashwin Maroli
e4da4d49d2 assign regex to a constant 2018-03-13 23:36:56 +05:30
Justin Li
a0bec1f873 Merge pull request #981 from nicolasleger/patch-1
[CI] Test against Ruby 2.5 version
2018-03-05 11:23:18 -05:00
Nicolas Leger
4aa3261518 [CI] Test against Ruby 2.5 version 2018-02-12 00:23:06 +01:00
Dylan Thacker-Smith
04d552fabb Gemfile: Use https rather than git protocol to fetch liquid-c 2018-02-01 07:08:19 -05:00
Dylan Thacker-Smith
5106466a2d Add a regression test for a liquid-c trim mode bug (#972) 2018-01-25 10:55:01 -05:00
Justin Li
5d6c1ed7c6 Merge pull request #963 from lostapathy/patch-1
have travis test against ruby 2.4
2017-12-15 16:53:26 -05:00
Joe Francis
a594653a0c have travis test against ruby 2.4 2017-12-15 14:27:17 -06:00
Thibaut Courouble
0c802aba17 Merge pull request #958 from Shopify/minmax
Rename min/max filters for clarity
2017-12-06 11:41:12 -05:00
Thibaut Courouble
147d7ae24d Rename min/max filters for clarity 2017-12-06 09:48:30 -05:00
Thibaut Courouble
282d42f98d Fix min/max filters 2017-12-06 08:58:05 -05:00
Justin Li
e6ba6ee87b Revert "Use replacement string for replace filters literally (#924)"
This reverts commit 27c91203ab.
2017-12-04 15:07:59 -05:00
Nithin Bekal
2ad7a37d44 Merge pull request #954 from Shopify/max-min-filters
Add max and min filters
2017-11-30 14:18:43 -05:00
Nithin Bekal
4bdaaf069f Add max/min filters 2017-11-30 13:56:37 -05:00
Justin Li
85b1e91aed Merge pull request #952 from Shopify/bump-rubocop
Bump rubocop
2017-11-22 12:44:36 -05:00
Justin Li
a7c5e247c8 Bump rubocop 2017-11-22 11:59:06 -05:00
Dylan Thacker-Smith
6c117fd7dd refactor: Reduce maximum block nesting in Liquid::BlockBody#parse (#944) 2017-10-19 10:12:40 -04:00
Maxime Bedard
7d2d90d715 Merge pull request #932 from Shopify/avoid-default-values-hash
Avoid hash with default values due to inconsistent marshaling
2017-10-17 16:02:45 -04:00
Maxime Bedard
f761d21215 Use {} notation 2017-09-20 09:48:23 -04:00
Maxime Bedard
a796c17f8b Avoid hash with default values due to inconsistent marshalling 2017-09-19 16:23:14 -04:00
Eric Chan
deb10ebc7a Sorting support for data with undefined values 2017-09-14 02:00:43 -04:00
Eric Chan
cfe1844de9 Added test coverage for sort_natural 2017-09-13 22:17:59 -04:00
Eric Chan
59950bff87 Fix sort_natural on sorting with non-string values 2017-09-13 01:37:40 -04:00
Dylan Thacker-Smith
27c91203ab Use replacement string for replace filters literally (#924) 2017-08-28 11:51:20 -04:00
Justin Li
44eaa4b9d8 Merge pull request #920 from Shopify/symbol_to_liquid
Support rendering symbols as strings
2017-08-18 12:10:53 -04:00
Pascal Betz
a979b3ec95 Do not raise when variable is defined but nil when using strict_variables 2017-08-18 12:09:57 -04:00
Justin Li
bf3e759da3 Support rendering symbols as strings 2017-08-17 23:10:57 -04:00
Rene
59162f7a0e added attr_readers for collection and variable names in for tag (#909) 2017-07-06 09:41:48 -04:00
Thierry Joyal
c582b86f16 Merge pull request #898 from Shopify/cgi-powered-standard-filters-to-handle-non-string-inputs
CGI powered standard filters to handle non string inputs
2017-05-26 18:05:42 +00:00
Thierry Joyal
e340803d12 CGI powered standard filters to handle non string inputs 2017-05-25 15:53:41 +00:00
Dylan Thacker-Smith
48a6d86ac2 Use stackprof to test to lack of object allocations (#896) 2017-05-12 09:20:51 -04:00
Dylan Thacker-Smith
3bb29d5456 Replace assert_equal nil, with a assert_nil (#895) 2017-05-11 14:05:03 -04:00
Dylan Thacker-Smith
9c72ccb82f Limit how much blocks can be nested during parsing (#894) 2017-05-11 09:37:53 -04:00
Dylan Thacker-Smith
62d4625468 Use a loop to strictly parse binary comparisons to avoid recursion (#892)
Using recursion allows a malicious template to cause a SystemStackError
2017-05-10 10:41:52 -04:00
Dylan Thacker-Smith
8928454e29 Use a loop to evaluate binary comparisions to avoid recursion (#891)
Using recursion allows a malicious template to cause a SystemStackError
2017-05-10 10:41:24 -04:00
Florian Weingarten
1370a102c9 Merge pull request #789 from evulse/contains-strict-fix
Allow variables to start with contains in strict parser
2017-03-24 09:50:31 -04:00
Mike Angell
c9bac9befe Merge branch 'master' into contains-strict-fix 2017-03-24 11:09:09 +10:00
Mike
210a0616f3 Update History to include fix 2017-03-24 10:35:56 +10:00
Lasse Skindstad Ebert
5149cde5c3 Fix include tag used with strict_variables (#829)
Fixes https://github.com/Shopify/liquid/issues/828
2017-03-22 16:00:31 -04:00
Florian Weingarten
22f2cec5de Merge pull request #864 from chenxianyu2015/fix-strainer-add_filter-method
fix  #861: duplicate inclusion condition logic error of Liquid::Strainer.add_filter method
2017-02-23 14:06:20 -05:00
chenxianyu
4318240ae0 test: modify Strainer.add_filter duplicate inclusion test case 2017-02-22 10:33:22 +08:00
chenxianyu
aa79c33dda fix: Strainer.add_filter method 2017-02-13 15:50:19 +08:00
Justin Li
b1ef28566e Merge pull request #846 from mrmanc/master
Clarifies spelling of for’s reversed flag to address #843
2017-02-10 19:26:38 -05:00
Justin Li
41bcc48222 Merge pull request #854 from jaredbeck/patch-1
Docs: Help people upgrade to 4, re: liquid_methods
2017-02-10 19:25:04 -05:00
Dylan Thacker-Smith
27d5106dc9 Merge pull request #860 from Shopify/handle-string-node-render-exc
Avoid calling line_number on String node when rescuing a render error.
2017-02-10 14:13:11 -05:00
Dylan Thacker-Smith
7334073be2 Avoid duck typing to detect whether to call render on a node. 2017-02-10 13:49:26 -05:00
Dylan Thacker-Smith
5dcefd7d77 Avoid calling line_number on String node when rescuing a render error. 2017-02-07 15:34:10 -05:00
Richard Monette
25c7b05916 Merge pull request #857 from Shopify/handle-join-on-fixnum
handle join on fixnum
2017-02-01 14:25:40 -05:00
Richard Monette
d17f86ba4d handle join on fixnum 2017-02-01 12:47:35 -05:00
Jerry Liu
384e4313ff Merge pull request #851 from Shopify/benchmark-render
Allow benchmarks to benchmark render by itself
2017-01-31 17:18:56 -05:00
Jerry Liu
23f2af8ff5 fix travis build 2017-01-31 17:04:36 -05:00
Jerry Liu
a93eac0268 Introduce new benchmarking methods to liquid to use on rubybench 2017-01-27 10:56:16 -05:00
Florian Weingarten
2cc7493cb0 Merge pull request #855 from Shopify/bundler-benchmark-group
Create a benchmark group in Gemfile
2017-01-20 16:41:11 -05:00
Jerry Liu
85463e1753 add benchmark-ips to benchmark group in Gemfile 2017-01-20 16:04:42 -05:00
Jared Beck
52ff9b0e84 Docs: Help people upgrade to 4, re: liquid_methods
The discussion in #568 helped me.

[ci skip]
2017-01-19 14:23:39 -05:00
Dylan Thacker-Smith
0c58328a40 test: Equality comparison of two hashes (#850) 2017-01-16 15:56:38 -05:00
Dylan Thacker-Smith
2bb3552033 Fix internal liquid error when comparing hash with incompatible type (#849) 2017-01-16 13:13:17 -05:00
Mark Crossfield
8b751ddf46 Removes a non ascii character from comment to appease Rubocop 2017-01-09 10:16:35 +00:00
Mark Crossfield
e5cbdb2b27 Clarifies spelling of for’s reversed flag to address #843
It should now be harder to read the docs and miss the extra letter required for reversed compared to reverse, which causes a fairly generic syntax warning when trying to reverse sort a collection in a for loop.
2017-01-08 12:44:12 +00:00
Justin Li
ffb0ace303 Update changelog for 4.0.0 2016-12-16 13:11:22 -05:00
Florian Weingarten
ad00998ef8 bump to v4 2016-12-14 11:58:42 -05:00
Dylan Thacker-Smith
869dbc7ebf feature: Allow a default exception renderer to be specified (#837)
This could be used to preserve the old default of rendering
non-Liquid::Error messages or for providing default behaviour like error
reporting which could be missed if the exception renderer needed to be
specified on each render.
2016-12-12 10:29:09 -05:00
Dylan Thacker-Smith
fae3a2de7b Add version constraint to rake to fix CI (#836) 2016-12-09 14:01:15 -05:00
Dylan Thacker-Smith
f27bd619b9 change: Render an opaque internal error by default for non-Liquid::Error (#835)
These errors may contain sensitive information, so is safer to
render a more vague message by default.

This is done by replacing non-Liquid::Error exceptions with a
Liquid::InternalError exception with the non-Liquid::Error accessible on
through the cause method. This also allows the template name and line
number to be attached to the template errors.

The exception_handler render option has been changed to exception_renderer
since now it should raise an exception to re-raise on a liquid rendering
error or return a string to be rendered where the error occurred.
2016-12-07 17:34:29 -05:00
Dylan Thacker-Smith
a9b84b7806 test: Use ruby 2.1 in Circle CI 2016-12-05 15:36:42 -05:00
Dylan Thacker-Smith
6cc2c567c5 Merge pull request #832 from Shopify/drop-ruby-2.0
Drop support for ruby 2.0
2016-12-05 13:56:15 -05:00
Dylan Thacker-Smith
812e3c51b9 test: Add ruby 2.3.3 to CI
Travis doesn't have a ruby 2.3 alias, so the latest 2.3.x version is
specified instead.
2016-12-05 13:53:02 -05:00
Dylan Thacker-Smith
9dd0801f5c Drop support for ruby 2.0
It is no longer maintained upstream
2016-12-05 13:51:49 -05:00
Dylan Thacker-Smith
b146b49f46 fix: Clear the strainer cache when a global filter is added (#826) 2016-11-24 10:32:11 -05:00
Richard Monette
86944fe7b7 Merge pull request #809 from Shopify/introduce-unhandled-liquid-exception
introduce unhandled liquid exception
2016-10-31 10:20:06 -04:00
Richard Monette
a549d289d7 introduce unhandled liquid exception
check arity
2016-10-28 09:40:44 -04:00
Richard Monette
b2feeacbce Merge pull request #812 from Shopify/allow-split-to-accept-numeric
allow split to accept numeric
2016-10-26 10:59:44 -04:00
Richard Monette
143ba39a08 allow split to accept numeric 2016-10-26 10:43:04 -04:00
Richard Monette
43e59796f6 Merge pull request #805 from Shopify/dont-explode-when-sorting-nil-property
dont explode when sorting nil property
2016-10-05 10:18:16 -04:00
Richard Monette
bb3624b799 dont explode when sorting nil property 2016-10-04 13:22:29 -04:00
Konstantin Tennhard
64fca66ef5 Merge pull request #797 from Shopify/truncatewords-resiliency
Standard filter truncate / truncatewords: force truncate_string to string
2016-09-13 10:43:55 -04:00
Florian Weingarten
e9d7486758 4.0.0.rc3 2016-09-13 06:33:20 -04:00
Philibert Dugas
2bb98c1431 Merge pull request #798 from PhilibertDugas/bugfix-#697
Fixing #697 with better exception
2016-09-12 13:53:22 -04:00
Konstantin Tennhard
95d5c24bfc Standard filter truncate: truncate_string string coercion
The argument `truncate_string` is now coerced into a string to avoid
`NoMethodError`s. This is mostly for added resiliency. It is doubtful
that someone would actually intent to use a number as truncate string,
but accidentally supplying one is entirely possible.
2016-09-12 12:13:12 -04:00
Philibert Dugas
b7ee1a2176 Fixing #697 with better exception
When including a template which is not defined, the exception raised is
*undefined method `split` for nil:NilClass*

This occurs for a scenario like the following:
`{% include nil %}`
or
`{% include undefined-var %}`

Making the code raise an argument error to allow better understanding of
the include error
2016-09-12 09:31:59 -04:00
Florian Weingarten
0eca61a977 Merge pull request #799 from kainjow/patch-1
Update liquid-c
2016-09-12 08:12:14 -04:00
Kevin Wojniak
9bfd04da2d Update liquid-c 2016-09-10 09:23:15 -07:00
Konstantin Tennhard
302185a7fc Standard filter truncatewords: force truncate_string to string
Currently, `truncatewords` raises a TypeError when the argument
`truncate_string` is an interger. This PR forces string coercion for any
value provided for this argument. Thus,

```ruby
assert_equal 'one two1', @filters.truncatewords("one two three", 2, 1)
```

holds true. Another option would be to raise a `Liquid::ArgumentError`.

What is preferred?
2016-09-09 16:50:50 -04:00
Michael Angell
6ed6e7e12f Allow :id to start with the word contains 2016-08-20 20:32:46 +10:00
Mike Angell
f41ed78378 Merge pull request #1 from Shopify/master
Pull inline with upstream
2016-08-17 21:30:08 +10:00
Florian Weingarten
50c85afc35 Merge pull request #786 from Shopify/bump-liquid-c
Bump LiquidC for whitespace changes
2016-08-11 13:38:42 -04:00
Florian Weingarten
5876dff326 Bump LiquidC for whitespace changes 2016-08-11 13:21:39 -04:00
Florian Weingarten
f25185631d Merge pull request #773 from evulse/whitespace-trim
Add whitespace control character and associated tests
2016-08-11 13:20:12 -04:00
Michael Angell
283f1bad18 Use .last instead of pop push method for updating last node in nodelist 2016-07-08 20:49:30 +10:00
Michael Angell
e1d40c7d89 Add whitespace control character and associated tests 2016-06-28 09:15:45 +10:00
Justin Li
19c6eb426a Merge pull request #769 from zacstewart/patch-1
Fix doc formatting of code examples in file_system
2016-06-15 17:10:11 -04:00
Zac Stewart
f87b06095d Fix doc formatting of code examples in file_system
These code examples are being rendered as paragraph text in the docs.
2016-06-15 15:34:14 -04:00
Gaurav Chande
b81d54e789 Merge pull request #761 from Shopify/range-to_liquid
Support Range Type
2016-06-02 16:48:30 -04:00
Gaurav Chande
00f53b16e8 Prevent Range usage in templates from blowing up 2016-06-02 16:38:44 -04:00
Gaurav Chande
e4cf55b112 Merge pull request #748 from Shopify/expose-tags
Make Template.tags loop-able
2016-04-25 11:59:37 -04:00
Gaurav Chande
5bb211d933 Ensure no tag leakage since registry is global 2016-04-25 11:50:46 -04:00
Gaurav Chande
6adc431a19 Make tag registry enumerable 2016-04-25 11:38:42 -04:00
Justin Li
23d2beed41 Merge pull request #744 from Shopify/raw-syntax-method
Make markup validation a method on Liquid::Raw
2016-04-13 17:08:02 -04:00
Drew Martin
a80ecb7678 make markup validation a method on Liquid::Raw 2016-04-13 14:52:30 -04:00
Florian Weingarten
361c695264 Merge pull request #736 from Shopify/abs-filter
Abs filter
2016-04-05 09:13:56 -04:00
Florian Weingarten
f93243cc1a abs filter 2016-04-04 09:32:31 -04:00
Florian Weingarten
1e533a52e7 Merge pull request #735 from Shopify/fix-to-number-for-negative-float-strings
Fix to_number filter for negative float strings
2016-03-31 15:52:51 -04:00
Dylan Thacker-Smith
3ea84f095f Merge pull request #734 from Shopify/concat-liquid-error
Raise a Liquid::Error when a non-array is passed into the concat filter.
2016-03-31 15:47:43 -04:00
Dylan Thacker-Smith
4239c899a4 Raise a Liquid::Error when a non-array is passed into the concat filter. 2016-03-31 15:47:06 -04:00
Florian Weingarten
1597f8859f Fix to_number filter for negative float strings 2016-03-31 09:18:55 -04:00
Florian Weingarten
b3dda384c9 Merge pull request #733 from Shopify/fix-some-ruby-warnings
Fix a bunch of Ruby warnings
2016-03-30 17:09:00 -04:00
Florian Weingarten
6828670bfe Merge pull request #732 from Shopify/v400rc2
Release v4.0.0rc2
2016-03-30 17:02:34 -04:00
Florian Weingarten
d2f16d92d6 Fix a bunch of Ruby warnings 2016-03-30 20:42:30 +00:00
Justin Li
d233acb483 Update history to reflect merge of #731 2016-03-30 16:36:57 -04:00
Florian Weingarten
8920e2a2a2 Release v4.0.0rc2 2016-03-30 20:13:21 +00:00
Justin Li
bfee507005 Merge pull request #731 from ismasan/duck_typed_maths_filters
Duck typed maths filters
2016-03-30 16:09:16 -04:00
Ismael Celis
929c89789f Test that all maths filters work with duck-typed #to_number 2016-03-30 13:35:09 -03:00
Ismael Celis
d03c4ae8e8 Allow Utils.to_number to work with anything that responds to #to_number 2016-03-30 01:57:21 -03:00
Justin Li
021bafd260 Merge pull request #725 from jeroenvisser101/performance-start-with-vs-regex
Use start_with? instead of Regex
2016-03-21 10:34:28 -04:00
Jeroen Visser
04c393ab07 Use start_with? instead of Regex
Performance is increased by doing this:

  require 'benchmark'
  require 'tempfile'

  n = 50000
  test = File.expand_path(Tempfile.new('foo'))
  Benchmark.bm(20) do |x|
    x.report("Regex:") do
      n.times { test =~ /\A#{test}/ }
    end
    x.report("String#start_with?:") do
      n.times { test =~ test.start_with?(test) }
    end
  end

Benchmark result:
                             user     system      total        real
  Regex:                 0.440000   0.010000   0.450000 (  0.447357)
  String#start_with?:    0.000000   0.000000   0.000000 (  0.006313)
2016-03-21 14:23:35 +01:00
Gaurav Chande
9a7778e52c Merge pull request #707 from Shopify/drop-without-context
@context not always present on a Drop
2016-03-01 18:07:16 -05:00
Gaurav Chande
dde00253f9 context is not always present on a Drop 2016-03-01 21:22:11 +00:00
Gaurav Chande
18d1644980 Merge pull request #705 from Shopify/register-filter-warn
Strainer#add_filter Raises on Private Override
2016-02-24 15:31:41 -05:00
Gaurav Chande
c424d47274 Add changelog entry for Strainer method override change 2016-02-24 20:23:57 +00:00
Gaurav Chande
8e6b9d503d Make Strainer also raise when registered method is overriden as protected 2016-02-24 20:23:49 +00:00
Gaurav Chande
8be38d1795 Strainer#add_filter should raise when registered method is overriden as private 2016-02-24 20:03:17 +00:00
Justin Li
3146d5c3f2 Grammatic and other fixes to CONTRIBUTING.md 2016-02-02 23:45:37 -05:00
Justin Li
0cc8b68a97 Make logic in Context#lookup_and_evaluate more understandable 2016-02-02 23:22:46 -05:00
Justin Li
5a50c12953 Update history to reflect merge of #691
[ci skip]
2016-02-02 23:14:41 -05:00
Justin Li
a6fa4c5c38 Merge pull request #691 from urbandictionary/missing_variables_and_filters
Merge pull request 691
2016-02-02 23:13:44 -05:00
Ivan Kuznetsov
dadd9b4dd2 Add strict_variables/strict_filters render options to check for undefined variables and filters 2016-02-03 10:49:33 +07:00
Justin Li
6434b8d2bb Merge pull request #696 from Shopify/no-send
Remove possibility for arbitrary sends
2016-02-02 11:01:46 -05:00
Justin Li
2d891ddd8f Merge pull request #695 from Shopify/assign-score
Take nested values into account for assign score
2016-02-01 13:14:40 -05:00
Justin Li
60b508b151 Use #each to avoid extra allocations 2016-02-01 13:01:25 -05:00
Justin Li
3891f14a1a Take nested values into account for assign score 2016-02-01 13:01:25 -05:00
Justin Li
198f0aa366 Add test for nested assign score bookkeeping 2016-02-01 13:01:23 -05:00
Justin Li
f2e6adf566 Remove arbitrary send vector 2016-02-01 11:38:40 -05:00
Justin Li
08de6ed2c5 Merge pull request #687 from pathawks/default
Performance improvement: `default` filter
2016-01-24 11:34:05 -05:00
Pat Hawks
7e322f5cf8 Performance improvement: default filter 2016-01-23 23:18:51 -08:00
Justin Li
bf86a5a069 Merge pull request #688 from Shopify/gmp
Install libgmp3-dev in travis
2016-01-23 21:46:37 -05:00
Justin Li
0141444814 Install libgmp3-dev in travis 2016-01-23 21:41:14 -05:00
Justin Li
6d30226768 Update changelog for 4.0.0rc1 2016-01-08 15:08:06 -05:00
Florian Weingarten
63e8bac1a4 meh 2016-01-08 20:00:45 +00:00
Florian Weingarten
8449849ed5 Merge pull request #682 from Shopify/4-pre-beta1
4.0.0.rc1
2016-01-08 20:59:59 +01:00
Florian Weingarten
4bc198a0db 4.0.0.rc1 2016-01-08 19:59:38 +00:00
Florian Weingarten
3921dbe919 Merge pull request #683 from Shopify/dropify-tablerowloop
Liquid::TablerowloopDrop
2016-01-08 20:41:54 +01:00
Florian Weingarten
79e2d1d8b4 Liquid::TablerowloopDrop 2016-01-08 18:46:23 +00:00
Florian Weingarten
b7c4041db8 Merge pull request #681 from Shopify/save-some-loop-allocations
Reuse 'forloop' hash to save memory allocations
2016-01-06 22:47:39 +01:00
Florian Weingarten
e113c891ec Convert forloop hash to drop 2016-01-06 21:30:32 +00:00
Guillaume Malette
a32ad449c0 Merge pull request #672 from Shopify/fix-proc-mapping
Test mapping over procs
2016-01-06 15:59:53 -05:00
Florian Weingarten
1662ba6679 Reuse 'forloop' hash to save memory allocations 2016-01-06 20:30:25 +00:00
Dylan Thacker-Smith
99b5e86f0a Merge pull request #680 from jcheatham/master
Ensure truncate is operating on a string
2015-12-23 18:41:39 -05:00
Jonathan Cheatham
b892a73463 Ensure truncate is operating on a string 2015-12-22 19:40:35 -08:00
Guillaume Malette
0b55d09cea Fix mapping over proc attributes 2015-11-20 13:04:42 -05:00
Dylan Thacker-Smith
5f8086572b Merge pull request #667 from Shopify/remove-empty-string-check
Remove nil and empty string check in invoke_drop.
2015-11-10 10:43:11 -05:00
Dylan Thacker-Smith
bdb9a4a47f Remove nil and empty string check in invoke_drop. 2015-11-09 15:03:36 -05:00
Dylan Thacker-Smith
c38eec0293 Merge pull request #665 from tanelj/escape_filter_nil_fix
Fixed issue where "nil" value for "escape" filter breaks rendering
2015-11-06 10:54:48 -05:00
Tanel Jakobsoo
8d5a907dc8 Fixed issue where "nil" value for "escape" filter breaks rendering
Closes #664
2015-11-06 16:32:02 +02:00
Florian Weingarten
74cc41ce74 Merge pull request #662 from nickpearson/keep-argument-error-backtrace
Keep original stack trace in Liquid::ArgumentError
2015-10-29 15:24:54 +01:00
Thierry Joyal
a120cc587a Merge pull request #661 from Shopify/rename-before-method-as-dynamic-method
Rename before_method as liquid_method_missing
2015-10-29 09:49:15 -04:00
Nick Pearson
c582023321 Keep original stack trace in Liquid::ArgumentError 2015-10-29 08:15:37 -05:00
Thierry Joyal
ac041c4ad1 Rename before_method as liquid_method_missing 2015-10-28 17:28:19 +00:00
Justin Li
31d7682f4e Update history to reflect merge of #658
[ci skip]
2015-10-21 12:50:12 -04:00
Justin Li
5f1acbc086 Merge pull request #658 from Shopify/url_decode-filter
Merge pull request 658
2015-10-21 12:49:14 -04:00
Justin Li
8612716129 Remove rescue in unescape filter 2015-10-21 02:01:21 -04:00
Larry Archer
e6392d1cc1 Tests for new url_decode filter 2015-10-21 01:58:22 -04:00
Larry Archer
04381418d3 Add url_decode filter to accompany url_encode 2015-10-21 01:58:22 -04:00
Justin Li
89ccdabe9a Merge pull request #655 from dijonkitchen/patch-1
Rename MIT-LICENSE to LICENSE
2015-10-14 12:08:37 -04:00
Jonathan Chen
c0fc6777b0 Rename MIT-LICENSE to LICENSE
Standard name format
2015-10-14 12:06:08 -04:00
Justin Li
cd03346239 Update history to reflect merge of #652
[ci skip]
2015-09-29 21:06:21 -04:00
Justin Li
b4f19da127 Merge pull request #652 from mcary/empty-array-sort
Merge pull request 652
2015-09-29 21:05:10 -04:00
Marcel M. Cary
4100f8d641 Fix "sort" filter on empty array to return empty array
When sorting an empty array with the "sort" filter, it returns nil
instead of [].  This confuses subsequent filters in the chain that
expect an array.  For example, when followed by the "map" filter, it
produces an array containing one nil element: [nil].

I could special-case the nil return value, but that would be more
cumbersome than making sure "sort" always returns an array.

Add a case to the "sort" method to return [] if the array is empty,
before performing any checks on ary.first that assume a non-empty array.

There is still a danger of returning nil if the first item in the array
is nil and it is non-empty, but I'm not sure how better to handle that
case.

Apply a similar fix to sort_natural, uniq, and compact filters.
2015-09-29 10:24:31 -07:00
Dylan Thacker-Smith
d8bda2c892 Merge pull request #653 from Shopify/fix-rubocop-offenses
Fix offenses from the new version of rubocop.
2015-09-25 19:48:09 -04:00
Dylan Thacker-Smith
4f81c0a658 Lock rubocop version to avoid CI failures from new releases. 2015-09-25 19:42:35 -04:00
Dylan Thacker-Smith
704937bc00 Fix offenses from the new version of rubocop. 2015-09-25 19:34:44 -04:00
Justin Li
27c6b8074a Update history to reflect merge of #610
[ci skip]
2015-08-03 20:51:41 -04:00
Justin Li
affae5ebef Merge pull request #610 from boobooninja/gf3
Merge pull request 610
2015-08-03 20:50:14 -04:00
Florian Weingarten
fc1c0d0d83 Merge pull request #632 from knu/fix_date_error
Properly rescue ::ArgumentError in the date filter
2015-07-24 10:50:52 -04:00
Akinori MUSHA
a215b70de9 Properly rescue ::ArgumentError in the date filter 2015-07-24 13:35:06 +09:00
Justin Li
1f70928f8a Update history to reflect merge of #631
[ci skip]
2015-07-23 17:07:40 -04:00
Justin Li
7713f6709d Update history for 3.0.5 2015-07-23 17:06:12 -04:00
Justin Li
239cf0e5f5 Update history for 2.6.3 2015-07-23 17:05:58 -04:00
Dylan Thacker-Smith
fa187665b3 Merge pull request #631 from Shopify/fix-tz-test-failure
Fix a timezone test failure.
2015-07-23 16:34:48 -04:00
Dylan Thacker-Smith
cd0c5e954c Fix a timezone test failure. 2015-07-23 16:19:59 -04:00
Florian Weingarten
490b457738 Merge pull request #626 from Shopify/fix_bracket_thing
Fix bracket thing
2015-07-17 17:19:06 +02:00
Florian Weingarten
4d6dec9b5a Fix chained access to multi-dimensional hash 2015-07-17 10:10:00 -04:00
Loren Hale
0b11b573d9 add global_filter
add a global filter using a proc
only add one proc and not an array
add tests to make sure the global_filter is applied after native filters
2015-07-12 16:46:43 +08:00
Justin Li
b42d35ff36 Merge pull request #620 from Shopify/accept-invalid-range-args
Add param to accept invalid input in to_integer
2015-07-09 13:24:28 -04:00
Justin Li
b4e133e26f Fix regression in range lookup 2015-07-09 13:21:46 -04:00
Justin Li
1f9bd1d809 Add param to accept invalid input in to_integer 2015-07-09 13:18:06 -04:00
Justin Li
e88be60818 Merge pull request #618 from Shopify/move-reraise-for-line-number
Move the syntax error rescue for adding error line numbers.
2015-07-09 11:42:41 -04:00
Dylan Thacker-Smith
14416b3c49 Move the syntax error rescue for adding error line numbers. 2015-07-09 11:25:05 -04:00
Dylan Thacker-Smith
bde14a650d Merge pull request #617 from Shopify/rename-options-iv
Rename options instance variable in Variable and Tag.
2015-07-08 20:50:20 -04:00
Dylan Thacker-Smith
c535af021a Rename options instance variable in Variable and Tag. 2015-07-08 19:59:44 -04:00
Dylan Thacker-Smith
9c9345869b Merge pull request #614 from Shopify/remove-token-class
Implement line numbers without the Liquid::Token class.
2015-07-08 19:48:55 -04:00
Dylan Thacker-Smith
73834a7e52 Use reject rather than dup and delete. 2015-07-08 19:27:24 -04:00
Dylan Thacker-Smith
c45310170b Use parse_context or options instead of @options. 2015-07-08 19:21:59 -04:00
Dylan Thacker-Smith
920e1df643 Rescue and re-raise syntax errors in Template#parse to add line numbers.
This can be done now that the parse context has the line number
information, so it doesn't need to be added on closer to the original
exception.  This has the advantage of not having to rescue and re-raise the
exception multiple times, and simplifies liquid-c which would otherwise
have to rescue the exception in BlockBody#parse.
2015-07-08 19:21:59 -04:00
Dylan Thacker-Smith
cebf75b8d7 Implement line numbers without the Liquid::Token class. 2015-07-08 19:21:59 -04:00
Justin Li
afda01adbb Merge pull request #616 from Shopify/handle-non-int-range-args
Handle non-int range lookup arguments
2015-07-08 17:47:27 -04:00
Justin Li
959cd6d2a2 Temporarily disable rubinius in CI
It takes much longer than the others and is currently broken
2015-07-08 17:47:05 -04:00
Justin Li
4c1b89e20e Add regression test for ranges on non-integer types 2015-07-08 17:41:18 -04:00
Justin Li
83b6dd0268 Use to_integer for range lookup arguments 2015-07-08 17:37:07 -04:00
Justin Li
6fb402e60d Move to_integer, to_date, and to_number to Liquid::Utils 2015-07-08 17:33:05 -04:00
Dylan Thacker-Smith
338287df5e Merge pull request #613 from Shopify/taint-context-warning
Add taint warnings to the context rather than the template.
2015-07-07 16:23:10 -04:00
Dylan Thacker-Smith
c4c398174b Use early returns rather than large if in Variable#taint_check 2015-07-07 15:56:03 -04:00
Dylan Thacker-Smith
80b6ac3bc7 Add taint warnings to the context rather than the template. 2015-07-07 15:53:02 -04:00
Dylan Thacker-Smith
15974d9168 Merge pull request #612 from Shopify/fix-block-body-naming
Use node to refer to objects from the nodelist rather than token.
2015-07-07 15:49:58 -04:00
Dylan Thacker-Smith
f22ab4358b Merge pull request #611 from Shopify/no-escape-rescue
Remove standard exception rescue in escape filter.
2015-07-07 15:49:43 -04:00
Justin Li
9cf0d264e1 Require RuboCop v0.32.0 or later 2015-07-06 15:58:36 -04:00
Justin Li
575e3cae7a Remove class length metric cop 2015-07-06 15:52:11 -04:00
Dylan Thacker-Smith
fad3b8275c Use node to refer to objects from the nodelist rather than token. 2015-07-04 20:57:35 -04:00
Dylan Thacker-Smith
5a071cb7f2 Remove standard exception rescue in escape filter. 2015-07-04 13:48:25 -04:00
Justin Li
8cb2364179 Merge pull request #608 from Shopify/tag-tag_name
Add Liquid::Tag#tag_name
2015-07-02 16:28:37 -04:00
Gaurav Chande
3c23cfc167 Add Liquid::Tag#tag_name 2015-07-02 20:18:09 +00:00
Justin Li
8a8de46c6a Merge pull request #603 from Shopify/format-history
Format changelog attribution to include one name only
2015-06-23 07:40:05 -07:00
Justin Li
58c7f226cc Format changelog attribution to include one name only 2015-06-19 11:45:37 -04:00
Justin Li
adfcd0ab13 Update history to reflect merge of #600
[ci skip]
2015-06-19 11:38:59 -04:00
Justin Li
30ef7d14b0 Merge pull request #600 from carsonreinke/filter-compact
Merge pull request 600
2015-06-19 11:38:14 -04:00
Florian Weingarten
4920ec50e4 update changelog 2015-06-19 07:41:39 -04:00
David Cornu
e395229283 Merge pull request #601 from Shopify/safe-to-integer
Use to_integer instead of to_i on arguments
2015-06-16 11:31:20 -04:00
David Cornu
9470fba0c8 Exclude lib/liquid/standardfilters.rb from ModuleLength 2015-06-16 15:19:06 +00:00
David Cornu
ac180e8402 Use to_integer instead of to_i on arguments 2015-06-16 15:08:29 +00:00
Carson Reinke
7c5d54aced Ignore Rubocop Metrics/ModuleLength for now 2015-06-15 15:07:25 -04:00
Carson Reinke
5fbb312a67 "Trailing whitespace detected." 2015-06-15 14:27:48 -04:00
Carson Reinke
8385099960 Added "compact" filter 2015-06-15 14:14:28 -04:00
Florian Weingarten
504b6fb3c7 Merge pull request #596 from Shopify/liquid_c_tests
Run tests with latest liquid/c gem
2015-06-08 22:52:57 +02:00
Florian Weingarten
01420e8014 fix gem platforms 2015-06-08 18:38:40 +00:00
Florian Weingarten
dde35a2907 shut up rubocop 2015-06-08 18:38:40 +00:00
Florian Weingarten
e2323332cd Run tests with latest liquid/c gem 2015-06-08 18:38:35 +00:00
Florian Weingarten
7b4398d0c4 Merge pull request #595 from Shopify/uniq_on_strings
Fix uniq filter with string input
2015-06-05 16:27:05 +02:00
Florian Weingarten
1e23036b2d Fix uniq filter with string input 2015-06-04 22:55:03 -04:00
Florian Weingarten
13716fa68b Merge pull request #594 from boobooninja/rake_console
add rake console
2015-06-05 04:21:01 +02:00
Loren Hale
232e8bb4cd add rake console
add Rake console task to load irb with liquid
2015-06-05 10:17:55 +08:00
Dylan Thacker-Smith
6968def5dd Merge pull request #574 from Shopify/template-name-in-errors
Include template name with line numbers in render errors.
2015-06-04 15:28:12 -04:00
Dylan Thacker-Smith
ad3748af21 Include template name with line numbers in render errors. 2015-06-04 13:44:01 -04:00
Florian Weingarten
c82e04f4e6 Merge pull request #593 from Shopify/fix_predicate_name
Rename 'has_key?' and 'has_interrupt?'
2015-06-04 19:40:14 +02:00
Florian Weingarten
5919626da4 Rename 'has_key?' and 'has_interrupt?' 2015-06-04 13:14:46 -04:00
Florian Weingarten
82269e2509 fix a few more rubocop offenses 2015-06-04 13:09:58 -04:00
Florian Weingarten
b347fac3c0 Merge pull request #592 from Shopify/method_literal
blank and empty as variable names
2015-06-04 19:09:48 +02:00
Florian Weingarten
e761a6864e clean up some rubocop stuff 2015-06-04 12:56:29 -04:00
Florian Weingarten
4c22cef341 blank and empty as variable names 2015-06-04 12:30:50 -04:00
Florian Weingarten
c319240174 run rubocop on CI 2015-06-04 11:57:25 -04:00
Florian Weingarten
6ace095207 Avoid parallel assignments 2015-06-04 11:56:47 -04:00
Florian Weingarten
e36f366c33 gitignore .bundle 2015-06-04 11:56:00 -04:00
Florian Weingarten
02729e89c0 make rubocop happy 2015-06-04 11:56:00 -04:00
Gaurav Chande
6b0f6401d0 Merge pull request #590 from Shopify/allow-template-tags
Local Tags
2015-06-04 11:19:24 -04:00
Gaurav Chande
fc8e6c8d3a Change Tokenizer test to fetch tokens instead of exposing ivar 2015-06-04 15:10:01 +00:00
Gaurav Chande
79d7dd06df Extract tag fetching into a method (which can be overriden then) 2015-06-04 04:39:54 +00:00
Gaurav Chande
3a907a4db7 Move DEFAULT_OPTIONS related logic to Document 2015-06-04 04:39:54 +00:00
Gaurav Chande
8b98f92c7f Extract tokenize logic from Template to a RubyTokenizer 2015-06-04 04:39:30 +00:00
Dylan Thacker-Smith
b79c0c611c Merge pull request #586 from Shopify/string-contains-non-string
Avoid an exception from checking if a string contains a non-string.
2015-06-03 10:58:38 -04:00
Dylan Thacker-Smith
8a2947865b Avoid an exception from checking if a string contains a non-string. 2015-06-03 02:21:51 -04:00
Dylan Thacker-Smith
ea29f8b4b8 Merge pull request #583 from Shopify/slice-nil-offset
Raise a Liquid::ArgumentError in slice filter for invalid integers.
2015-06-03 01:43:56 -04:00
Dylan Thacker-Smith
c84f4520cc Keep input out of error message and add test for slice Integer parsing. 2015-06-03 01:35:01 -04:00
Dylan Thacker-Smith
3dd6433e2f Merge pull request #584 from Shopify/replace-non-string
Convert arguments to replace filters to strings to avoid exceptions.
2015-06-02 16:41:15 -04:00
Dylan Thacker-Smith
ab7109a335 Raise a Liquid::ArgumentError in slice filter for invalid integers. 2015-06-02 16:05:08 -04:00
Dylan Thacker-Smith
94fe050952 Convert arguments to replace filters to strings to avoid exceptions. 2015-06-02 15:59:29 -04:00
Justin Li
9b98c436c4 Merge pull request #582 from Shopify/require-empty-raw-tag
Ensure raw tag has no arguments
2015-06-02 15:58:15 -04:00
Justin Li
889019f53a Keep old test as well 2015-06-02 15:21:51 -04:00
Justin Li
c290375aec Remove unnecessary regex options 2015-06-02 15:17:36 -04:00
Justin Li
719a98a25e Ensure raw tag has no arguments 2015-06-02 14:32:39 -04:00
Justin Li
86d8b552da Merge pull request #581 from Shopify/require-closed-raw-tag
Raise SyntaxError if raw tag is unclosed
2015-06-02 11:38:45 -04:00
Justin Li
b1ee9129e7 Raise SyntaxError if raw tag is unclosed 2015-06-02 10:56:51 -04:00
Justin Li
be2e41e4d5 Merge pull request #579 from Shopify/ast-match
Ensure For@reversed is a boolean
2015-05-28 16:45:09 -04:00
Justin Li
20ca2b9632 Update history to reflect merge of #570
[ci skip]
2015-05-28 16:43:22 -04:00
Justin Li
6c058823ad Merge pull request #570 from Shopify/fix-strict-conditions
Fix condition parse order in strict mode
2015-05-28 16:33:54 -04:00
Dylan Thacker-Smith
27245c9eab Merge pull request #577 from Shopify/table-row-blank-string-collection
Fix exception from using an empty string for the table row collection.
2015-05-28 16:20:42 -04:00
Justin Li
a639a13380 Use cleaner recursive solution 2015-05-28 16:16:30 -04:00
Justin Li
05a0fe56c8 Ensure For@reversed is a boolean 2015-05-28 16:09:26 -04:00
Dylan Thacker-Smith
c1eb694057 Remove the redundant iterable check in the for tag.
Just do it in slice_collection for consistency with the tablerow tag.
2015-05-28 16:04:50 -04:00
Dylan Thacker-Smith
f53b31c867 Merge pull request #578 from Shopify/filter-error-handling
Handle some more standard filter errors.
2015-05-28 15:00:57 -04:00
Dylan Thacker-Smith
363388e92f Handle some more standard filter errors. 2015-05-28 14:18:53 -04:00
Dylan Thacker-Smith
873eddbb85 Split a line and use String#empty? for readability 2015-05-28 12:55:04 -04:00
Dylan Thacker-Smith
e790b60f60 Fix exception from using an empty string for the table row collection. 2015-05-28 12:11:39 -04:00
Dylan Thacker-Smith
3264d60425 Merge pull request #576 from Shopify/flexible-exception-handler
Allow the exception handler to convert exceptions to hide error messges
2015-05-28 11:38:44 -04:00
Dylan Thacker-Smith
8ff1b8e01f Test set_line_number_from_token after exception is converted. 2015-05-28 09:22:02 -04:00
Dylan Thacker-Smith
8d5e71f856 Allow the exception handler to convert exceptions to hide error messages. 2015-05-27 18:59:51 -04:00
Dylan Thacker-Smith
89c6e605f8 Merge pull request #575 from Shopify/zero-division-error
Raise Liquid::ZeroDivisionError instead of ZeroDivisionError.
2015-05-26 10:43:23 -04:00
Dylan Thacker-Smith
6265c36ec9 Raise Liquid::ZeroDivisionError instead of ZeroDivisionError. 2015-05-25 15:40:17 -04:00
Dylan Thacker-Smith
8af99ff918 Merge pull request #573 from Shopify/optional-error-rendering
Make liquid error rendering optional.
2015-05-25 12:11:10 -04:00
Dylan Thacker-Smith
36200ff704 Make liquid error rendering optional.
Although the author of the liquid template wants to see these errors, they
probably don't want the visitor to see the liquid errors.  Probably the
best fallback when rendering the page for visitors is to render the empty
string for tags with errors.
2015-05-25 11:24:53 -04:00
Justin Li
a9c7df931f Strict parse conditions in reverse order 2015-05-19 11:51:01 -04:00
Justin Li
070639daba Push to for_stack at the beginning of For#render 2015-05-15 23:13:15 -04:00
Justin Li
dad98cfc89 Merge pull request #562 from Shopify/use-find_variable-for-parentloop
Use custom stack for forloop references
2015-05-15 21:48:57 -04:00
Florian Weingarten
1d3c0b3dab Merge pull request #568 from Shopify/remove_liquid_methods
Remove support for `liquid_methods` Module extension
2015-05-14 22:19:02 +02:00
Justin Li
648a4888af Pop the for_stack register in an ensure 2015-05-14 15:02:20 -04:00
Justin Li
b4e5017c79 Add truth table test for multiple if conditions 2015-05-14 14:11:03 -04:00
Justin Li
f1bc9f27df Include message in assert_template_result 2015-05-14 14:10:45 -04:00
Florian Weingarten
f4724f0db3 Remove support for liquid_methods Module extension 2015-05-14 14:44:19 +00:00
Florian Weingarten
df74955ac4 Merge pull request #564 from Shopify/rubocop
Rubocop
2015-05-14 16:41:32 +02:00
Florian Weingarten
3372ca8136 Rubocop 2015-05-14 14:37:18 +00:00
Jean Boussier
8cf524e91c Merge pull request #565 from Shopify/file-dirname
Modernize code base with __dir__ and require_relative
2015-05-13 15:45:22 -04:00
Jean Boussier
5e38626309 Force circle in ruby 2.0 2015-05-13 15:40:34 -04:00
Jean Boussier
b31df0fb3d Mordernize code base with __dir__ and require_relative 2015-05-13 15:33:00 -04:00
Florian Weingarten
9e815ec594 Merge pull request #563 from Shopify/webscale_exceptions
Prefer Class.new() where possible
2015-05-13 06:06:35 +02:00
Florian Weingarten
93b29b67ef Prefer Class.new() where possible 2015-05-13 02:47:43 +00:00
Justin Li
863e8968f0 Use extra stack for forloop references 2015-05-12 17:04:34 -04:00
Justin Li
4c9d2009f9 Add find_own_variable method to look up internal context variables 2015-05-12 16:49:39 -04:00
Justin Li
239cfa5a44 Use find_variable for parentloop 2015-05-12 16:11:32 -04:00
Justin Li
8a8996387b Update history to reflect merge of #554
[ci skip]
2015-05-12 13:20:06 -04:00
Justin Li
9310640bdd Merge pull request #554 from arthanzel/529-sort_natural
Merge pull request 554
2015-05-12 13:19:24 -04:00
Justin Li
4c3381a523 Update history to reflect merge of #559
[ci skip]
2015-05-12 10:59:58 -04:00
Justin Li
261aa2e726 Merge pull request #559 from Shopify/fix-include-var
Merge pull request 559
2015-05-12 10:50:13 -04:00
Justin Li
247c51ac70 Call Context#find_variable directly 2015-05-11 18:22:15 -04:00
Justin Li
37dbec3610 Remove unnecessary parse 2015-05-11 18:10:38 -04:00
Justin Li
ff253a04c6 Lazily evaluate template name for context variable injection 2015-05-11 18:01:24 -04:00
Justin Li
25ef0df671 Add tests for #461 2015-05-11 17:59:05 -04:00
Martin Hanzel
32460c255b Removed a few superfluous comments 2015-05-08 11:48:33 -04:00
Justin Li
724d625f47 Update history to reflect merge of #555 [ci skip] 2015-05-07 14:03:38 -04:00
Justin Li
f658dcee8b Merge pull request #555 from boobooninja/date_filter
Merge pull request 555
2015-05-07 13:59:22 -04:00
Loren Hale
fa6cd6287e date filter gracefully accepts empty string 2015-05-07 17:04:21 +08:00
Justin Li
76c24db039 Remove ruby-head from allowed failures 2015-05-05 12:49:04 -04:00
Martin Hanzel
068791d698 Added method parens 2015-05-05 11:49:14 -04:00
Martin Hanzel
3a082ddbbd Changed sort_natural filter to use casecmp. Strings only. 2015-05-04 11:55:14 -04:00
Martin Hanzel
03b3446119 Resolves #529. Resolves #404. Added natural sorting filter and tests. 2015-05-03 20:55:28 -04:00
James Reid-Smith
251ce7483c Merge pull request #441 from Shopify/remove_context_from_read_template_file
Removed context from read_template_file, fixed tests to match new arity
2015-04-27 12:13:36 -04:00
James Reid-Smith
4592afcc8b Updated History.md and removed a couple remaining methods using the old signature 2015-04-27 15:45:44 +00:00
James Reid-Smith
448766b0c4 Removed context from read_template_file, fixed tests to match new arity 2015-04-27 15:27:03 +00:00
Justin Li
6390652c3f Update changelog with backported patches 2015-04-24 16:09:37 -04:00
Justin Li
f266aee2e5 Slightly more concise issue# reference in changelog 2015-04-21 23:40:42 -04:00
Justin Li
df0649a031 Update changelog 2015-04-21 23:36:54 -04:00
Justin Li
78a5972487 Merge pull request #541 from Shopify/history-sync
Sync History.md for Liquid 4
2015-04-21 23:34:29 -04:00
Justin Li
298ae3357c Merge pull request #551 from Shopify/expose-variable-name
Merge pull request 551
2015-04-21 23:33:13 -04:00
Justin Li
f1f3f57647 Remove command_lookups reader 2015-04-21 00:25:51 -04:00
Justin Li
e5dd63e1fc Expose name, lookups, and command flags from VariableLookup 2015-04-20 17:36:04 -04:00
Justin Li
881f86d698 Merge pull request #550 from Shopify/minitest-fail-workaround
Disable minitest expectation interface due to reckless modification of Object
2015-04-20 10:22:19 -04:00
Justin Li
a1b209d212 Disable minitest expectation interface due to reckless modification of Object 2015-04-20 10:15:19 -04:00
Thierry Joyal
8e5926669b Merge pull request #545 from Shopify/explode-invokable_methods-on-drop
Explode invokable_methods method on Liquid::Drop
2015-04-02 09:02:48 -07:00
Thierry Joyal
8736b602ea Explode invokable_methods method on Liquid::Drop 2015-04-02 13:16:07 +00:00
Justin Li
b8365af07d Add changes for 4.0.0 2015-03-25 14:53:43 -04:00
Justin Li
53842a471e Create history section for 4.0.0 2015-03-25 14:40:19 -04:00
Justin Li
86a82d3039 Merge pull request #540 from Shopify/array-concat
Add array concat filter
2015-03-25 01:42:22 -04:00
Justin Li
2b78e74b4e Add test for concat filter with non-array input 2015-03-25 01:34:47 -04:00
divecch
db396dd739 adding concat filter to append arrays 2015-03-25 01:31:22 -04:00
Justin Li
3213db54d6 Merge pull request #520 from Shopify/forloop-parentloop
Add forloop.parentloop as a reference to the parent loop
2015-03-25 01:22:35 -04:00
Justin Li
97a3f145a1 Merge pull request #499 from kreynolds/to_date_downcase_regression
Fix case sensitivity regression in date standard filter
2015-03-25 01:22:04 -04:00
Florian Weingarten
2fbe813770 Merge pull request #539 from Dorian/patch-1
Update module_ex.rb code documentation and code style
2015-03-24 15:21:22 +01:00
Dorian Marié
23a23c6419 Update module_ex.rb code documentation and code style
Didn't look good on rubydoc.info: http://i.imgur.com/469N92P.png
2015-03-24 14:09:08 +01:00
Dylan Thacker-Smith
63eb1aac69 Merge pull request #519 from Shopify/remove-filter-method-blacklist
Allow filters to redefine Object methods to make them invokable.
2015-02-04 18:07:51 -05:00
Justin Li
205bd19d3f Add forloop.parentloop as a reference to the parent loop 2015-02-04 12:43:09 -05:00
Dylan Thacker-Smith
950f062041 Allow filters to redefine Object methods to make them invokable. 2015-02-03 13:51:33 -05:00
Tobias Lütke
3476a556dd Merge pull request #512 from Shopify/fix_tobi_name
Fix Tobi last name on gemspec
2015-01-23 21:24:04 -05:00
Arthur Neves
d2ef9cef10 master is 4.0.0 2015-01-23 10:49:07 -05:00
Arthur Neves
0021c93fef Add ruby 2.2 to travis
and allow failure on ruby head
2015-01-23 10:42:26 -05:00
Arthur Neves
dcf7064460 Fix Tobi last name on gemspec 2015-01-23 10:21:40 -05:00
Florian Weingarten
bebd3570ee Merge pull request #506 from Shopify/fix_capture_with_hyphen
Use VariableSignature as Syntax for Capture tag to allow hyphens in variable names
2015-01-10 23:27:00 -05:00
Florian Weingarten
7cfee1616a Use VariableSignature as Syntax for Capture tag to allow hyphens in variable names 2015-01-09 14:15:26 +00:00
Arthur Nogueira Neves
4b0a7c5d1d Merge pull request #504 from alfredxing/duplicate-keys
Remove duplicate `index0` key in TableRow tag
2014-12-30 13:15:10 -05:00
Alfred Xing
5df1a262ad Remove duplicate key in hash 2014-12-25 12:12:42 -08:00
Kelley Reynolds
84fddba2e1 Remove regex for downcase and is_a?(String) 2014-12-18 13:01:23 -05:00
Kelley Reynolds
8b0774b519 Fix case sensitivity regression in date standard filter 2014-12-16 10:37:05 -05:00
Justin Li
e2f8b28f56 Merge pull request #492 from Shopify/resource-counting-perf
Resource counting perf
2014-12-11 16:05:41 -05:00
Justin Li
3080f95a4f Make render_length tests stricter 2014-12-11 10:41:47 -05:00
Justin Li
cc57908c03 Add test for render_length persisting between block bodies 2014-12-11 10:38:47 -05:00
Justin Li
4df4f218cf Use same template instance 2014-12-09 17:25:15 -05:00
Justin Li
c2f71ee86b Reset resource consumption before each render 2014-12-09 17:23:07 -05:00
Justin Li
9f7e601110 Convert render output to strings in BlockBody 2014-12-05 15:17:09 -05:00
Justin Li
3755031c18 Merge pull request #485 from Shopify/lazy-load-profiler-hooks
Defer loading profiler hooks
2014-12-05 15:10:16 -05:00
Justin Li
b628477af1 Disambiguate checking if Liquid::Profiler is defined 2014-12-04 17:51:54 -05:00
Justin Li
dd455a6361 Force user to require the profiler themselves 2014-12-04 17:48:26 -05:00
Justin Li
8c70682d6b Don't automatically load hooks 2014-12-04 17:39:41 -05:00
Justin Li
742b3c69bb Remove commented code 2014-12-04 16:30:37 -05:00
Justin Li
1593b784a7 Simplify interface for setting template resource limits 2014-12-04 16:18:21 -05:00
Justin Li
db00ec8b32 Move resource limit tracking to its own class 2014-12-04 16:18:09 -05:00
Justin Li
3ca40b5dea Merge pull request #491 from Shopify/drop-ruby-1-9
Drop Ruby 1.9 from CI, add Ruby head
2014-12-03 12:52:10 -05:00
Justin Li
378775992f Drop Ruby 1.9 from CI, add Ruby head 2014-12-02 14:33:51 -05:00
Florian Weingarten
319400ea23 Merge pull request #489 from alex-ross/patch-1
Fixes syntax error in documentation for unless tag
2014-11-19 14:02:58 +01:00
Alexander Ross
289a03f9d7 Fixes syntax error in documentation for unless tag 2014-11-19 10:49:58 +01:00
Justin Li
a0710f4c70 Merge pull request #486 from Shopify/fix-exponential-warnings
Fix #warnings taking exponential time to compute
2014-11-12 17:22:16 -05:00
Justin Li
737be1a0c1 Use Timeout#timeout for warnings tests 2014-11-12 17:03:48 -05:00
Justin Li
1673098126 Handle potential case where warnings returns nil 2014-11-12 16:46:10 -05:00
Justin Li
422bafd66a Fix #warnings taking exponential time to compute 2014-11-12 16:12:00 -05:00
Justin Li
c0aab820ed Lazily load profiler hooks 2014-11-12 00:05:01 -05:00
Florian Weingarten
3321cffe08 Merge pull request #482 from joshk/patch-1
Use the new beta build env on Travis
2014-11-07 03:06:52 +01:00
Josh Kalderimis
f2772518b0 Use the new beta build env on Travis
job start in seconds, instead of 20-120 seconds
2014-11-07 14:54:21 +13:00
Justin Li
76ef675eb2 Merge pull request #481 from Shopify/fix-nil-blank
Coerce regex @blank output to a boolean
2014-11-06 13:03:15 -05:00
Justin Li
e5fd4d929f Coerce regex @blank output to a boolean 2014-11-05 20:44:06 -05:00
Justin Li
2e42c7be1f Merge pull request #480 from Shopify/number_variables
Add quirks test for variables with number prefixes
2014-11-05 12:05:21 -05:00
Justin Li
95b031ee04 Add quirks test for extra dots in ranges 2014-11-05 11:41:12 -05:00
Justin Li
4d97a714a9 Add quirks test for variables with number prefixes 2014-11-05 10:56:58 -05:00
Justin Li
aa182f64b4 Merge pull request #479 from Shopify/tweaks-for-c
Tweaks for C
2014-11-04 14:02:14 -05:00
Justin Li
4e870302b1 Add env var for saving stackprof graphviz output 2014-11-04 18:38:14 +00:00
Justin Li
098c89b5f5 Convenience methods for raising terminator syntax errors 2014-11-04 18:38:08 +00:00
Justin Li
70c45f8cd8 Use SVG badge URLs
[ci skip]
2014-11-03 17:41:42 -05:00
Justin Li
12d526a05c Merge pull request #458 from Shopify/block-body
Create a BlockBody class to decouple block body parsing from tags.
2014-11-03 17:34:39 -05:00
Dylan Thacker-Smith
2fd8ad08c0 Remove unused local variable that was accidentally added. 2014-11-03 17:07:42 -05:00
Dylan Thacker-Smith
15e1d46125 Avoid storing options instance variable in BlockBody.
There is no need to pass parse options to the BlockBody initializer, since
it does all the parsing in the parse method, unlike tags which parse the
tag markup in the initializer.
2014-11-03 17:07:42 -05:00
Dylan Thacker-Smith
73fcd42403 Create a BlockBody class to decouple block body parsing from tags. 2014-11-03 17:07:42 -05:00
Justin Li
263e90e772 Merge pull request #478 from Shopify/numbers-in-identifiers
Use a single token for identifiers
2014-10-30 21:59:26 -04:00
Justin Li
81770f094d Remove unnecessary + 2014-10-29 13:39:43 -04:00
Justin Li
dd5ee81089 Disallow number and dash identifier prefixes 2014-10-29 12:08:00 -04:00
Justin Li
a07e382617 Use a single token for identifiers 2014-10-29 11:28:41 -04:00
Justin Li
4dc682313f Merge pull request #476 from Shopify/missing-variable-name-error
Disallow filters with no variable in strict mode
2014-10-27 13:56:11 -04:00
Justin Li
5616ddf00e Remove obsolete comment 2014-10-27 13:44:14 -04:00
Justin Li
fcb23a4cd2 Disallow filters with no variable in strict mode 2014-10-27 13:34:27 -04:00
Justin Li
a8f60ff6b1 Merge pull request #472 from Shopify/fix-leaky-test
Fix test leaking error_mode, fix equality check for VariableLookup
2014-10-23 10:12:41 -04:00
Justin Li
a206c8301d Fix test leaking error_mode, fix equality check for VariableLookup 2014-10-22 15:40:41 -04:00
Justin Li
ee0de01480 Merge pull request #469 from Shopify/falsy-variable-fix
Fix case where a variable name is falsy
2014-10-21 15:06:34 -04:00
Justin Li
887b05e6ed Clarify test name 2014-10-21 14:06:30 -04:00
Justin Li
5d68e8803f Ensure nil works as a variable name 2014-10-21 14:03:10 -04:00
Justin Li
dedd1d3dc0 Fix case where a variable name is falsy 2014-10-21 12:09:26 -04:00
Dylan Thacker-Smith
d9ae36ec40 Merge pull request #466 from Shopify/remove-expression-cache
Remove expression cache
2014-10-20 13:57:17 -04:00
Dylan Thacker-Smith
b9ac3fef8f Remove the quotes from the partial string in the profiler timing objects. 2014-10-18 16:26:18 -04:00
Dylan Thacker-Smith
f5faa4858c Remove parsed expression cache. 2014-10-18 15:03:40 -04:00
Dylan Thacker-Smith
bc5e444d04 Use Expression.parse and Context#evaluate in the Include class. 2014-10-18 15:03:40 -04:00
Dylan Thacker-Smith
3a4b63f37e Use Expression.parse and Context#evaluate in the TableRow class. 2014-10-18 15:03:40 -04:00
Dylan Thacker-Smith
a1a128db19 Refactor Condition so that it takes a parsed expression. 2014-10-18 15:03:40 -04:00
Dylan Thacker-Smith
d502b9282a Use Expression.parse and Context#evaluate in the For class. 2014-10-18 15:03:36 -04:00
Dylan Thacker-Smith
fee8e41466 Use Expression.parse and Context#evaluate in the Cycle class. 2014-10-18 14:27:58 -04:00
Dylan Thacker-Smith
37260f17ff Use Expression.parse and Context#evaluate in the Condition class. 2014-10-18 14:27:58 -04:00
Florian Weingarten
2da9d49478 Merge pull request #465 from Shopify/avoid_multi_assigns
Avoid parallel assignments
2014-10-18 16:19:02 +02:00
Florian Weingarten
7196a2d58e Avoid parallel assignments 2014-10-18 13:58:32 +00:00
Justin Li
a056f6521c Merge pull request #463 from Shopify/stricter-identifiers
Separate ? and - into special tokens
2014-10-17 13:45:48 -04:00
Justin Li
de16db9b72 Don't allow - to end a variable name 2014-10-17 13:38:07 -04:00
Justin Li
b4ea483c4e Separate ? and - into special tokens 2014-10-17 13:30:54 -04:00
Justin Li
7843bcca8d Merge pull request #443 from Shopify/completely-parse-variables
Parse expressions in Liquid::Variable#parse.
2014-10-17 13:12:46 -04:00
Florian Weingarten
76ea5596ff Merge pull request #462 from Shopify/flat_map
nodelist flat_map over map.flatten
2014-10-17 18:32:00 +02:00
Florian Weingarten
f9318e8c93 flat_map 2014-10-17 16:11:12 +00:00
Florian Weingarten
71253ec6f9 Merge pull request #459 from Shopify/pop_vs_shift
Use pop over shift to avoid reverse
2014-10-15 21:42:53 +02:00
Florian Weingarten
0fa075b879 Use pop over shift to avoid reverse 2014-10-15 19:26:39 +00:00
Dylan Thacker-Smith
6d080afd22 Merge pull request #446 from Shopify/remove-end-tag
Remove unused Block#end_tag method.
2014-10-14 03:03:31 -04:00
Dylan Thacker-Smith
a67e2a0a00 Remove unused Block#end_tag method.
Although the method is called, it is defined with an empty body and not
overridden to do anything else.
2014-10-14 02:58:11 -04:00
Dylan Thacker-Smith
f387508666 Parse expressions in Liquid::Variable#parse. 2014-10-08 21:06:59 -04:00
Florian Weingarten
632b1fb702 Merge pull request #455 from Shopify/parse_error_line_numbers
Line numbers for all parse errors
2014-10-04 17:53:30 +02:00
Dylan Thacker-Smith
d84870d7a5 Test line number of errors in nested blocks. 2014-10-03 16:25:12 -05:00
Florian Weingarten
584b492e70 Line numbers for all parse errors 2014-10-03 21:00:31 +00:00
Dylan Thacker-Smith
b79c9cb9bf Merge pull request #453 from Shopify/no-modify-default-resource-limit
Avoid modifying the default resources limits hash.
2014-10-01 19:02:09 -05:00
Dylan Thacker-Smith
cf5ccede50 Avoid modifying the default resources limits hash. 2014-10-01 18:51:06 -05:00
Evan Huus
23622a9739 Merge pull request #440 from Shopify/drop-tainting
Variable tainting
2014-09-22 13:43:35 -04:00
Florian Weingarten
7ba5a6ab75 Merge pull request #450 from Shopify/additional_test_for_includes
Regression test for including assignments
2014-09-18 21:05:37 +02:00
Florian Weingarten
be3d261e11 Regression test for including assignments 2014-09-18 10:37:44 +00:00
Evan Huus
eeb061ef44 Address code review comments
- clean up comment wording
- fix potentially leaky tests
2014-09-16 17:23:26 +00:00
Evan Huus
67b2c320a1 Add tainting tests 2014-09-16 17:23:26 +00:00
Evan Huus
1d151885be Auto-untaint variables passed through 'escape' 2014-09-16 17:23:26 +00:00
Evan Huus
e836024dd9 Check and handle when a tainted variable is used 2014-09-16 17:23:26 +00:00
Dylan Thacker-Smith
638455ed92 Merge pull request #448 from Shopify/remove-unused-filter-not-found-error
Remove Liquid::FilterNotFoundError since it is never raised.
2014-09-15 17:43:33 -04:00
Dylan Thacker-Smith
b2a74883e9 Remove Liquid::FilterNotFoundError since it is never raised. 2014-09-15 17:42:07 -04:00
Dylan Thacker-Smith
6875e5e16f Merge pull request #449 from Shopify/fix-flaky-total-render-time-test
Fix flaky test which assumes total_render_time can't be 0.
2014-09-15 17:41:41 -04:00
Dylan Thacker-Smith
a5717a3f8d Fix flaky test which assumes total_render_time can't be 0.
jruby has millisecond precision for Time.now, so total_render_time can be 0
due to this lack of precision.
2014-09-15 17:26:55 -04:00
Dylan Thacker-Smith
804fcfebd1 Merge pull request #444 from Shopify/remove-block-children
Avoid keeping track of two lists of nodes during parsing.
2014-09-15 09:56:08 -04:00
Dylan Thacker-Smith
b37ee5684a Merge pull request #445 from Shopify/prefer-super-over-render-all
Use super rather than render_all in single block render classes.
2014-09-15 09:53:54 -04:00
Dylan Thacker-Smith
0573b63b4c Use super rather than render_all in single block render classes. 2014-09-12 16:58:07 -04:00
Dylan Thacker-Smith
29c21d7867 Avoid keeping track of two lists of nodes during parsing. 2014-09-12 16:43:00 -04:00
129 changed files with 5585 additions and 2190 deletions

2
.github/probots.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
enabled:
- cla

1
.gitignore vendored
View File

@@ -6,3 +6,4 @@ pkg
.rvmrc .rvmrc
.ruby-version .ruby-version
Gemfile.lock Gemfile.lock
.bundle

132
.rubocop.yml Normal file
View File

@@ -0,0 +1,132 @@
inherit_from:
- .rubocop_todo.yml
- ./.rubocop_todo.yml
AllCops:
Exclude:
- 'performance/shopify/*'
- 'pkg/**'
Metrics/BlockNesting:
Max: 3
Metrics/ModuleLength:
Enabled: false
Metrics/ClassLength:
Enabled: false
Lint/AssignmentInCondition:
Enabled: false
Lint/AmbiguousOperator:
Enabled: false
Lint/AmbiguousRegexpLiteral:
Enabled: false
Lint/ParenthesesAsGroupedExpression:
Enabled: false
Lint/UnusedBlockArgument:
Enabled: false
Layout/EndAlignment:
EnforcedStyleAlignWith: variable
Lint/UnusedMethodArgument:
Enabled: false
Style/SingleLineBlockParams:
Enabled: false
Style/DoubleNegation:
Enabled: false
Style/StringLiteralsInInterpolation:
Enabled: false
Style/AndOr:
Enabled: false
Style/SignalException:
Enabled: false
Style/StringLiterals:
Enabled: false
Style/BracesAroundHashParameters:
Enabled: false
Style/NumericLiterals:
Enabled: false
Layout/SpaceInsideArrayLiteralBrackets:
Enabled: false
Layout/SpaceBeforeBlockBraces:
Enabled: false
Style/Documentation:
Enabled: false
Style/ClassAndModuleChildren:
Enabled: false
Style/TrailingCommaInArrayLiteral:
Enabled: false
Style/TrailingCommaInHashLiteral:
Enabled: false
Layout/IndentHash:
EnforcedStyle: consistent
Style/FormatString:
Enabled: false
Layout/AlignParameters:
EnforcedStyle: with_fixed_indentation
Layout/MultilineOperationIndentation:
EnforcedStyle: indented
Style/IfUnlessModifier:
Enabled: false
Style/RaiseArgs:
Enabled: false
Style/PreferredHashMethods:
Enabled: false
Style/RegexpLiteral:
Enabled: false
Style/SymbolLiteral:
Enabled: false
Performance/Count:
Enabled: false
Naming/ConstantName:
Enabled: false
Layout/CaseIndentation:
Enabled: false
Style/ClassVars:
Enabled: false
Style/PerlBackrefs:
Enabled: false
Style/TrivialAccessors:
AllowPredicates: true
Style/WordArray:
Enabled: false
Naming/MethodName:
Exclude:
- 'example/server/liquid_servlet.rb'

260
.rubocop_todo.yml Normal file
View File

@@ -0,0 +1,260 @@
# This configuration was generated by
# `rubocop --auto-gen-config`
# on 2019-03-19 11:04:37 -0400 using RuboCop version 0.53.0.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again.
# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: Include, TreatCommentsAsGroupSeparators.
# Include: **/*.gemspec
Gemspec/OrderedDependencies:
Exclude:
- 'liquid.gemspec'
# Offense count: 5
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: auto_detection, squiggly, active_support, powerpack, unindent
Layout/IndentHeredoc:
Exclude:
- 'test/integration/tags/for_tag_test.rb'
- 'test/integration/trim_mode_test.rb'
# Offense count: 6
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: symmetrical, new_line, same_line
Layout/MultilineMethodCallBraceLayout:
Exclude:
- 'test/integration/error_handling_test.rb'
- 'test/unit/strainer_unit_test.rb'
# Offense count: 2
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: runtime_error, standard_error
Lint/InheritException:
Exclude:
- 'lib/liquid/interrupts.rb'
# Offense count: 1
# Configuration parameters: CheckForMethodsWithNoSideEffects.
Lint/Void:
Exclude:
- 'lib/liquid/parse_context.rb'
# Offense count: 54
Metrics/AbcSize:
Max: 56
# Offense count: 12
Metrics/CyclomaticComplexity:
Max: 12
# Offense count: 112
# Configuration parameters: CountComments.
Metrics/MethodLength:
Max: 37
# Offense count: 8
Metrics/PerceivedComplexity:
Max: 11
# Offense count: 52
# Configuration parameters: Blacklist.
# Blacklist: END, (?-mix:EO[A-Z]{1})
Naming/HeredocDelimiterNaming:
Exclude:
- 'test/integration/assign_test.rb'
- 'test/integration/capture_test.rb'
- 'test/integration/trim_mode_test.rb'
# Offense count: 23
# Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames.
# AllowedNames: io, id
Naming/UncommunicativeMethodParamName:
Exclude:
- 'example/server/example_servlet.rb'
- 'lib/liquid/condition.rb'
- 'lib/liquid/context.rb'
- 'lib/liquid/standardfilters.rb'
- 'lib/liquid/tags/if.rb'
- 'lib/liquid/utils.rb'
- 'lib/liquid/variable.rb'
- 'test/integration/filter_test.rb'
- 'test/integration/standard_filter_test.rb'
- 'test/integration/tags/for_tag_test.rb'
- 'test/integration/template_test.rb'
- 'test/unit/condition_unit_test.rb'
# Offense count: 10
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: prefer_alias, prefer_alias_method
Style/Alias:
Exclude:
- 'lib/liquid/drop.rb'
- 'lib/liquid/i18n.rb'
- 'lib/liquid/profiler/hooks.rb'
- 'lib/liquid/standardfilters.rb'
- 'lib/liquid/tag.rb'
- 'lib/liquid/tags/include.rb'
- 'lib/liquid/variable.rb'
# Offense count: 22
Style/CommentedKeyword:
Enabled: false
# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SingleLineConditionsOnly, IncludeTernaryExpressions.
# SupportedStyles: assign_to_condition, assign_inside_condition
Style/ConditionalAssignment:
Exclude:
- 'lib/liquid/errors.rb'
# Offense count: 1
Style/DateTime:
Exclude:
- 'test/unit/context_unit_test.rb'
# Offense count: 2
# Cop supports --auto-correct.
Style/EmptyCaseCondition:
Exclude:
- 'lib/liquid/block_body.rb'
- 'lib/liquid/lexer.rb'
# Offense count: 5
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: compact, expanded
Style/EmptyMethod:
Exclude:
- 'lib/liquid/tag.rb'
- 'lib/liquid/tags/comment.rb'
- 'lib/liquid/tags/include.rb'
- 'test/integration/tags/include_tag_test.rb'
- 'test/unit/context_unit_test.rb'
# Offense count: 3
# Cop supports --auto-correct.
Style/Encoding:
Exclude:
- 'lib/liquid/version.rb'
- 'liquid.gemspec'
- 'test/integration/standard_filter_test.rb'
# Offense count: 2
# Cop supports --auto-correct.
Style/ExpandPathArguments:
Exclude:
- 'Rakefile'
- 'liquid.gemspec'
# Offense count: 7
# Configuration parameters: EnforcedStyle.
# SupportedStyles: annotated, template, unannotated
Style/FormatStringToken:
Exclude:
- 'test/integration/filter_test.rb'
- 'test/integration/hash_ordering_test.rb'
# Offense count: 14
# Configuration parameters: MinBodyLength.
Style/GuardClause:
Exclude:
- 'lib/liquid/condition.rb'
- 'lib/liquid/lexer.rb'
- 'lib/liquid/strainer.rb'
- 'lib/liquid/tags/assign.rb'
- 'lib/liquid/tags/capture.rb'
- 'lib/liquid/tags/case.rb'
- 'lib/liquid/tags/for.rb'
- 'lib/liquid/tags/include.rb'
- 'lib/liquid/tags/raw.rb'
- 'lib/liquid/tags/table_row.rb'
- 'lib/liquid/variable.rb'
- 'test/unit/tokenizer_unit_test.rb'
# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, MinBodyLength.
# SupportedStyles: skip_modifier_ifs, always
Style/Next:
Exclude:
- 'lib/liquid/tags/for.rb'
# Offense count: 4
# Cop supports --auto-correct.
# Configuration parameters: AutoCorrect, EnforcedStyle.
# SupportedStyles: predicate, comparison
Style/NumericPredicate:
Exclude:
- 'spec/**/*'
- 'lib/liquid/context.rb'
- 'lib/liquid/forloop_drop.rb'
- 'lib/liquid/standardfilters.rb'
- 'lib/liquid/tablerowloop_drop.rb'
# Offense count: 14
# Cop supports --auto-correct.
# Configuration parameters: PreferredDelimiters.
Style/PercentLiteralDelimiters:
Exclude:
- 'lib/liquid/tags/if.rb'
- 'liquid.gemspec'
- 'test/integration/assign_test.rb'
- 'test/integration/standard_filter_test.rb'
# Offense count: 1
# Cop supports --auto-correct.
Style/RedundantSelf:
Exclude:
- 'lib/liquid/strainer.rb'
# Offense count: 9
# Cop supports --auto-correct.
# Configuration parameters: AllowAsExpressionSeparator.
Style/Semicolon:
Exclude:
- 'test/integration/error_handling_test.rb'
- 'test/integration/template_test.rb'
- 'test/unit/context_unit_test.rb'
# Offense count: 7
# Cop supports --auto-correct.
# Configuration parameters: MinSize.
# SupportedStyles: percent, brackets
Style/SymbolArray:
EnforcedStyle: brackets
# Offense count: 2
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, AllowSafeAssignment.
# SupportedStyles: require_parentheses, require_no_parentheses, require_parentheses_when_complex
Style/TernaryParentheses:
Exclude:
- 'lib/liquid/context.rb'
- 'lib/liquid/utils.rb'
# Offense count: 2
# Cop supports --auto-correct.
Style/UnneededPercentQ:
Exclude:
- 'test/integration/error_handling_test.rb'
# Offense count: 1
# Cop supports --auto-correct.
Style/WhileUntilModifier:
Exclude:
- 'lib/liquid/tags/case.rb'
# Offense count: 640
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
# URISchemes: http, https
Metrics/LineLength:
Max: 294

View File

@@ -1,16 +1,31 @@
language: ruby
rvm: rvm:
- 1.9
- 2.0
- 2.1 - 2.1
- jruby-19mode - 2.2
- 2.3
- 2.4
- 2.5
- ruby-head
- jruby-head - jruby-head
- rbx-2 # - rbx-2
sudo: false
addons:
apt:
packages:
- libgmp3-dev
matrix: matrix:
allow_failures: allow_failures:
- rvm: rbx-2 - rvm: ruby-head
- rvm: jruby-head - rvm: jruby-head
script: "rake test" install:
- bundle install
script: bundle exec rake
notifications: notifications:
disable: true disable: true

View File

@@ -4,23 +4,22 @@
* Bugfixes * Bugfixes
* Performance improvements * Performance improvements
* Features which are likely to be useful to the majority of Liquid users * Features that are likely to be useful to the majority of Liquid users
## Things we won't merge ## Things we won't merge
* Code which introduces considerable performance degrations * Code that introduces considerable performance degrations
* Code which touches performance critical parts of Liquid and comes without benchmarks * Code that touches performance-critical parts of Liquid and comes without benchmarks
* Features which are not important for most people (we want to keep the core Liquid code small and tidy) * Features that are not important for most people (we want to keep the core Liquid code small and tidy)
* Features which can easily be implemented on top of Liquid (for example as a custom filter or custom filesystem) * Features that can easily be implemented on top of Liquid (for example as a custom filter or custom filesystem)
* Code which comes without tests * Code that does not include tests
* Code which breaks existing tests * Code that breaks existing tests
## Workflow ## Workflow
* Fork the Liquid repository * Fork the Liquid repository
* Create a new branch in your fork * Create a new branch in your fork
* If it makes sense, add tests for your code and run a performance benchmark * If it makes sense, add tests for your code and/or run a performance benchmark
* Make sure all tests pass * Make sure all tests pass (`bundle exec rake`)
* Create a pull request * Create a pull request
* In the description, ping one of [@boourns](https://github.com/boourns), [@fw42](https://github.com/fw42), [@camilo](https://github.com/camilo), [@dylanahsmith](https://github.com/dylanahsmith), or [@arthurnn](https://github.com/arthurnn) and ask for a code review.

20
Gemfile
View File

@@ -1,9 +1,23 @@
source 'https://rubygems.org' source 'https://rubygems.org'
git_source(:github) do |repo_name|
"https://github.com/#{repo_name}.git"
end
gemspec gemspec
gem 'stackprof', platforms: :mri_21
group :benchmark, :test do
gem 'benchmark-ips'
gem 'memory_profiler'
install_if -> { RUBY_PLATFORM !~ /mingw|mswin|java/ } do
gem 'stackprof'
end
end
group :test do group :test do
gem 'spy', '0.4.1' gem 'rubocop', '~> 0.53.0'
gem 'benchmark-ips'
platform :mri do
gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'reversable-range'
end
end end

View File

@@ -1,45 +1,170 @@
# Liquid Version History # Liquid Change Log
## 3.0.0 / not yet released / branch "master" ## 4.0.3 / 2019-03-12
* ... ### Fixed
* Fixed condition with wrong data types, see #423 [Bogdan Gusiev] * Fix break and continue tags inside included templates in loops (#1072) [Justin Li]
* Add url_encode to standard filters, see #421 [Derrick Reimer, djreimer]
* Add uniq to standard filters [Florian Weingarten, fw42]
* Add exception_handler feature, see #397 and #254 [Bogdan Gusiev, bogdan and Florian Weingarten, fw42]
* Optimize variable parsing to avoid repeated regex evaluation during template rendering #383 [Jason Hiltz-Laforge, jasonhl]
* Optimize checking for block interrupts to reduce object allocation #380 [Jason Hiltz-Laforge, jasonhl]
* Properly set context rethrow_errors on render! #349 [Thierry Joyal, tjoyal]
* Fix broken rendering of variables which are equal to false, see #345 [Florian Weingarten, fw42]
* Remove ActionView template handler [Dylan Thacker-Smith, dylanahsmith]
* Freeze lots of string literals for new Ruby 2.1 optimization, see #297 [Florian Weingarten, fw42]
* Allow newlines in tags and variables, see #324 [Dylan Thacker-Smith, dylanahsmith]
* Tag#parse is called after initialize, which now takes options instead of tokens as the 3rd argument. See #321 [Dylan Thacker-Smith, dylanahsmith]
* Raise `Liquid::ArgumentError` instead of `::ArgumentError` when filter has wrong number of arguments #309 [Bogdan Gusiev, bogdan]
* Add a to_s default for liquid drops, see #306 [Adam Doeler, releod]
* Add strip, lstrip, and rstrip to standard filters [Florian Weingarten, fw42]
* Make if, for & case tags return complete and consistent nodelists, see #250 [Nick Jones, dntj]
* Prevent arbitrary method invocation on condition objects, see #274 [Dylan Thacker-Smith, dylanahsmith]
* Don't call to_sym when creating conditions for security reasons, see #273 [Bouke van der Bijl, bouk]
* Fix resource counting bug with respond_to?(:length), see #263 [Florian Weingarten, fw42]
* Allow specifying custom patterns for template filenames, see #284 [Andrei Gladkyi, agladkyi]
* Allow drops to optimize loading a slice of elements, see #282 [Tom Burns, boourns]
* Support for passing variables to snippets in subdirs, see #271 [Joost Hietbrink, joost]
* Add a class cache to avoid runtime extend calls, see #249 [James Tucker, raggi]
* Remove some legacy Ruby 1.8 compatibility code, see #276 [Florian Weingarten, fw42]
* Add default filter to standard filters, see #267 [Derrick Reimer, djreimer]
* Add optional strict parsing and warn parsing, see #235 [Tristan Hume, trishume]
* Add I18n syntax error translation, see #241 [Simon Hørup Eskildsen, Sirupsen]
* Make sort filter work on enumerable drops, see #239 [Florian Weingarten, fw42]
* Fix clashing method names in enumerable drops, see #238 [Florian Weingarten, fw42]
* Make map filter work on enumerable drops, see #233 [Florian Weingarten, fw42]
* Improved whitespace stripping for blank blocks, related to #216 [Florian Weingarten, fw42]
## 2.6.1 / 2014-01-10 / branch "2-6-stable" ## 4.0.2 / 2019-03-08
### Changed
* Add `where` filter (#1026) [Samuel Doiron]
* Add `ParseTreeVisitor` to iterate the Liquid AST (#1025) [Stephen Paul Weber]
* Improve `strip_html` performance (#1032) [printercu]
### Fixed
* Add error checking for invalid combinations of inputs to sort, sort_natural, where, uniq, map, compact filters (#1059) [Garland Zhang]
* Validate the character encoding in url_decode (#1070) [Clayton Smith]
## 4.0.1 / 2018-10-09
### Changed
* Add benchmark group in Gemfile (#855) [Jerry Liu]
* Allow benchmarks to benchmark render by itself (#851) [Jerry Liu]
* Avoid calling `line_number` on String node when rescuing a render error. (#860) [Dylan Thacker-Smith]
* Avoid duck typing to detect whether to call render on a node. [Dylan Thacker-Smith]
* Clarify spelling of `reversed` on `for` block tag (#843) [Mark Crossfield]
* Replace recursion with loop to avoid potential stack overflow from malicious input (#891, #892) [Dylan Thacker-Smith]
* Limit block tag nesting to 100 (#894) [Dylan Thacker-Smith]
* Replace `assert_equal nil` with `assert_nil` (#895) [Dylan Thacker-Smith]
* Remove Spy Gem (#896) [Dylan Thacker-Smith]
* Add `collection_name` and `variable_name` reader to `For` block (#909)
* Symbols render as strings (#920) [Justin Li]
* Remove default value from Hash objects (#932) [Maxime Bedard]
* Remove one level of nesting (#944) [Dylan Thacker-Smith]
* Update Rubocop version (#952) [Justin Li]
* Add `at_least` and `at_most` filters (#954, #958) [Nithin Bekal]
* Add a regression test for a liquid-c trim mode bug (#972) [Dylan Thacker-Smith]
* Use https rather than git protocol to fetch liquid-c [Dylan Thacker-Smith]
* Add tests against Ruby 2.4 (#963) and 2.5 (#981)
* Replace RegExp literals with constants (#988) [Ashwin Maroli]
* Replace unnecessary `#each_with_index` with `#each` (#992) [Ashwin Maroli]
* Improve the unexpected end delimiter message for block tags. (#1003) [Dylan Thacker-Smith]
* Refactor and optimize rendering (#1005) [Christopher Aue]
* Add installation instruction (#1006) [Ben Gift]
* Remove Circle CI (#1010)
* Rename deprecated `BigDecimal.new` to `BigDecimal` (#1024) [Koichi ITO]
* Rename deprecated Rubocop name (#1027) [Justin Li]
### Fixed
* Handle `join` filter on non String joiners (#857) [Richard Monette]
* Fix duplicate inclusion condition logic error of `Liquid::Strainer.add_filter` method (#861)
* Fix `escape`, `url_encode`, `url_decode` not handling non-string values (#898) [Thierry Joyal]
* Fix raise when variable is defined but nil when using `strict_variables` [Pascal Betz]
* Fix `sort` and `sort_natural` to handle arrays with nils (#930) [Eric Chan]
## 4.0.0 / 2016-12-14 / branch "4-0-stable"
### Changed
* Render an opaque internal error by default for non-Liquid::Error (#835) [Dylan Thacker-Smith]
* Ruby 2.0 support dropped (#832) [Dylan Thacker-Smith]
* Add to_number Drop method to allow custom drops to work with number filters (#731)
* Add strict_variables and strict_filters options to detect undefined references (#691)
* Improve loop performance (#681) [Florian Weingarten]
* Rename Drop method `before_method` to `liquid_method_missing` (#661) [Thierry Joyal]
* Add url_decode filter to invert url_encode (#645) [Larry Archer]
* Add global_filter to apply a filter to all output (#610) [Loren Hale]
* Add compact filter (#600) [Carson Reinke]
* Rename deprecated "has_key?" and "has_interrupt?" methods (#593) [Florian Weingarten]
* Include template name with line numbers in render errors (574) [Dylan Thacker-Smith]
* Add sort_natural filter (#554) [Martin Hanzel]
* Add forloop.parentloop as a reference to the parent loop (#520) [Justin Li]
* Block parsing moved to BlockBody class (#458) [Dylan Thacker-Smith]
* Add concat filter to concatenate arrays (#429) [Diogo Beato]
* Ruby 1.9 support dropped (#491) [Justin Li]
* Liquid::Template.file_system's read_template_file method is no longer passed the context. (#441) [James Reid-Smith]
* Remove `liquid_methods` (See https://github.com/Shopify/liquid/pull/568 for replacement)
* Liquid::Template.register_filter raises when the module overrides registered public methods as private or protected (#705) [Gaurav Chande]
### Fixed
* Fix variable names being detected as an operator when starting with contains (#788) [Michael Angell]
* Fix include tag used with strict_variables (#828) [QuickPay]
* Fix map filter when value is a Proc (#672) [Guillaume Malette]
* Fix truncate filter when value is not a string (#672) [Guillaume Malette]
* Fix behaviour of escape filter when input is nil (#665) [Tanel Jakobsoo]
* Fix sort filter behaviour with empty array input (#652) [Marcel Cary]
* Fix test failure under certain timezones (#631) [Dylan Thacker-Smith]
* Fix bug in uniq filter (#595) [Florian Weingarten]
* Fix bug when "blank" and "empty" are used as variable names (#592) [Florian Weingarten]
* Fix condition parse order in strict mode (#569) [Justin Li]
* Fix naming of the "context variable" when dynamically including a template (#559) [Justin Li]
* Gracefully accept empty strings in the date filter (#555) [Loren Hale]
* Fix capturing into variables with a hyphen in the name (#505) [Florian Weingarten]
* Fix case sensitivity regression in date standard filter (#499) [Kelley Reynolds]
* Disallow filters with no variable in strict mode (#475) [Justin Li]
* Disallow variable names in the strict parser that are not valid in the lax parser (#463) [Justin Li]
* Fix BlockBody#warnings taking exponential time to compute (#486) [Justin Li]
## 3.0.5 / 2015-07-23 / branch "3-0-stable"
* Fix test failure under certain timezones [Dylan Thacker-Smith]
## 3.0.4 / 2015-07-17
* Fix chained access to multi-dimensional hashes [Florian Weingarten]
## 3.0.3 / 2015-05-28
* Fix condition parse order in strict mode (#569) [Justin Li]
## 3.0.2 / 2015-04-24
* Expose VariableLookup private members (#551) [Justin Li]
* Documentation fixes
## 3.0.1 / 2015-01-23
* Remove duplicate `index0` key in TableRow tag (#502) [Alfred Xing]
## 3.0.0 / 2014-11-12
* Removed Block#end_tag. Instead, override parse with `super` followed by your code. See #446 [Dylan Thacker-Smith]
* Fixed condition with wrong data types (#423) [Bogdan Gusiev]
* Add url_encode to standard filters (#421) [Derrick Reimer]
* Add uniq to standard filters [Florian Weingarten]
* Add exception_handler feature (#397) and #254 [Bogdan Gusiev, Florian Weingarten]
* Optimize variable parsing to avoid repeated regex evaluation during template rendering #383 [Jason Hiltz-Laforge]
* Optimize checking for block interrupts to reduce object allocation #380 [Jason Hiltz-Laforge]
* Properly set context rethrow_errors on render! #349 [Thierry Joyal]
* Fix broken rendering of variables which are equal to false (#345) [Florian Weingarten]
* Remove ActionView template handler [Dylan Thacker-Smith]
* Freeze lots of string literals for new Ruby 2.1 optimization (#297) [Florian Weingarten]
* Allow newlines in tags and variables (#324) [Dylan Thacker-Smith]
* Tag#parse is called after initialize, which now takes options instead of tokens as the 3rd argument. See #321 [Dylan Thacker-Smith]
* Raise `Liquid::ArgumentError` instead of `::ArgumentError` when filter has wrong number of arguments #309 [Bogdan Gusiev]
* Add a to_s default for liquid drops (#306) [Adam Doeler]
* Add strip, lstrip, and rstrip to standard filters [Florian Weingarten]
* Make if, for & case tags return complete and consistent nodelists (#250) [Nick Jones]
* Prevent arbitrary method invocation on condition objects (#274) [Dylan Thacker-Smith]
* Don't call to_sym when creating conditions for security reasons (#273) [Bouke van der Bijl]
* Fix resource counting bug with respond_to?(:length) (#263) [Florian Weingarten]
* Allow specifying custom patterns for template filenames (#284) [Andrei Gladkyi]
* Allow drops to optimize loading a slice of elements (#282) [Tom Burns]
* Support for passing variables to snippets in subdirs (#271) [Joost Hietbrink]
* Add a class cache to avoid runtime extend calls (#249) [James Tucker]
* Remove some legacy Ruby 1.8 compatibility code (#276) [Florian Weingarten]
* Add default filter to standard filters (#267) [Derrick Reimer]
* Add optional strict parsing and warn parsing (#235) [Tristan Hume]
* Add I18n syntax error translation (#241) [Simon Hørup Eskildsen, Sirupsen]
* Make sort filter work on enumerable drops (#239) [Florian Weingarten]
* Fix clashing method names in enumerable drops (#238) [Florian Weingarten]
* Make map filter work on enumerable drops (#233) [Florian Weingarten]
* Improved whitespace stripping for blank blocks, related to #216 [Florian Weingarten]
## 2.6.3 / 2015-07-23 / branch "2-6-stable"
* Fix test failure under certain timezones [Dylan Thacker-Smith]
## 2.6.2 / 2015-01-23
* Remove duplicate hash key [Parker Moore]
## 2.6.1 / 2014-01-10
Security fix, cherry-picked from master (4e14a65): Security fix, cherry-picked from master (4e14a65):
* Don't call to_sym when creating conditions for security reasons, see #273 [Bouke van der Bijl, bouk] * Don't call to_sym when creating conditions for security reasons (#273) [Bouke van der Bijl]
* Prevent arbitrary method invocation on condition objects, see #274 [Dylan Thacker-Smith, dylanahsmith] * Prevent arbitrary method invocation on condition objects (#274) [Dylan Thacker-Smith]
## 2.6.0 / 2013-11-25 ## 2.6.0 / 2013-11-25
@@ -47,37 +172,37 @@ IMPORTANT: Liquid 2.6 is going to be the last version of Liquid which maintains
The following releases will only be tested against Ruby 1.9 and Ruby 2.0 and are likely to break on Ruby 1.8. The following releases will only be tested against Ruby 1.9 and Ruby 2.0 and are likely to break on Ruby 1.8.
* Bugfix for #106: fix example servlet [gnowoel] * Bugfix for #106: fix example servlet [gnowoel]
* Bugfix for #97: strip_html filter supports multi-line tags [Jo Liss, joliss] * Bugfix for #97: strip_html filter supports multi-line tags [Jo Liss]
* Bugfix for #114: strip_html filter supports style tags [James Allardice, jamesallardice] * Bugfix for #114: strip_html filter supports style tags [James Allardice]
* Bugfix for #117: 'now' support for date filter in Ruby 1.9 [Notre Dame Webgroup, ndwebgroup] * Bugfix for #117: 'now' support for date filter in Ruby 1.9 [Notre Dame Webgroup]
* Bugfix for #166: truncate filter on UTF-8 strings with Ruby 1.8 [Florian Weingarten, fw42] * Bugfix for #166: truncate filter on UTF-8 strings with Ruby 1.8 [Florian Weingarten]
* Bugfix for #204: 'raw' parsing bug [Florian Weingarten, fw42] * Bugfix for #204: 'raw' parsing bug [Florian Weingarten]
* Bugfix for #150: 'for' parsing bug [Peter Schröder, phoet] * Bugfix for #150: 'for' parsing bug [Peter Schröder]
* Bugfix for #126: Strip CRLF in strip_newline [Peter Schröder, phoet] * Bugfix for #126: Strip CRLF in strip_newline [Peter Schröder]
* Bugfix for #174, "can't convert Fixnum into String" for "replace" [wǒ_is神仙, jsw0528] * Bugfix for #174, "can't convert Fixnum into String" for "replace" [jsw0528]
* Allow a Liquid::Drop to be passed into Template#render [Daniel Huckstep, darkhelmet] * Allow a Liquid::Drop to be passed into Template#render [Daniel Huckstep]
* Resource limits [Florian Weingarten, fw42] * Resource limits [Florian Weingarten]
* Add reverse filter [Jay Strybis, unreal] * Add reverse filter [Jay Strybis]
* Add utf-8 support * Add utf-8 support
* Use array instead of Hash to keep the registered filters [Tasos Stathopoulos, astathopoulos] * Use array instead of Hash to keep the registered filters [Tasos Stathopoulos]
* Cache tokenized partial templates [Tom Burns, boourns] * Cache tokenized partial templates [Tom Burns]
* Avoid warnings in Ruby 1.9.3 [Marcus Stollsteimer, stomar] * Avoid warnings in Ruby 1.9.3 [Marcus Stollsteimer]
* Better documentation for 'include' tag (closes #163) [Peter Schröder, phoet] * Better documentation for 'include' tag (closes #163) [Peter Schröder]
* Use of BigDecimal on filters to have better precision (closes #155) [Arthur Nogueira Neves, arthurnn] * Use of BigDecimal on filters to have better precision (closes #155) [Arthur Nogueira Neves]
## 2.5.5 / 2014-01-10 / branch "2-5-stable" ## 2.5.5 / 2014-01-10 / branch "2-5-stable"
Security fix, cherry-picked from master (4e14a65): Security fix, cherry-picked from master (4e14a65):
* Don't call to_sym when creating conditions for security reasons, see #273 [Bouke van der Bijl, bouk] * Don't call to_sym when creating conditions for security reasons (#273) [Bouke van der Bijl]
* Prevent arbitrary method invocation on condition objects, see #274 [Dylan Thacker-Smith, dylanahsmith] * Prevent arbitrary method invocation on condition objects (#274) [Dylan Thacker-Smith]
## 2.5.4 / 2013-11-11 ## 2.5.4 / 2013-11-11
* Fix "can't convert Fixnum into String" for "replace", see #173, [wǒ_is神仙, jsw0528] * Fix "can't convert Fixnum into String" for "replace" (#173), [jsw0528]
## 2.5.3 / 2013-10-09 ## 2.5.3 / 2013-10-09
* #232, #234, #237: Fix map filter bugs [Florian Weingarten, fw42] * #232, #234, #237: Fix map filter bugs [Florian Weingarten]
## 2.5.2 / 2013-09-03 / deleted ## 2.5.2 / 2013-09-03 / deleted
@@ -85,7 +210,7 @@ Yanked from rubygems, as it contained too many changes that broke compatibility.
## 2.5.1 / 2013-07-24 ## 2.5.1 / 2013-07-24
* #230: Fix security issue with map filter, Use invoke_drop in map filter [Florian Weingarten, fw42] * #230: Fix security issue with map filter, Use invoke_drop in map filter [Florian Weingarten]
## 2.5.0 / 2013-03-06 ## 2.5.0 / 2013-03-06

View File

@@ -1,5 +1,5 @@
[![Build Status](https://secure.travis-ci.org/Shopify/liquid.png?branch=master)](http://travis-ci.org/Shopify/liquid) [![Build Status](https://api.travis-ci.org/Shopify/liquid.svg?branch=master)](http://travis-ci.org/Shopify/liquid)
[![Inline docs](http://inch-ci.org/github/Shopify/liquid.png)](http://inch-ci.org/github/Shopify/liquid) [![Inline docs](http://inch-ci.org/github/Shopify/liquid.svg?branch=master)](http://inch-ci.org/github/Shopify/liquid)
# Liquid template engine # Liquid template engine
@@ -42,6 +42,8 @@ Liquid is a template engine which was written with very specific requirements:
## How to use Liquid ## How to use Liquid
Install Liquid by adding `gem 'liquid'` to your gemfile.
Liquid supports a very simple API based around the Liquid::Template class. Liquid supports a very simple API based around the Liquid::Template class.
For standard use you can just pass it the content of a file and call render with a parameters hash. For standard use you can just pass it the content of a file and call render with a parameters hash.
@@ -73,3 +75,34 @@ This is useful for doing things like enabling strict mode only in the theme edit
It is recommended that you enable `:strict` or `:warn` mode on new apps to stop invalid templates from being created. It is recommended that you enable `:strict` or `:warn` mode on new apps to stop invalid templates from being created.
It is also recommended that you use it in the template editors of existing apps to give editors better error messages. It is also recommended that you use it in the template editors of existing apps to give editors better error messages.
### Undefined variables and filters
By default, the renderer doesn't raise or in any other way notify you if some variables or filters are missing, i.e. not passed to the `render` method.
You can improve this situation by passing `strict_variables: true` and/or `strict_filters: true` options to the `render` method.
When one of these options is set to true, all errors about undefined variables and undefined filters will be stored in `errors` array of a `Liquid::Template` instance.
Here are some examples:
```ruby
template = Liquid::Template.parse("{{x}} {{y}} {{z.a}} {{z.b}}")
template.render({ 'x' => 1, 'z' => { 'a' => 2 } }, { strict_variables: true })
#=> '1 2 ' # when a variable is undefined, it's rendered as nil
template.errors
#=> [#<Liquid::UndefinedVariable: Liquid error: undefined variable y>, #<Liquid::UndefinedVariable: Liquid error: undefined variable b>]
```
```ruby
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>]
```
If you want to raise on a first exception instead of pushing all of them in `errors`, you can use `render!` method:
```ruby
template = Liquid::Template.parse("{{x}} {{y}}")
template.render!({ 'x' => 1}, { strict_variables: true })
#=> Liquid::UndefinedVariable: Liquid error: undefined variable y
```

View File

@@ -3,7 +3,7 @@ require 'rake/testtask'
$LOAD_PATH.unshift File.expand_path("../lib", __FILE__) $LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
require "liquid/version" require "liquid/version"
task :default => 'test' task default: [:test, :rubocop]
desc 'run test suite with default parser' desc 'run test suite with default parser'
Rake::TestTask.new(:base_test) do |t| Rake::TestTask.new(:base_test) do |t|
@@ -18,25 +18,43 @@ task :warn_test do
Rake::Task['base_test'].invoke Rake::Task['base_test'].invoke
end end
task :rubocop do
require 'rubocop/rake_task'
RuboCop::RakeTask.new
end
desc 'runs test suite with both strict and lax parsers' desc 'runs test suite with both strict and lax parsers'
task :test do task :test do
ENV['LIQUID_PARSER_MODE'] = 'lax' ENV['LIQUID_PARSER_MODE'] = 'lax'
Rake::Task['base_test'].invoke Rake::Task['base_test'].invoke
ENV['LIQUID_PARSER_MODE'] = 'strict' ENV['LIQUID_PARSER_MODE'] = 'strict'
Rake::Task['base_test'].reenable Rake::Task['base_test'].reenable
Rake::Task['base_test'].invoke Rake::Task['base_test'].invoke
if RUBY_ENGINE == 'ruby'
ENV['LIQUID-C'] = '1'
ENV['LIQUID_PARSER_MODE'] = 'lax'
Rake::Task['base_test'].reenable
Rake::Task['base_test'].invoke
ENV['LIQUID_PARSER_MODE'] = 'strict'
Rake::Task['base_test'].reenable
Rake::Task['base_test'].invoke
end
end end
task :gem => :build task gem: :build
task :build do task :build do
system "gem build liquid.gemspec" system "gem build liquid.gemspec"
end end
task :install => :build do task install: :build do
system "gem install liquid-#{Liquid::VERSION}.gem" system "gem install liquid-#{Liquid::VERSION}.gem"
end end
task :release => :build do task release: :build do
system "git tag -a v#{Liquid::VERSION} -m 'Tagging #{Liquid::VERSION}'" system "git tag -a v#{Liquid::VERSION} -m 'Tagging #{Liquid::VERSION}'"
system "git push --tags" system "git push --tags"
system "gem push liquid-#{Liquid::VERSION}.gem" system "gem push liquid-#{Liquid::VERSION}.gem"
@@ -44,7 +62,6 @@ task :release => :build do
end end
namespace :benchmark do namespace :benchmark do
desc "Run the liquid benchmark with lax parsing" desc "Run the liquid benchmark with lax parsing"
task :run do task :run do
ruby "./performance/benchmark.rb lax" ruby "./performance/benchmark.rb lax"
@@ -56,9 +73,7 @@ namespace :benchmark do
end end
end end
namespace :profile do namespace :profile do
desc "Run the liquid profile/performance coverage" desc "Run the liquid profile/performance coverage"
task :run do task :run do
ruby "./performance/profile.rb" ruby "./performance/profile.rb"
@@ -68,10 +83,20 @@ namespace :profile do
task :strict do task :strict do
ruby "./performance/profile.rb strict" ruby "./performance/profile.rb strict"
end end
end
namespace :memory_profile do
desc "Run memory profiler"
task :run do
ruby "./performance/memory_profile.rb"
end
end end
desc "Run example" desc "Run example"
task :example do task :example do
ruby "-w -d -Ilib example/server/server.rb" ruby "-w -d -Ilib example/server/server.rb"
end end
task :console do
exec 'irb -I lib -r liquid'
end

View File

@@ -4,7 +4,7 @@ module ProductsFilter
end end
def prettyprint(text) def prettyprint(text)
text.gsub( /\*(.*)\*/, '<b>\1</b>' ) text.gsub(/\*(.*)\*/, '<b>\1</b>')
end end
def count(array) def count(array)
@@ -17,25 +17,28 @@ module ProductsFilter
end end
class Servlet < LiquidServlet class Servlet < LiquidServlet
def index def index
{ 'date' => Time.now } { 'date' => Time.now }
end end
def products def products
{ 'products' => products_list, 'description' => description, 'section' => 'Snowboards', 'cool_products' => true} { 'products' => products_list, 'more_products' => more_products_list, 'description' => description, 'section' => 'Snowboards', 'cool_products' => true }
end end
private private
def products_list def products_list
[{'name' => 'Arbor Draft', 'price' => 39900, 'description' => 'the *arbor draft* is a excellent product' }, [{ 'name' => 'Arbor Draft', 'price' => 39900, 'description' => 'the *arbor draft* is a excellent product' },
{'name' => 'Arbor Element', 'price' => 40000, 'description' => 'the *arbor element* rocks for freestyling'}, { 'name' => 'Arbor Element', 'price' => 40000, 'description' => 'the *arbor element* rocks for freestyling' },
{'name' => 'Arbor Diamond', 'price' => 59900, 'description' => 'the *arbor diamond* is a made up product because im obsessed with arbor and have no creativity'}] { 'name' => 'Arbor Diamond', 'price' => 59900, 'description' => 'the *arbor diamond* is a made up product because im obsessed with arbor and have no creativity' }]
end
def more_products_list
[{ 'name' => 'Arbor Catalyst', 'price' => 39900, 'description' => 'the *arbor catalyst* is an advanced drop-through for freestyle and flatground performance and versatility' },
{ 'name' => 'Arbor Fish', 'price' => 40000, 'description' => 'the *arbor fish* is a compact pin that features an extended wheelbase and time-honored teardrop shape' }]
end end
def description def description
"List of Products ~ This is a list of products with price and description." "List of Products ~ This is a list of products with price and description."
end end
end end

View File

@@ -1,5 +1,4 @@
class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet
def do_GET(req, res) def do_GET(req, res)
handle(:get, req, res) handle(:get, req, res)
end end
@@ -11,7 +10,8 @@ class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet
private private
def handle(type, req, res) def handle(type, req, res)
@request, @response = req, res @request = req
@response = res
@request.path_info =~ /(\w+)\z/ @request.path_info =~ /(\w+)\z/
@action = $1 || 'index' @action = $1 || 'index'
@@ -19,10 +19,10 @@ class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet
@response['Content-Type'] = "text/html" @response['Content-Type'] = "text/html"
@response.status = 200 @response.status = 200
@response.body = Liquid::Template.parse(read_template).render(@assigns, :filters => [ProductsFilter]) @response.body = Liquid::Template.parse(read_template).render(@assigns, filters: [ProductsFilter])
end end
def read_template(filename = @action) def read_template(filename = @action)
File.read( File.dirname(__FILE__) + "/templates/#{filename}.liquid" ) File.read("#{__dir__}/templates/#{filename}.liquid")
end end
end end

View File

@@ -1,14 +1,12 @@
require 'webrick' require 'webrick'
require 'rexml/document' require 'rexml/document'
DIR = File.expand_path(File.dirname(__FILE__)) require_relative '../../lib/liquid'
require_relative 'liquid_servlet'
require DIR + '/../../lib/liquid' require_relative 'example_servlet'
require DIR + '/liquid_servlet'
require DIR + '/example_servlet'
# Setup webrick # Setup webrick
server = WEBrick::HTTPServer.new( :Port => ARGV[1] || 3000 ) server = WEBrick::HTTPServer.new(Port: ARGV[1] || 3000)
server.mount('/', Servlet) server.mount('/', Servlet)
trap("INT"){ server.shutdown } trap("INT"){ server.shutdown }
server.start server.start

View File

@@ -16,12 +16,12 @@
</head> </head>
<body> <body>
{% assign all_products = products | concat: more_products %}
<h1>{{ description | split: '~' | first }}</h1> <h1>{{ description | split: '~' | first }}</h1>
<h2>{{ description | split: '~' | last }}</h2> <h2>{{ description | split: '~' | last }}</h2>
<h2>There are currently {{products | count}} products in the {{section}} catalog</h2> <h2>There are currently {{all_products | count}} products in the {{section}} catalog</h2>
{% if cool_products %} {% if cool_products %}
Cool products :) Cool products :)
@@ -31,7 +31,7 @@
<ul id="products"> <ul id="products">
{% for product in products %} {% for product in all_products %}
<li> <li>
<h2>{{product.name}}</h2> <h2>{{product.name}}</h2>
Only {{product.price | price }} Only {{product.price | price }}

View File

@@ -24,6 +24,7 @@ module Liquid
ArgumentSeparator = ','.freeze ArgumentSeparator = ','.freeze
FilterArgumentSeparator = ':'.freeze FilterArgumentSeparator = ':'.freeze
VariableAttributeSeparator = '.'.freeze VariableAttributeSeparator = '.'.freeze
WhitespaceControl = '-'.freeze
TagStart = /\{\%/ TagStart = /\{\%/
TagEnd = /\%\}/ TagEnd = /\%\}/
VariableSignature = /\(?[\w\-\.\[\]]\)?/ VariableSignature = /\(?[\w\-\.\[\]]\)?/
@@ -34,7 +35,7 @@ module Liquid
QuotedString = /"[^"]*"|'[^']*'/ QuotedString = /"[^"]*"|'[^']*'/
QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o
TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/o TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/o
AnyStartingTag = /\{\{|\{\%/ AnyStartingTag = /#{TagStart}|#{VariableStart}/o
PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om
VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o
@@ -44,10 +45,13 @@ module Liquid
end end
require "liquid/version" require "liquid/version"
require 'liquid/parse_tree_visitor'
require 'liquid/lexer' require 'liquid/lexer'
require 'liquid/parser' require 'liquid/parser'
require 'liquid/i18n' require 'liquid/i18n'
require 'liquid/drop' require 'liquid/drop'
require 'liquid/tablerowloop_drop'
require 'liquid/forloop_drop'
require 'liquid/extensions' require 'liquid/extensions'
require 'liquid/errors' require 'liquid/errors'
require 'liquid/interrupts' require 'liquid/interrupts'
@@ -57,21 +61,21 @@ require 'liquid/context'
require 'liquid/parser_switching' require 'liquid/parser_switching'
require 'liquid/tag' require 'liquid/tag'
require 'liquid/block' require 'liquid/block'
require 'liquid/block_body'
require 'liquid/document' require 'liquid/document'
require 'liquid/variable' require 'liquid/variable'
require 'liquid/variable_lookup' require 'liquid/variable_lookup'
require 'liquid/range_lookup' require 'liquid/range_lookup'
require 'liquid/file_system' require 'liquid/file_system'
require 'liquid/resource_limits'
require 'liquid/template' require 'liquid/template'
require 'liquid/standardfilters' require 'liquid/standardfilters'
require 'liquid/condition' require 'liquid/condition'
require 'liquid/module_ex'
require 'liquid/utils' require 'liquid/utils'
require 'liquid/token' require 'liquid/tokenizer'
require 'liquid/parse_context'
require 'liquid/reversable_range'
# Load all the tags of the standard library # Load all the tags of the standard library
# #
Dir[File.dirname(__FILE__) + '/liquid/tags/*.rb'].each { |f| require f } Dir["#{__dir__}/liquid/tags/*.rb"].each { |f| require f }
require 'liquid/profiler'
require 'liquid/profiler/hooks'

View File

@@ -1,96 +1,41 @@
module Liquid module Liquid
class Block < Tag class Block < Tag
FullToken = /\A#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om MAX_DEPTH = 100
ContentOfVariable = /\A#{VariableStart}(.*)#{VariableEnd}\z/om
TAGSTART = "{%".freeze def initialize(tag_name, markup, options)
VARSTART = "{{".freeze super
@blank = true
end
def parse(tokens)
@body = BlockBody.new
while parse_body(@body, tokens)
end
end
def render(context)
@body.render(context)
end
def blank? def blank?
@blank @blank
end end
def parse(tokens) def nodelist
@blank = true @body.nodelist
@nodelist ||= []
@nodelist.clear
# All child tags of the current block.
@children = []
while token = tokens.shift
unless token.empty?
case
when token.start_with?(TAGSTART)
if token =~ FullToken
# if we found the proper block delimiter just end parsing here and let the outer block
# proceed
if block_delimiter == $1
end_tag
return
end
# fetch the tag from registered blocks
if tag = Template.tags[$1]
markup = token.is_a?(Token) ? token.child($2) : $2
new_tag = tag.parse($1, markup, tokens, @options)
new_tag.line_number = token.line_number if token.is_a?(Token)
@blank &&= new_tag.blank?
@nodelist << new_tag
@children << new_tag
else
# this tag is not registered with the system
# pass it to the current block for special handling or error reporting
unknown_tag($1, $2, tokens)
end
else
raise SyntaxError.new(options[:locale].t("errors.syntax.tag_termination".freeze, :token => token, :tag_end => TagEnd.inspect))
end
when token.start_with?(VARSTART)
new_var = create_variable(token)
new_var.line_number = token.line_number if token.is_a?(Token)
@nodelist << new_var
@children << new_var
@blank = false
else
@nodelist << token
@blank &&= (token =~ /\A\s*\z/)
end
end
end
# Make sure that it's ok to end parsing in the current block.
# Effectively this method will throw an exception unless the current block is
# of type Document
assert_missing_delimitation!
end end
# warnings of this block and all sub-tags def unknown_tag(tag, _params, _tokens)
def warnings if tag == 'else'.freeze
all_warnings = [] raise SyntaxError.new(parse_context.locale.t("errors.syntax.unexpected_else".freeze,
all_warnings.concat(@warnings) if @warnings block_name: block_name))
elsif tag.start_with?('end'.freeze)
(@children || []).each do |node| raise SyntaxError.new(parse_context.locale.t("errors.syntax.invalid_delimiter".freeze,
all_warnings.concat(node.warnings || []) tag: tag,
end block_name: block_name,
block_delimiter: block_delimiter))
all_warnings
end
def end_tag
end
def unknown_tag(tag, params, tokens)
case tag
when 'else'.freeze
raise SyntaxError.new(options[:locale].t("errors.syntax.unexpected_else".freeze,
:block_name => block_name))
when 'end'.freeze
raise SyntaxError.new(options[:locale].t("errors.syntax.invalid_delimiter".freeze,
:block_name => block_name,
:block_delimiter => block_delimiter))
else else
raise SyntaxError.new(options[:locale].t("errors.syntax.unknown_tag".freeze, :tag => tag)) raise SyntaxError.new(parse_context.locale.t("errors.syntax.unknown_tag".freeze, tag: tag))
end end
end end
@@ -102,65 +47,31 @@ module Liquid
@block_delimiter ||= "end#{block_name}" @block_delimiter ||= "end#{block_name}"
end end
def create_variable(token)
token.scan(ContentOfVariable) do |content|
markup = token.is_a?(Token) ? token.child(content.first) : content.first
return Variable.new(markup, @options)
end
raise SyntaxError.new(options[:locale].t("errors.syntax.variable_termination".freeze, :token => token, :tag_end => VariableEnd.inspect))
end
def render(context)
render_all(@nodelist, context)
end
protected protected
def assert_missing_delimitation! def parse_body(body, tokens)
raise SyntaxError.new(options[:locale].t("errors.syntax.tag_never_closed".freeze, :block_name => block_name)) if parse_context.depth >= MAX_DEPTH
end raise StackLevelError, "Nesting too deep".freeze
end
parse_context.depth += 1
begin
body.parse(tokens, parse_context) do |end_tag_name, end_tag_params|
@blank &&= body.blank?
def render_all(list, context) return false if end_tag_name == block_delimiter
output = [] unless end_tag_name
context.resource_limits[:render_length_current] = 0 raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name))
context.resource_limits[:render_score_current] += list.length
list.each do |token|
# Break out if we have any unhanded interrupts.
break if context.has_interrupt?
begin
# If we get an Interrupt that means the block must stop processing. An
# Interrupt is any command that stops block execution such as {% break %}
# or {% continue %}
if token.is_a? Continue or token.is_a? Break
context.push_interrupt(token.interrupt)
break
end end
token_output = render_token(token, context) # this tag is not registered with the system
# pass it to the current block for special handling or error reporting
unless token.is_a?(Block) && token.blank? unknown_tag(end_tag_name, end_tag_params, tokens)
output << token_output
end
rescue MemoryError => e
raise e
rescue ::StandardError => e
output << (context.handle_error(e, token))
end end
ensure
parse_context.depth -= 1
end end
output.join true
end
def render_token(token, context)
token_output = (token.respond_to?(:render) ? token.render(context) : token)
context.increment_used_resources(:render_length_current, token_output)
if context.resource_limits_reached?
context.resource_limits[:reached] = true
raise MemoryError.new("Memory limits exceeded".freeze)
end
token_output
end end
end end
end end

143
lib/liquid/block_body.rb Normal file
View File

@@ -0,0 +1,143 @@
module Liquid
class BlockBody
FullToken = /\A#{TagStart}#{WhitespaceControl}?\s*(\w+)\s*(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
WhitespaceOrNothing = /\A\s*\z/
TAGSTART = "{%".freeze
VARSTART = "{{".freeze
attr_reader :nodelist
def initialize
@nodelist = []
@blank = true
end
def parse(tokenizer, parse_context)
parse_context.line_number = tokenizer.line_number
while token = tokenizer.shift
next if token.empty?
case
when token.start_with?(TAGSTART)
whitespace_handler(token, parse_context)
unless token =~ FullToken
raise_missing_tag_terminator(token, parse_context)
end
tag_name = $1
markup = $2
# fetch the tag from registered blocks
unless tag = registered_tags[tag_name]
# end parsing if we reach an unknown tag and let the caller decide
# determine how to proceed
return yield tag_name, markup
end
new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
@blank &&= new_tag.blank?
@nodelist << new_tag
when token.start_with?(VARSTART)
whitespace_handler(token, parse_context)
@nodelist << create_variable(token, parse_context)
@blank = false
else
if parse_context.trim_whitespace
token.lstrip!
end
parse_context.trim_whitespace = false
@nodelist << token
@blank &&= !!(token =~ WhitespaceOrNothing)
end
parse_context.line_number = tokenizer.line_number
end
yield nil, nil
end
def whitespace_handler(token, parse_context)
if token[2] == WhitespaceControl
previous_token = @nodelist.last
if previous_token.is_a? String
previous_token.rstrip!
end
end
parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
end
def blank?
@blank
end
def render(context)
output = []
context.resource_limits.render_score += @nodelist.length
idx = 0
while node = @nodelist[idx]
case node
when String
check_resources(context, node)
output << node
when Variable
render_node_to_output(node, output, context)
when Block
render_node_to_output(node, output, context, node.blank?)
break if context.interrupt? # might have happened in a for-block
when Continue, Break
# If we get an Interrupt that means the block must stop processing. An
# Interrupt is any command that stops block execution such as {% break %}
# or {% continue %}
context.push_interrupt(node.interrupt)
break
else # Other non-Block tags
render_node_to_output(node, output, context)
break if context.interrupt? # might have happened through an include
end
idx += 1
end
output.join
end
private
def render_node_to_output(node, output, context, skip_output = false)
node_output = node.render(context)
node_output = node_output.is_a?(Array) ? node_output.join : node_output.to_s
check_resources(context, node_output)
output << node_output unless skip_output
rescue MemoryError => e
raise e
rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
context.handle_error(e, node.line_number)
output << nil
rescue ::StandardError => e
line_number = node.is_a?(String) ? nil : node.line_number
output << context.handle_error(e, line_number)
end
def check_resources(context, node_output)
context.resource_limits.render_length += node_output.bytesize
return unless context.resource_limits.reached?
raise MemoryError.new("Memory limits exceeded".freeze)
end
def create_variable(token, parse_context)
token.scan(ContentOfVariable) do |content|
markup = content.first
return Variable.new(markup, parse_context)
end
raise_missing_variable_terminator(token, parse_context)
end
def raise_missing_tag_terminator(token, parse_context)
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_termination".freeze, token: token, tag_end: TagEnd.inspect))
end
def raise_missing_variable_terminator(token, parse_context)
raise SyntaxError.new(parse_context.locale.t("errors.syntax.variable_termination".freeze, token: token, tag_end: VariableEnd.inspect))
end
def registered_tags
Template.tags
end
end
end

View File

@@ -3,55 +3,70 @@ module Liquid
# #
# Example: # Example:
# #
# c = Condition.new('1', '==', '1') # c = Condition.new(1, '==', 1)
# c.evaluate #=> true # c.evaluate #=> true
# #
class Condition #:nodoc: class Condition #:nodoc:
@@operators = { @@operators = {
'=='.freeze => lambda { |cond, left, right| cond.send(:equal_variables, left, right) }, '=='.freeze => ->(cond, left, right) { cond.send(:equal_variables, left, right) },
'!='.freeze => lambda { |cond, left, right| !cond.send(:equal_variables, left, right) }, '!='.freeze => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
'<>'.freeze => lambda { |cond, left, right| !cond.send(:equal_variables, left, right) }, '<>'.freeze => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
'<'.freeze => :<, '<'.freeze => :<,
'>'.freeze => :>, '>'.freeze => :>,
'>='.freeze => :>=, '>='.freeze => :>=,
'<='.freeze => :<=, '<='.freeze => :<=,
'contains'.freeze => lambda { |cond, left, right| 'contains'.freeze => lambda do |cond, left, right|
left && right && left.respond_to?(:include?) ? left.include?(right) : false if left && right && left.respond_to?(:include?)
} right = right.to_s if left.is_a?(String)
left.include?(right)
else
false
end
end
} }
def self.operators def self.operators
@@operators @@operators
end end
attr_reader :attachment attr_reader :attachment, :child_condition
attr_accessor :left, :operator, :right attr_accessor :left, :operator, :right
def initialize(left = nil, operator = nil, right = nil) def initialize(left = nil, operator = nil, right = nil)
@left, @operator, @right = left, operator, right @left = left
@operator = operator
@right = right
@child_relation = nil @child_relation = nil
@child_condition = nil @child_condition = nil
end end
def evaluate(context = Context.new) def evaluate(context = Context.new)
result = interpret_condition(left, right, operator, context) condition = self
result = nil
loop do
result = interpret_condition(condition.left, condition.right, condition.operator, context)
case @child_relation case condition.child_relation
when :or when :or
result || @child_condition.evaluate(context) break if result
when :and when :and
result && @child_condition.evaluate(context) break unless result
else else
result break
end
condition = condition.child_condition
end end
result
end end
def or(condition) def or(condition)
@child_relation, @child_condition = :or, condition @child_relation = :or
@child_condition = condition
end end
def and(condition) def and(condition)
@child_relation, @child_condition = :and, condition @child_relation = :and
@child_condition = condition
end end
def attach(attachment) def attach(attachment)
@@ -66,20 +81,24 @@ module Liquid
"#<Condition #{[@left, @operator, @right].compact.join(' '.freeze)}>" "#<Condition #{[@left, @operator, @right].compact.join(' '.freeze)}>"
end end
protected
attr_reader :child_relation
private private
def equal_variables(left, right) def equal_variables(left, right)
if left.is_a?(Symbol) if left.is_a?(Liquid::Expression::MethodLiteral)
if right.respond_to?(left) if right.respond_to?(left.method_name)
return right.send(left.to_s) return right.send(left.method_name)
else else
return nil return nil
end end
end end
if right.is_a?(Symbol) if right.is_a?(Liquid::Expression::MethodLiteral)
if left.respond_to?(right) if left.respond_to?(right.method_name)
return left.send(right.to_s) return left.send(right.method_name)
else else
return nil return nil
end end
@@ -92,35 +111,41 @@ module Liquid
# If the operator is empty this means that the decision statement is just # If the operator is empty this means that the decision statement is just
# a single variable. We can just poll this variable from the context and # a single variable. We can just poll this variable from the context and
# return this as the result. # return this as the result.
return context[left] if op == nil return context.evaluate(left) if op.nil?
left, right = context[left], context[right] left = context.evaluate(left)
right = context.evaluate(right)
operation = self.class.operators[op] || raise(Liquid::ArgumentError.new("Unknown operator #{op}")) operation = self.class.operators[op] || raise(Liquid::ArgumentError.new("Unknown operator #{op}"))
if operation.respond_to?(:call) if operation.respond_to?(:call)
operation.call(self, left, right) operation.call(self, left, right)
elsif left.respond_to?(operation) and right.respond_to?(operation) elsif left.respond_to?(operation) && right.respond_to?(operation) && !left.is_a?(Hash) && !right.is_a?(Hash)
begin begin
left.send(operation, right) left.send(operation, right)
rescue ::ArgumentError => e rescue ::ArgumentError => e
raise Liquid::ArgumentError.new(e.message) raise Liquid::ArgumentError.new(e.message)
end end
else end
nil end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[
@node.left, @node.right,
@node.child_condition, @node.attachment
].compact
end end
end end
end end
class ElseCondition < Condition class ElseCondition < Condition
def else? def else?
true true
end end
def evaluate(context) def evaluate(_context)
true true
end end
end end
end end

View File

@@ -1,5 +1,4 @@
module Liquid module Liquid
# Context keeps the variable stack and resolves variables, as well as keywords # Context keeps the variable stack and resolves variables, as well as keywords
# #
# context['variable'] = 'testing' # context['variable'] = 'testing'
@@ -14,41 +13,32 @@ module Liquid
# context['bob'] #=> nil class Context # context['bob'] #=> nil class Context
class Context class Context
attr_reader :scopes, :errors, :registers, :environments, :resource_limits attr_reader :scopes, :errors, :registers, :environments, :resource_limits
attr_accessor :exception_handler attr_accessor :exception_renderer, :template_name, :partial, :global_filter, :strict_variables, :strict_filters
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil) def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil)
@environments = [environments].flatten @environments = [environments].flatten
@scopes = [(outer_scope || {})] @scopes = [(outer_scope || {})]
@registers = registers @registers = registers
@errors = [] @errors = []
@resource_limits = resource_limits || Template.default_resource_limits @partial = false
@resource_limits[:render_score_current] = 0 @strict_variables = false
@resource_limits[:assign_score_current] = 0 @resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
@parsed_expression = Hash.new{ |cache, markup| cache[markup] = Expression.parse(markup) }
squash_instance_assigns_with_environments squash_instance_assigns_with_environments
@this_stack_used = false @this_stack_used = false
self.exception_renderer = Template.default_exception_renderer
if rethrow_errors if rethrow_errors
self.exception_handler = ->(e) { true } self.exception_renderer = ->(e) { raise }
end end
@interrupts = [] @interrupts = []
@filters = [] @filters = []
@global_filter = nil
end end
def increment_used_resources(key, obj) def warnings
@resource_limits[key] += if obj.kind_of?(String) || obj.kind_of?(Array) || obj.kind_of?(Hash) @warnings ||= []
obj.length
else
1
end
end
def resource_limits_reached?
(@resource_limits[:render_length_limit] && @resource_limits[:render_length_current] > @resource_limits[:render_length_limit]) ||
(@resource_limits[:render_score_limit] && @resource_limits[:render_score_current] > @resource_limits[:render_score_limit] ) ||
(@resource_limits[:assign_score_limit] && @resource_limits[:assign_score_current] > @resource_limits[:assign_score_limit] )
end end
def strainer def strainer
@@ -61,25 +51,16 @@ module Liquid
# for that # for that
def add_filters(filters) def add_filters(filters)
filters = [filters].flatten.compact filters = [filters].flatten.compact
filters.each do |f| @filters += filters
raise ArgumentError, "Expected module but got: #{f.class}" unless f.is_a?(Module) @strainer = nil
Strainer.add_known_filter(f) end
end
# If strainer is already setup then there's no choice but to use a runtime def apply_global_filter(obj)
# extend call. If strainer is not yet created, we can utilize strainers global_filter.nil? ? obj : global_filter.call(obj)
# cached class based API, which avoids busting the method cache.
if @strainer
filters.each do |f|
strainer.extend(f)
end
else
@filters.concat filters
end
end end
# are there any not handled interrupts? # are there any not handled interrupts?
def has_interrupt? def interrupt?
!@interrupts.empty? !@interrupts.empty?
end end
@@ -93,15 +74,12 @@ module Liquid
@interrupts.pop @interrupts.pop
end end
def handle_error(e, line_number = nil)
def handle_error(e, token=nil) e = internal_error unless e.is_a?(Liquid::Error)
if e.is_a?(Liquid::Error) e.template_name ||= template_name
e.set_line_number_from_token(token) e.line_number ||= line_number
end
errors.push(e) errors.push(e)
raise if exception_handler && exception_handler.call(e) exception_renderer.call(e).to_s
Liquid::Error.render(e)
end end
def invoke(method, *args) def invoke(method, *args)
@@ -109,9 +87,9 @@ module Liquid
end end
# Push new local scope on the stack. use <tt>Context#stack</tt> instead # Push new local scope on the stack. use <tt>Context#stack</tt> instead
def push(new_scope={}) def push(new_scope = {})
@scopes.unshift(new_scope) @scopes.unshift(new_scope)
raise StackLevelError, "Nesting too deep".freeze if @scopes.length > 100 raise StackLevelError, "Nesting too deep".freeze if @scopes.length > Block::MAX_DEPTH
end end
# Merge a hash of variables in the current local scope # Merge a hash of variables in the current local scope
@@ -133,7 +111,7 @@ module Liquid
# end # end
# #
# context['var] #=> nil # context['var] #=> nil
def stack(new_scope=nil) def stack(new_scope = nil)
old_stack_used = @this_stack_used old_stack_used = @this_stack_used
if new_scope if new_scope
push(new_scope) push(new_scope)
@@ -170,10 +148,10 @@ module Liquid
# Example: # Example:
# products == empty #=> products.empty? # products == empty #=> products.empty?
def [](expression) def [](expression)
evaluate(@parsed_expression[expression]) evaluate(Expression.parse(expression))
end end
def has_key?(key) def key?(key)
self[key] != nil self[key] != nil
end end
@@ -182,36 +160,43 @@ module Liquid
end end
# Fetches an object starting at the local scope and then moving up the hierachy # Fetches an object starting at the local scope and then moving up the hierachy
def find_variable(key) def find_variable(key, raise_on_not_found: true)
# This was changed from find() to find_index() because this is a very hot # This was changed from find() to find_index() because this is a very hot
# path and find_index() is optimized in MRI to reduce object allocation # path and find_index() is optimized in MRI to reduce object allocation
index = @scopes.find_index { |s| s.has_key?(key) } index = @scopes.find_index { |s| s.key?(key) }
scope = @scopes[index] if index scope = @scopes[index] if index
variable = nil variable = nil
if scope.nil? if scope.nil?
@environments.each do |e| @environments.each do |e|
variable = lookup_and_evaluate(e, key) variable = lookup_and_evaluate(e, key, raise_on_not_found: raise_on_not_found)
unless variable.nil? # When lookup returned a value OR there is no value but the lookup also did not raise
# then it is the value we are looking for.
if !variable.nil? || @strict_variables && raise_on_not_found
scope = e scope = e
break break
end end
end end
end end
scope ||= @environments.last || @scopes.last scope ||= @environments.last || @scopes.last
variable ||= lookup_and_evaluate(scope, key) variable ||= lookup_and_evaluate(scope, key, raise_on_not_found: raise_on_not_found)
variable = variable.to_liquid variable = variable.to_liquid
variable.context = self if variable.respond_to?(:context=) variable.context = self if variable.respond_to?(:context=)
return variable variable
end end
def lookup_and_evaluate(obj, key) def lookup_and_evaluate(obj, key, raise_on_not_found: true)
if (value = obj[key]).is_a?(Proc) && obj.respond_to?(:[]=) if @strict_variables && raise_on_not_found && obj.respond_to?(:key?) && !obj.key?(key)
raise Liquid::UndefinedVariable, "undefined variable #{key}"
end
value = obj[key]
if value.is_a?(Proc) && obj.respond_to?(:[]=)
obj[key] = (value.arity == 0) ? value.call : value.call(self) obj[key] = (value.arity == 0) ? value.call : value.call(self)
else else
value value
@@ -219,15 +204,23 @@ module Liquid
end end
private private
def squash_instance_assigns_with_environments
@scopes.last.each_key do |k| def internal_error
@environments.each do |env| # raise and catch to set backtrace and cause on exception
if env.has_key?(k) raise Liquid::InternalError, 'internal'
scopes.last[k] = lookup_and_evaluate(env, k) rescue Liquid::InternalError => exc
break exc
end end
def squash_instance_assigns_with_environments
@scopes.last.each_key do |k|
@environments.each do |env|
if env.key?(k)
scopes.last[k] = lookup_and_evaluate(env, k)
break
end end
end end
end # squash_instance_assigns_with_environments end
end # squash_instance_assigns_with_environments
end # Context end # Context
end # Liquid end # Liquid

View File

@@ -1,17 +1,27 @@
module Liquid module Liquid
class Document < Block class Document < BlockBody
def self.parse(tokens, options={}) def self.parse(tokens, parse_context)
# we don't need markup to open this block doc = new
super(nil, nil, tokens, options) doc.parse(tokens, parse_context)
doc
end end
# There isn't a real delimiter def parse(tokens, parse_context)
def block_delimiter super do |end_tag_name, end_tag_params|
[] unknown_tag(end_tag_name, parse_context) if end_tag_name
end
rescue SyntaxError => e
e.line_number ||= parse_context.line_number
raise
end end
# Document blocks don't need to be terminated since they are not actually opened def unknown_tag(tag, parse_context)
def assert_missing_delimitation! case tag
when 'else'.freeze, 'end'.freeze
raise SyntaxError.new(parse_context.locale.t("errors.syntax.unexpected_outer_tag".freeze, tag: tag))
else
raise SyntaxError.new(parse_context.locale.t("errors.syntax.unknown_tag".freeze, tag: tag))
end
end end
end end
end end

View File

@@ -1,7 +1,6 @@
require 'set' require 'set'
module Liquid module Liquid
# A drop in liquid is a class which allows you to export DOM like things to liquid. # A drop in liquid is a class which allows you to export DOM like things to liquid.
# Methods of drops are callable. # Methods of drops are callable.
# The main use for liquid drops is to implement lazy loaded objects. # The main use for liquid drops is to implement lazy loaded objects.
@@ -19,28 +18,27 @@ module Liquid
# tmpl = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {%endfor%} ' ) # tmpl = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {%endfor%} ' )
# tmpl.render('product' => ProductDrop.new ) # will invoke top_sales query. # tmpl.render('product' => ProductDrop.new ) # will invoke top_sales query.
# #
# Your drop can either implement the methods sans any parameters or implement the before_method(name) method which is a # Your drop can either implement the methods sans any parameters
# catch all. # or implement the liquid_method_missing(name) method which is a catch all.
class Drop class Drop
attr_writer :context attr_writer :context
EMPTY_STRING = ''.freeze
# Catch all for the method # Catch all for the method
def before_method(method) def liquid_method_missing(method)
nil return nil unless @context && @context.strict_variables
raise Liquid::UndefinedDropMethod, "undefined method #{method}"
end end
# called by liquid to invoke a drop # called by liquid to invoke a drop
def invoke_drop(method_or_key) def invoke_drop(method_or_key)
if method_or_key && method_or_key != EMPTY_STRING && self.class.invokable?(method_or_key) if self.class.invokable?(method_or_key)
send(method_or_key) send(method_or_key)
else else
before_method(method_or_key) liquid_method_missing(method_or_key)
end end
end end
def has_key?(name) def key?(_name)
true true
end end
@@ -56,22 +54,25 @@ module Liquid
self.class.name self.class.name
end end
alias :[] :invoke_drop alias_method :[], :invoke_drop
private
# Check for method existence without invoking respond_to?, which creates symbols # Check for method existence without invoking respond_to?, which creates symbols
def self.invokable?(method_name) def self.invokable?(method_name)
unless @invokable_methods invokable_methods.include?(method_name.to_s)
end
def self.invokable_methods
@invokable_methods ||= begin
blacklist = Liquid::Drop.public_instance_methods + [:each] blacklist = Liquid::Drop.public_instance_methods + [:each]
if include?(Enumerable) if include?(Enumerable)
blacklist += Enumerable.public_instance_methods blacklist += Enumerable.public_instance_methods
blacklist -= [:sort, :count, :first, :min, :max, :include?] blacklist -= [:sort, :count, :first, :min, :max, :include?]
end end
whitelist = [:to_liquid] + (public_instance_methods - blacklist) whitelist = [:to_liquid] + (public_instance_methods - blacklist)
@invokable_methods = Set.new(whitelist.map(&:to_s)) Set.new(whitelist.map(&:to_s))
end end
@invokable_methods.include?(method_name.to_s)
end end
end end
end end

View File

@@ -1,9 +1,10 @@
module Liquid module Liquid
class Error < ::StandardError class Error < ::StandardError
attr_accessor :line_number attr_accessor :line_number
attr_accessor :template_name
attr_accessor :markup_context attr_accessor :markup_context
def to_s(with_prefix=true) def to_s(with_prefix = true)
str = "" str = ""
str << message_prefix if with_prefix str << message_prefix if with_prefix
str << super() str << super()
@@ -16,19 +17,6 @@ module Liquid
str str
end end
def set_line_number_from_token(token)
return unless token.respond_to?(:line_number)
self.line_number = token.line_number
end
def self.render(e)
if e.is_a?(Liquid::Error)
e.to_s
else
"Liquid error: #{e.to_s}"
end
end
private private
def message_prefix def message_prefix
@@ -40,7 +28,9 @@ module Liquid
end end
if line_number if line_number
str << " (line #{line_number})" str << " ("
str << template_name << " " if template_name
str << "line " << line_number.to_s << ")"
end end
str << ": " str << ": "
@@ -48,12 +38,19 @@ module Liquid
end end
end end
class ArgumentError < Error; end ArgumentError = Class.new(Error)
class ContextError < Error; end ContextError = Class.new(Error)
class FilterNotFound < Error; end FileSystemError = Class.new(Error)
class FileSystemError < Error; end StandardError = Class.new(Error)
class StandardError < Error; end SyntaxError = Class.new(Error)
class SyntaxError < Error; end StackLevelError = Class.new(Error)
class StackLevelError < Error; end TaintedError = Class.new(Error)
class MemoryError < Error; end MemoryError = Class.new(Error)
ZeroDivisionError = Class.new(Error)
FloatDomainError = Class.new(Error)
UndefinedVariable = Class.new(Error)
UndefinedDropMethod = Class.new(Error)
UndefinedFilter = Class.new(Error)
MethodOverrideError = Class.new(Error)
InternalError = Class.new(Error)
end end

View File

@@ -1,33 +1,49 @@
module Liquid module Liquid
class Expression class Expression
class MethodLiteral
attr_reader :method_name, :to_s
def initialize(method_name, to_s)
@method_name = method_name
@to_s = to_s
end
def to_liquid
to_s
end
end
LITERALS = { LITERALS = {
nil => nil, 'nil'.freeze => nil, 'null'.freeze => nil, ''.freeze => nil, nil => nil, 'nil'.freeze => nil, 'null'.freeze => nil, ''.freeze => nil,
'true'.freeze => true, 'true'.freeze => true,
'false'.freeze => false, 'false'.freeze => false,
'blank'.freeze => :blank?, 'blank'.freeze => MethodLiteral.new(:blank?, '').freeze,
'empty'.freeze => :empty? 'empty'.freeze => MethodLiteral.new(:empty?, '').freeze
} }.freeze
SINGLE_QUOTED_STRING = /\A'(.*)'\z/m
DOUBLE_QUOTED_STRING = /\A"(.*)"\z/m
INTEGERS_REGEX = /\A(-?\d+)\z/
FLOATS_REGEX = /\A(-?\d[\d\.]+)\z/
RANGES_REGEX = /\A\((\S+)\.\.(\S+)\)\z/
def self.parse(markup) def self.parse(markup)
if LITERALS.key?(markup) if LITERALS.key?(markup)
LITERALS[markup] LITERALS[markup]
else else
case markup case markup
when /\A'(.*)'\z/m # Single quoted strings when SINGLE_QUOTED_STRING, DOUBLE_QUOTED_STRING
$1 $1
when /\A"(.*)"\z/m # Double quoted strings when INTEGERS_REGEX
$1
when /\A(-?\d+)\z/ # Integer and floats
$1.to_i $1.to_i
when /\A\((\S+)\.\.(\S+)\)\z/ # Ranges when RANGES_REGEX
RangeLookup.parse($1, $2) RangeLookup.parse($1, $2)
when /\A(-?\d[\d\.]+)\z/ # Floats when FLOATS_REGEX
$1.to_f $1.to_f
else else
VariableLookup.parse(markup) VariableLookup.parse(markup)
end end
end end
end end
end end
end end

View File

@@ -7,44 +7,56 @@ class String # :nodoc:
end end
end end
class Array # :nodoc: class Symbol # :nodoc:
def to_liquid
to_s
end
end
class Array # :nodoc:
def to_liquid def to_liquid
self self
end end
end end
class Hash # :nodoc: class Hash # :nodoc:
def to_liquid def to_liquid
self self
end end
end end
class Numeric # :nodoc: class Numeric # :nodoc:
def to_liquid def to_liquid
self self
end end
end end
class Time # :nodoc: class Range # :nodoc:
def to_liquid def to_liquid
self self
end end
end end
class DateTime < Date # :nodoc: class Time # :nodoc:
def to_liquid def to_liquid
self self
end end
end end
class Date # :nodoc: class DateTime < Date # :nodoc:
def to_liquid
self
end
end
class Date # :nodoc:
def to_liquid def to_liquid
self self
end end
end end
class TrueClass class TrueClass
def to_liquid # :nodoc: def to_liquid # :nodoc:
self self
end end
end end

View File

@@ -8,13 +8,13 @@ module Liquid
# #
# Example: # Example:
# #
# Liquid::Template.file_system = Liquid::LocalFileSystem.new(template_path) # Liquid::Template.file_system = Liquid::LocalFileSystem.new(template_path)
# liquid = Liquid::Template.parse(template) # liquid = Liquid::Template.parse(template)
# #
# This will parse the template with a LocalFileSystem implementation rooted at 'template_path'. # This will parse the template with a LocalFileSystem implementation rooted at 'template_path'.
class BlankFileSystem class BlankFileSystem
# Called by Liquid to retrieve a template file # Called by Liquid to retrieve a template file
def read_template_file(template_path, context) def read_template_file(_template_path)
raise FileSystemError, "This liquid context does not allow includes." raise FileSystemError, "This liquid context does not allow includes."
end end
end end
@@ -26,10 +26,10 @@ module Liquid
# #
# Example: # Example:
# #
# file_system = Liquid::LocalFileSystem.new("/some/path") # file_system = Liquid::LocalFileSystem.new("/some/path")
# #
# file_system.full_path("mypartial") # => "/some/path/_mypartial.liquid" # file_system.full_path("mypartial") # => "/some/path/_mypartial.liquid"
# file_system.full_path("dir/mypartial") # => "/some/path/dir/_mypartial.liquid" # file_system.full_path("dir/mypartial") # => "/some/path/dir/_mypartial.liquid"
# #
# Optionally in the second argument you can specify a custom pattern for template filenames. # Optionally in the second argument you can specify a custom pattern for template filenames.
# The Kernel::sprintf format specification is used. # The Kernel::sprintf format specification is used.
@@ -37,9 +37,9 @@ module Liquid
# #
# Example: # Example:
# #
# file_system = Liquid::LocalFileSystem.new("/some/path", "%s.html") # file_system = Liquid::LocalFileSystem.new("/some/path", "%s.html")
# #
# file_system.full_path("index") # => "/some/path/index.html" # file_system.full_path("index") # => "/some/path/index.html"
# #
class LocalFileSystem class LocalFileSystem
attr_accessor :root attr_accessor :root
@@ -49,9 +49,9 @@ module Liquid
@pattern = pattern @pattern = pattern
end end
def read_template_file(template_path, context) def read_template_file(template_path)
full_path = full_path(template_path) full_path = full_path(template_path)
raise FileSystemError, "No such template '#{template_path}'" unless File.exists?(full_path) raise FileSystemError, "No such template '#{template_path}'" unless File.exist?(full_path)
File.read(full_path) File.read(full_path)
end end
@@ -65,7 +65,7 @@ module Liquid
File.join(root, @pattern % template_path) File.join(root, @pattern % template_path)
end end
raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path) =~ /\A#{File.expand_path(root)}/ raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path).start_with?(File.expand_path(root))
full_path full_path
end end

View File

@@ -0,0 +1,42 @@
module Liquid
class ForloopDrop < Drop
def initialize(name, length, parentloop)
@name = name
@length = length
@parentloop = parentloop
@index = 0
end
attr_reader :name, :length, :parentloop
def index
@index + 1
end
def index0
@index
end
def rindex
@length - @index
end
def rindex0
@length - @index - 1
end
def first
@index == 0
end
def last
@index == @length - 1
end
protected
def increment!
@index += 1
end
end
end

View File

@@ -2,10 +2,9 @@ require 'yaml'
module Liquid module Liquid
class I18n class I18n
DEFAULT_LOCALE = File.join(File.expand_path(File.dirname(__FILE__)), "locales", "en.yml") DEFAULT_LOCALE = File.join(File.expand_path(__dir__), "locales", "en.yml")
class TranslationError < StandardError TranslationError = Class.new(StandardError)
end
attr_reader :path attr_reader :path
@@ -23,11 +22,12 @@ module Liquid
end end
private private
def interpolate(name, vars) def interpolate(name, vars)
name.gsub(/%\{(\w+)\}/) { name.gsub(/%\{(\w+)\}/) do
# raise TranslationError, "Undefined key #{$1} for interpolation in translation #{name}" unless vars[$1.to_sym] # raise TranslationError, "Undefined key #{$1} for interpolation in translation #{name}" unless vars[$1.to_sym]
"#{vars[$1.to_sym]}" (vars[$1.to_sym]).to_s
} end
end end
def deep_fetch_translation(name) def deep_fetch_translation(name)

View File

@@ -1,10 +1,9 @@
module Liquid module Liquid
# An interrupt is any command that breaks processing of a block (ex: a for loop). # An interrupt is any command that breaks processing of a block (ex: a for loop).
class Interrupt class Interrupt
attr_reader :message attr_reader :message
def initialize(message=nil) def initialize(message = nil)
@message = message || "interrupt".freeze @message = message || "interrupt".freeze
end end
end end

View File

@@ -9,24 +9,28 @@ module Liquid
'['.freeze => :open_square, '['.freeze => :open_square,
']'.freeze => :close_square, ']'.freeze => :close_square,
'('.freeze => :open_round, '('.freeze => :open_round,
')'.freeze => :close_round ')'.freeze => :close_round,
} '?'.freeze => :question,
IDENTIFIER = /[\w\-?!]+/ '-'.freeze => :dash
}.freeze
IDENTIFIER = /[a-zA-Z_][\w-]*\??/
SINGLE_STRING_LITERAL = /'[^\']*'/ SINGLE_STRING_LITERAL = /'[^\']*'/
DOUBLE_STRING_LITERAL = /"[^\"]*"/ DOUBLE_STRING_LITERAL = /"[^\"]*"/
NUMBER_LITERAL = /-?\d+(\.\d+)?/ NUMBER_LITERAL = /-?\d+(\.\d+)?/
DOTDOT = /\.\./ DOTDOT = /\.\./
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains/ COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/
WHITESPACE_OR_NOTHING = /\s*/
def initialize(input) def initialize(input)
@ss = StringScanner.new(input.rstrip) @ss = StringScanner.new(input)
end end
def tokenize def tokenize
@output = [] @output = []
while !@ss.eos? until @ss.eos?
@ss.skip(/\s*/) @ss.skip(WHITESPACE_OR_NOTHING)
break if @ss.eos?
tok = case tok = case
when t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t] when t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t]
when t = @ss.scan(SINGLE_STRING_LITERAL) then [:string, t] when t = @ss.scan(SINGLE_STRING_LITERAL) then [:string, t]
@@ -37,7 +41,7 @@ module Liquid
else else
c = @ss.getch c = @ss.getch
if s = SPECIALS[c] if s = SPECIALS[c]
[s,c] [s, c]
else else
raise SyntaxError, "Unexpected character #{c}" raise SyntaxError, "Unexpected character #{c}"
end end

View File

@@ -1,6 +1,7 @@
--- ---
errors: errors:
syntax: syntax:
tag_unexpected_args: "Syntax Error in '%{tag}' - Valid syntax: %{tag}"
assign: "Syntax Error in 'assign' - Valid syntax: assign [var] = [source]" assign: "Syntax Error in 'assign' - Valid syntax: assign [var] = [source]"
capture: "Syntax Error in 'capture' - Valid syntax: capture [var]" capture: "Syntax Error in 'capture' - Valid syntax: capture [var]"
case: "Syntax Error in 'case' - Valid syntax: case [condition]" case: "Syntax Error in 'case' - Valid syntax: case [condition]"
@@ -13,10 +14,13 @@
if: "Syntax Error in tag 'if' - Valid syntax: if [expression]" if: "Syntax Error in tag 'if' - Valid syntax: if [expression]"
include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]" include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]"
unknown_tag: "Unknown tag '%{tag}'" unknown_tag: "Unknown tag '%{tag}'"
invalid_delimiter: "'end' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}" invalid_delimiter: "'%{tag}' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}"
unexpected_else: "%{block_name} tag does not expect else tag" unexpected_else: "%{block_name} tag does not expect 'else' tag"
unexpected_outer_tag: "Unexpected outer '%{tag}' tag"
tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}" tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"
variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}" variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}"
tag_never_closed: "'%{block_name}' tag was never closed" tag_never_closed: "'%{block_name}' tag was never closed"
meta_syntax_error: "Liquid syntax error: #{e.message}" meta_syntax_error: "Liquid syntax error: #{e.message}"
table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3" table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
argument:
include: "Argument error in tag 'include' - Illegal template name"

View File

@@ -1,62 +0,0 @@
# Copyright 2007 by Domizio Demichelis
# This library is free software. It may be used, redistributed and/or modified
# under the same terms as Ruby itself
#
# This extension is used in order to expose the object of the implementing class
# to liquid as it were a Drop. It also limits the liquid-callable methods of the instance
# to the allowed method passed with the liquid_methods call
# Example:
#
# class SomeClass
# liquid_methods :an_allowed_method
#
# def an_allowed_method
# 'this comes from an allowed method'
# end
# def unallowed_method
# 'this will never be an output'
# end
# end
#
# if you want to extend the drop to other methods you can defines more methods
# in the class <YourClass>::LiquidDropClass
#
# class SomeClass::LiquidDropClass
# def another_allowed_method
# 'and this from another allowed method'
# end
# end
# end
#
# usage:
# @something = SomeClass.new
#
# template:
# {{something.an_allowed_method}}{{something.unallowed_method}} {{something.another_allowed_method}}
#
# output:
# 'this comes from an allowed method and this from another allowed method'
#
# You can also chain associations, by adding the liquid_method call in the
# association models.
#
class Module
def liquid_methods(*allowed_methods)
drop_class = eval "class #{self.to_s}::LiquidDropClass < Liquid::Drop; self; end"
define_method :to_liquid do
drop_class.new(self)
end
drop_class.class_eval do
def initialize(object)
@object = object
end
allowed_methods.each do |sym|
define_method sym do
@object.send sym
end
end
end
end
end

View File

@@ -0,0 +1,38 @@
module Liquid
class ParseContext
attr_accessor :locale, :line_number, :trim_whitespace, :depth
attr_reader :partial, :warnings, :error_mode
def initialize(options = {})
@template_options = options ? options.dup : {}
@locale = @template_options[:locale] ||= I18n.new
@warnings = []
self.depth = 0
self.partial = false
end
def [](option_key)
@options[option_key]
end
def partial=(value)
@partial = value
@options = value ? partial_options : @template_options
@error_mode = @options[:error_mode] || Template.error_mode
value
end
def partial_options
@partial_options ||= begin
dont_pass = @template_options[:include_options_blacklist]
if dont_pass == true
{ locale: locale }
elsif dont_pass.is_a?(Array)
@template_options.reject { |k, v| dont_pass.include?(k) }
else
@template_options
end
end
end
end
end

View File

@@ -0,0 +1,42 @@
# frozen_string_literal: true
module Liquid
class ParseTreeVisitor
def self.for(node, callbacks = Hash.new(proc {}))
if defined?(node.class::ParseTreeVisitor)
node.class::ParseTreeVisitor
else
self
end.new(node, callbacks)
end
def initialize(node, callbacks)
@node = node
@callbacks = callbacks
end
def add_callback_for(*classes, &block)
callback = block
callback = ->(node, _) { yield node } if block.arity.abs == 1
callback = ->(_, _) { yield } if block.arity.zero?
classes.each { |klass| @callbacks[klass] = callback }
self
end
def visit(context = nil)
children.map do |node|
item, new_context = @callbacks[node.class].call(node, context)
[
item,
ParseTreeVisitor.for(node, @callbacks).visit(new_context || context)
]
end
end
protected
def children
@node.respond_to?(:nodelist) ? Array(@node.nodelist) : []
end
end
end

View File

@@ -75,7 +75,7 @@ module Liquid
def variable_signature def variable_signature
str = consume(:id) str = consume(:id)
if look(:open_square) while look(:open_square)
str << consume str << consume
str << expression str << expression
str << consume(:close_square) str << consume(:close_square)

View File

@@ -1,25 +1,25 @@
module Liquid module Liquid
module ParserSwitching module ParserSwitching
def parse_with_selected_parser(markup) def parse_with_selected_parser(markup)
case @options[:error_mode] || Template.error_mode case parse_context.error_mode
when :strict then strict_parse_with_error_context(markup) when :strict then strict_parse_with_error_context(markup)
when :lax then lax_parse(markup) when :lax then lax_parse(markup)
when :warn when :warn
begin begin
return strict_parse_with_error_context(markup) return strict_parse_with_error_context(markup)
rescue SyntaxError => e rescue SyntaxError => e
e.set_line_number_from_token(markup) parse_context.warnings << e
@warnings ||= []
@warnings << e
return lax_parse(markup) return lax_parse(markup)
end end
end end
end end
private private
def strict_parse_with_error_context(markup) def strict_parse_with_error_context(markup)
strict_parse(markup) strict_parse(markup)
rescue SyntaxError => e rescue SyntaxError => e
e.line_number = line_number
e.markup_context = markup_context(markup) e.markup_context = markup_context(markup)
raise e raise e
end end

View File

@@ -1,9 +1,11 @@
module Liquid require 'liquid/profiler/hooks'
module Liquid
# Profiler enables support for profiling template rendering to help track down performance issues. # Profiler enables support for profiling template rendering to help track down performance issues.
# #
# To enable profiling, pass the <tt>profile: true</tt> option to <tt>Liquid::Template.parse</tt>. Then, after # To enable profiling, first require 'liquid/profiler'.
# <tt>Liquid::Template#render</tt> is called, the template object makes available an instance of this # Then, to profile a parse/render cycle, pass the <tt>profile: true</tt> option to <tt>Liquid::Template.parse</tt>.
# After <tt>Liquid::Template#render</tt> is called, the template object makes available an instance of this
# class via the <tt>Liquid::Template#profiler</tt> method. # class via the <tt>Liquid::Template#profiler</tt> method.
# #
# template = Liquid::Template.parse(template_content, profile: true) # template = Liquid::Template.parse(template_content, profile: true)
@@ -17,7 +19,7 @@ module Liquid
# inside of <tt>{% include %}</tt> tags. # inside of <tt>{% include %}</tt> tags.
# #
# profile.each do |node| # profile.each do |node|
# # Access to the token itself # # Access to the node itself
# node.code # node.code
# #
# # Which template and line number of this node. # # Which template and line number of this node.
@@ -44,17 +46,15 @@ module Liquid
class Timing class Timing
attr_reader :code, :partial, :line_number, :children attr_reader :code, :partial, :line_number, :children
def initialize(token, partial) def initialize(node, partial)
@code = token.respond_to?(:raw) ? token.raw : token @code = node.respond_to?(:raw) ? node.raw : node
@partial = partial @partial = partial
@line_number = token.respond_to?(:line_number) ? token.line_number : nil @line_number = node.respond_to?(:line_number) ? node.line_number : nil
@children = [] @children = []
end end
def self.start(token, partial) def self.start(node, partial)
new(token, partial).tap do |t| new(node, partial).tap(&:start)
t.start
end
end end
def start def start
@@ -70,11 +70,11 @@ module Liquid
end end
end end
def self.profile_token_render(token) def self.profile_node_render(node)
if Profiler.current_profile && token.respond_to?(:render) if Profiler.current_profile && node.respond_to?(:render)
Profiler.current_profile.start_token(token) Profiler.current_profile.start_node(node)
output = yield output = yield
Profiler.current_profile.end_token(token) Profiler.current_profile.end_node(node)
output output
else else
yield yield
@@ -132,11 +132,11 @@ module Liquid
@root_timing.children.length @root_timing.children.length
end end
def start_token(token) def start_node(node)
@timing_stack.push(Timing.start(token, current_partial)) @timing_stack.push(Timing.start(node, current_partial))
end end
def end_token(token) def end_node(_node)
timing = @timing_stack.pop timing = @timing_stack.pop
timing.finish timing.finish
@@ -154,6 +154,5 @@ module Liquid
def pop_partial def pop_partial
@partial_stack.pop @partial_stack.pop
end end
end end
end end

View File

@@ -1,18 +1,18 @@
module Liquid module Liquid
class Block < Tag class BlockBody
def render_token_with_profiling(token, context) def render_node_with_profiling(node, output, context, skip_output = false)
Profiler.profile_token_render(token) do Profiler.profile_node_render(node) do
render_token_without_profiling(token, context) render_node_without_profiling(node, output, context, skip_output)
end end
end end
alias_method :render_token_without_profiling, :render_token alias_method :render_node_without_profiling, :render_node_to_output
alias_method :render_token, :render_token_with_profiling alias_method :render_node_to_output, :render_node_with_profiling
end end
class Include < Tag class Include < Tag
def render_with_profiling(context) def render_with_profiling(context)
Profiler.profile_children(@template_name) do Profiler.profile_children(context.evaluate(@template_name_expr).to_s) do
render_without_profiling(context) render_without_profiling(context)
end end
end end

View File

@@ -6,7 +6,7 @@ module Liquid
if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate) if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate)
new(start_obj, end_obj) new(start_obj, end_obj)
else else
start_obj.to_i..end_obj.to_i ReversableRange.new(start_obj.to_i, end_obj.to_i)
end end
end end
@@ -16,7 +16,22 @@ module Liquid
end end
def evaluate(context) def evaluate(context)
context.evaluate(@start_obj).to_i..context.evaluate(@end_obj).to_i start_int = to_integer(context.evaluate(@start_obj))
end_int = to_integer(context.evaluate(@end_obj))
ReversableRange.new(start_int, end_int)
end
private
def to_integer(input)
case input
when Integer
input
when NilClass, String
input.to_i
else
Utils.to_integer(input)
end
end end
end end
end end

View File

@@ -0,0 +1,23 @@
module Liquid
class ResourceLimits
attr_accessor :render_length, :render_score, :assign_score,
:render_length_limit, :render_score_limit, :assign_score_limit
def initialize(limits)
@render_length_limit = limits[:render_length_limit]
@render_score_limit = limits[:render_score_limit]
@assign_score_limit = limits[:assign_score_limit]
reset
end
def reached?
(@render_length_limit && @render_length > @render_length_limit) ||
(@render_score_limit && @render_score > @render_score_limit) ||
(@assign_score_limit && @assign_score > @assign_score_limit)
end
def reset
@render_length = @render_score = @assign_score = 0
end
end
end

View File

@@ -0,0 +1,77 @@
module Liquid
class ReversableRange
include Enumerable
def initialize(min, max)
@min = min
@max = max
@reversed = false
end
def each
if reversed
index = max
while index >= min
yield index
index -= 1
end
else
index = min
while index <= max
yield index
index += 1
end
end
end
def reverse!
@reversed = !reversed
self
end
def empty?
max < min
end
def size
difference = max - min
if difference > 0
difference + 1
else
0
end
end
def load_slice(from, to = nil)
to ||= max
slice_max = [max, to].min
slice_min = [min, from].max
range = ReversableRange.new(slice_min, slice_max)
range.reverse! if reversed
range
end
def ==(other)
other.is_a?(self.class) &&
other.min == min &&
other.max == max &&
other.reversed == reversed
end
def to_s
if reversed
"#{max}..#{min}"
else
"#{min}..#{max}"
end
end
def to_liquid
self
end
protected
attr_reader :min, :max, :reversed
end
end

View File

@@ -2,7 +2,6 @@ require 'cgi'
require 'bigdecimal' require 'bigdecimal'
module Liquid module Liquid
module StandardFilters module StandardFilters
HTML_ESCAPE = { HTML_ESCAPE = {
'&'.freeze => '&amp;'.freeze, '&'.freeze => '&amp;'.freeze,
@@ -10,8 +9,14 @@ module Liquid
'<'.freeze => '&lt;'.freeze, '<'.freeze => '&lt;'.freeze,
'"'.freeze => '&quot;'.freeze, '"'.freeze => '&quot;'.freeze,
"'".freeze => '&#39;'.freeze "'".freeze => '&#39;'.freeze
} }.freeze
HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/ HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/
STRIP_HTML_BLOCKS = Regexp.union(
/<script.*?<\/script>/m,
/<!--.*?-->/m,
/<style.*?<\/style>/m
)
STRIP_HTML_TAGS = /<.*?>/m
# Return the size of an array or of an string # Return the size of an array or of an string
def size(input) def size(input)
@@ -34,7 +39,7 @@ module Liquid
end end
def escape(input) def escape(input)
CGI.escapeHTML(input) rescue input CGI.escapeHTML(input.to_s).untaint unless input.nil?
end end
alias_method :h, :escape alias_method :h, :escape
@@ -43,12 +48,21 @@ module Liquid
end end
def url_encode(input) def url_encode(input)
CGI.escape(input) rescue input CGI.escape(input.to_s) unless input.nil?
end end
def slice(input, offset, length=nil) def url_decode(input)
offset = Integer(offset) return if input.nil?
length = length ? Integer(length) : 1
result = CGI.unescape(input.to_s)
raise Liquid::ArgumentError, "invalid byte sequence in #{result.encoding}" unless result.valid_encoding?
result
end
def slice(input, offset, length = nil)
offset = Utils.to_integer(offset)
length = length ? Utils.to_integer(length) : 1
if input.is_a?(Array) if input.is_a?(Array)
input.slice(offset, length) || [] input.slice(offset, length) || []
@@ -59,18 +73,22 @@ module Liquid
# Truncate a string down to x characters # Truncate a string down to x characters
def truncate(input, length = 50, truncate_string = "...".freeze) def truncate(input, length = 50, truncate_string = "...".freeze)
if input.nil? then return end return if input.nil?
l = length.to_i - truncate_string.length input_str = input.to_s
length = Utils.to_integer(length)
truncate_string_str = truncate_string.to_s
l = length - truncate_string_str.length
l = 0 if l < 0 l = 0 if l < 0
input.length > length.to_i ? input[0...l] + truncate_string : input input_str.length > length ? input_str[0...l] + truncate_string_str : input_str
end end
def truncatewords(input, words = 15, truncate_string = "...".freeze) def truncatewords(input, words = 15, truncate_string = "...".freeze)
if input.nil? then return end return if input.nil?
wordlist = input.to_s.split wordlist = input.to_s.split
l = words.to_i - 1 words = Utils.to_integer(words)
l = words - 1
l = 0 if l < 0 l = 0 if l < 0
wordlist.length > l ? wordlist[0..l].join(" ".freeze) + truncate_string : input wordlist.length > l ? wordlist[0..l].join(" ".freeze) + truncate_string.to_s : input
end end
# Split input string into an array of substrings separated by given pattern. # Split input string into an array of substrings separated by given pattern.
@@ -79,7 +97,7 @@ module Liquid
# <div class="summary">{{ post | split '//' | first }}</div> # <div class="summary">{{ post | split '//' | first }}</div>
# #
def split(input, pattern) def split(input, pattern)
input.to_s.split(pattern) input.to_s.split(pattern.to_s)
end end
def strip(input) def strip(input)
@@ -96,7 +114,9 @@ module Liquid
def strip_html(input) def strip_html(input)
empty = ''.freeze empty = ''.freeze
input.to_s.gsub(/<script.*?<\/script>/m, empty).gsub(/<!--.*?-->/m, empty).gsub(/<style.*?<\/style>/m, empty).gsub(/<.*?>/m, empty) result = input.to_s.gsub(STRIP_HTML_BLOCKS, empty)
result.gsub!(STRIP_HTML_TAGS, empty)
result
end end
# Remove all newlines from the string # Remove all newlines from the string
@@ -113,12 +133,61 @@ module Liquid
# provide optional property with which to sort an array of hashes or drops # provide optional property with which to sort an array of hashes or drops
def sort(input, property = nil) def sort(input, property = nil)
ary = InputIterator.new(input) ary = InputIterator.new(input)
return [] if ary.empty?
if property.nil? if property.nil?
ary.sort ary.sort do |a, b|
elsif ary.first.respond_to?(:[]) && !ary.first[property].nil? nil_safe_compare(a, b)
ary.sort {|a,b| a[property] <=> b[property] } end
elsif ary.first.respond_to?(property) elsif ary.all? { |el| el.respond_to?(:[]) }
ary.sort {|a,b| a.send(property) <=> b.send(property) } begin
ary.sort { |a, b| nil_safe_compare(a[property], b[property]) }
rescue TypeError
raise_property_error(property)
end
end
end
# Sort elements of an array ignoring case if strings
# provide optional property with which to sort an array of hashes or drops
def sort_natural(input, property = nil)
ary = InputIterator.new(input)
return [] if ary.empty?
if property.nil?
ary.sort do |a, b|
nil_safe_casecmp(a, b)
end
elsif ary.all? { |el| el.respond_to?(:[]) }
begin
ary.sort { |a, b| nil_safe_casecmp(a[property], b[property]) }
rescue TypeError
raise_property_error(property)
end
end
end
# Filter the elements of an array to those with a certain property value.
# By default the target is any truthy value.
def where(input, property, target_value = nil)
ary = InputIterator.new(input)
if ary.empty?
[]
elsif ary.first.respond_to?(:[]) && target_value.nil?
begin
ary.select { |item| item[property] }
rescue TypeError
raise_property_error(property)
end
elsif ary.first.respond_to?(:[])
begin
ary.select { |item| item[property] == target_value }
rescue TypeError
raise_property_error(property)
end
end end
end end
@@ -126,10 +195,17 @@ module Liquid
# provide optional property with which to determine uniqueness # provide optional property with which to determine uniqueness
def uniq(input, property = nil) def uniq(input, property = nil)
ary = InputIterator.new(input) ary = InputIterator.new(input)
if property.nil? if property.nil?
input.uniq ary.uniq
elsif input.first.respond_to?(:[]) elsif ary.empty? # The next two cases assume a non-empty array.
input.uniq{ |a| a[property] } []
elsif ary.first.respond_to?(:[])
begin
ary.uniq { |a| a[property] }
rescue TypeError
raise_property_error(property)
end
end end
end end
@@ -147,29 +223,50 @@ module Liquid
if property == "to_liquid".freeze if property == "to_liquid".freeze
e e
elsif e.respond_to?(:[]) elsif e.respond_to?(:[])
e[property] r = e[property]
r.is_a?(Proc) ? r.call : r
end
end
rescue TypeError
raise_property_error(property)
end
# Remove nils within an array
# provide optional property with which to check for nil
def compact(input, property = nil)
ary = InputIterator.new(input)
if property.nil?
ary.compact
elsif ary.empty? # The next two cases assume a non-empty array.
[]
elsif ary.first.respond_to?(:[])
begin
ary.reject { |a| a[property].nil? }
rescue TypeError
raise_property_error(property)
end end
end end
end end
# Replace occurrences of a string with another # Replace occurrences of a string with another
def replace(input, string, replacement = ''.freeze) def replace(input, string, replacement = ''.freeze)
input.to_s.gsub(string, replacement.to_s) input.to_s.gsub(string.to_s, replacement.to_s)
end end
# Replace the first occurrences of a string with another # Replace the first occurrences of a string with another
def replace_first(input, string, replacement = ''.freeze) def replace_first(input, string, replacement = ''.freeze)
input.to_s.sub(string, replacement.to_s) input.to_s.sub(string.to_s, replacement.to_s)
end end
# remove a substring # remove a substring
def remove(input, string) def remove(input, string)
input.to_s.gsub(string, ''.freeze) input.to_s.gsub(string.to_s, ''.freeze)
end end
# remove the first occurrences of a substring # remove the first occurrences of a substring
def remove_first(input, string) def remove_first(input, string)
input.to_s.sub(string, ''.freeze) input.to_s.sub(string.to_s, ''.freeze)
end end
# add one string to another # add one string to another
@@ -177,6 +274,13 @@ module Liquid
input.to_s + string.to_s input.to_s + string.to_s
end end
def concat(input, array)
unless array.respond_to?(:to_ary)
raise ArgumentError.new("concat filter requires an array argument")
end
InputIterator.new(input).concat(array)
end
# prepend a string to another # prepend a string to another
def prepend(input, string) def prepend(input, string)
string.to_s + input.to_s string.to_s + input.to_s
@@ -221,7 +325,7 @@ module Liquid
def date(input, format) def date(input, format)
return input if format.to_s.empty? return input if format.to_s.empty?
return input unless date = to_date(input) return input unless date = Utils.to_date(input)
date.strftime(format.to_s) date.strftime(format.to_s)
end end
@@ -244,6 +348,12 @@ module Liquid
array.last if array.respond_to?(:last) array.last if array.respond_to?(:last)
end end
# absolute value
def abs(input)
result = Utils.to_number(input).abs
result.is_a?(BigDecimal) ? result.to_f : result
end
# addition # addition
def plus(input, operand) def plus(input, operand)
apply_operation(input, operand, :+) apply_operation(input, operand, :+)
@@ -262,69 +372,88 @@ module Liquid
# division # division
def divided_by(input, operand) def divided_by(input, operand)
apply_operation(input, operand, :/) apply_operation(input, operand, :/)
rescue ::ZeroDivisionError => e
raise Liquid::ZeroDivisionError, e.message
end end
def modulo(input, operand) def modulo(input, operand)
apply_operation(input, operand, :%) apply_operation(input, operand, :%)
rescue ::ZeroDivisionError => e
raise Liquid::ZeroDivisionError, e.message
end end
def round(input, n = 0) def round(input, n = 0)
result = to_number(input).round(to_number(n)) result = Utils.to_number(input).round(Utils.to_number(n))
result = result.to_f if result.is_a?(BigDecimal) result = result.to_f if result.is_a?(BigDecimal)
result = result.to_i if n == 0 result = result.to_i if n == 0
result result
rescue ::FloatDomainError => e
raise Liquid::FloatDomainError, e.message
end end
def ceil(input) def ceil(input)
to_number(input).ceil.to_i Utils.to_number(input).ceil.to_i
rescue ::FloatDomainError => e
raise Liquid::FloatDomainError, e.message
end end
def floor(input) def floor(input)
to_number(input).floor.to_i Utils.to_number(input).floor.to_i
rescue ::FloatDomainError => e
raise Liquid::FloatDomainError, e.message
end end
def default(input, default_value = "".freeze) def at_least(input, n)
is_blank = input.respond_to?(:empty?) ? input.empty? : !input min_value = Utils.to_number(n)
is_blank ? default_value : input
result = Utils.to_number(input)
result = min_value if min_value > result
result.is_a?(BigDecimal) ? result.to_f : result
end
def at_most(input, n)
max_value = Utils.to_number(n)
result = Utils.to_number(input)
result = max_value if max_value < result
result.is_a?(BigDecimal) ? result.to_f : result
end
def default(input, default_value = ''.freeze)
if !input || input.respond_to?(:empty?) && input.empty?
default_value
else
input
end
end end
private private
def to_number(obj) def raise_property_error(property)
case obj raise Liquid::ArgumentError.new("cannot select the property '#{property}'")
when Float
BigDecimal.new(obj.to_s)
when Numeric
obj
when String
(obj.strip =~ /\A\d+\.\d+\z/) ? BigDecimal.new(obj) : obj.to_i
else
0
end
end
def to_date(obj)
return obj if obj.respond_to?(:strftime)
case obj
when 'now'.freeze, 'today'.freeze
Time.now
when /\A\d+\z/, Integer
Time.at(obj.to_i)
when String
Time.parse(obj)
else
nil
end
rescue ArgumentError
nil
end end
def apply_operation(input, operand, operation) def apply_operation(input, operand, operation)
result = to_number(input).send(operation, to_number(operand)) result = Utils.to_number(input).send(operation, Utils.to_number(operand))
result.is_a?(BigDecimal) ? result.to_f : result result.is_a?(BigDecimal) ? result.to_f : result
end end
def nil_safe_compare(a, b)
if !a.nil? && !b.nil?
a <=> b
else
a.nil? ? 1 : -1
end
end
def nil_safe_casecmp(a, b)
if !a.nil? && !b.nil?
a.to_s.casecmp(b.to_s)
else
a.nil? ? 1 : -1
end
end
class InputIterator class InputIterator
include Enumerable include Enumerable
@@ -341,13 +470,30 @@ module Liquid
end end
def join(glue) def join(glue)
to_a.join(glue) to_a.join(glue.to_s)
end
def concat(args)
to_a.concat(args)
end end
def reverse def reverse
reverse_each.to_a reverse_each.to_a
end end
def uniq(&block)
to_a.uniq(&block)
end
def compact
to_a.compact
end
def empty?
@input.each { return false }
true
end
def each def each
@input.each do |e| @input.each do |e|
yield(e.respond_to?(:to_liquid) ? e.to_liquid : e) yield(e.respond_to?(:to_liquid) ? e.to_liquid : e)

View File

@@ -1,19 +1,19 @@
require 'set' require 'set'
module Liquid module Liquid
# Strainer is the parent class for the filters system. # Strainer is the parent class for the filters system.
# New filters are mixed into the strainer class which is then instantiated for each liquid template render run. # New filters are mixed into the strainer class which is then instantiated for each liquid template render run.
# #
# The Strainer only allows method calls defined in filters given to it via Strainer.global_filter, # The Strainer only allows method calls defined in filters given to it via Strainer.global_filter,
# Context#add_filters or Template.register_filter # Context#add_filters or Template.register_filter
class Strainer #:nodoc: class Strainer #:nodoc:
@@filters = [] @@global_strainer = Class.new(Strainer) do
@@known_filters = Set.new @filter_methods = Set.new
@@known_methods = Set.new end
@@strainer_class_cache = Hash.new do |hash, filters| @@strainer_class_cache = Hash.new do |hash, filters|
hash[filters] = Class.new(Strainer) do hash[filters] = Class.new(@@global_strainer) do
filters.each { |f| include f } @filter_methods = @@global_strainer.filter_methods.dup
filters.each { |f| add_filter(f) }
end end
end end
@@ -21,43 +21,46 @@ module Liquid
@context = context @context = context
end end
def self.global_filter(filter) class << self
raise ArgumentError, "Passed filter is not a module" unless filter.is_a?(Module) attr_reader :filter_methods
add_known_filter(filter)
@@filters << filter unless @@filters.include?(filter)
end end
def self.add_known_filter(filter) def self.add_filter(filter)
unless @@known_filters.include?(filter) raise ArgumentError, "Expected module but got: #{filter.class}" unless filter.is_a?(Module)
@@method_blacklist ||= Set.new(Strainer.instance_methods.map(&:to_s)) unless self.include?(filter)
new_methods = filter.instance_methods.map(&:to_s) invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) }
new_methods.reject!{ |m| @@method_blacklist.include?(m) } if invokable_non_public_methods.any?
@@known_methods.merge(new_methods) raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}"
@@known_filters.add(filter) else
send(:include, filter)
@filter_methods.merge(filter.public_instance_methods.map(&:to_s))
end
end end
end end
def self.strainer_class_cache def self.global_filter(filter)
@@strainer_class_cache @@strainer_class_cache.clear
@@global_strainer.add_filter(filter)
end
def self.invokable?(method)
@filter_methods.include?(method.to_s)
end end
def self.create(context, filters = []) def self.create(context, filters = [])
filters = @@filters + filters @@strainer_class_cache[filters].new(context)
strainer_class_cache[filters].new(context)
end end
def invoke(method, *args) def invoke(method, *args)
if invokable?(method) if self.class.invokable?(method)
send(method, *args) send(method, *args)
elsif @context && @context.strict_filters
raise Liquid::UndefinedFilter, "undefined filter #{method}"
else else
args.first args.first
end end
rescue ::ArgumentError => e rescue ::ArgumentError => e
raise Liquid::ArgumentError.new(e.message) raise Liquid::ArgumentError, e.message, e.backtrace
end
def invokable?(method)
@@known_methods.include?(method.to_s) && respond_to?(method)
end end
end end
end end

View File

@@ -0,0 +1,62 @@
module Liquid
class TablerowloopDrop < Drop
def initialize(length, cols)
@length = length
@row = 1
@col = 1
@cols = cols
@index = 0
end
attr_reader :length, :col, :row
def index
@index + 1
end
def index0
@index
end
def col0
@col - 1
end
def rindex
@length - @index
end
def rindex0
@length - @index - 1
end
def first
@index == 0
end
def last
@index == @length - 1
end
def col_first
@col == 1
end
def col_last
@col == @cols
end
protected
def increment!
@index += 1
if @col == @cols
@col = 1
@row += 1
else
@col += 1
end
end
end
end

View File

@@ -1,26 +1,27 @@
module Liquid module Liquid
class Tag class Tag
attr_accessor :options, :line_number attr_reader :nodelist, :tag_name, :line_number, :parse_context
attr_reader :nodelist, :warnings alias_method :options, :parse_context
include ParserSwitching include ParserSwitching
class << self class << self
def parse(tag_name, markup, tokens, options) def parse(tag_name, markup, tokenizer, options)
tag = new(tag_name, markup, options) tag = new(tag_name, markup, options)
tag.parse(tokens) tag.parse(tokenizer)
tag tag
end end
private :new private :new
end end
def initialize(tag_name, markup, options) def initialize(tag_name, markup, parse_context)
@tag_name = tag_name @tag_name = tag_name
@markup = markup @markup = markup
@options = options @parse_context = parse_context
@line_number = parse_context.line_number
end end
def parse(tokens) def parse(_tokens)
end end
def raw def raw
@@ -31,7 +32,7 @@ module Liquid
self.class.name.downcase self.class.name.downcase
end end
def render(context) def render(_context)
''.freeze ''.freeze
end end

View File

@@ -1,5 +1,4 @@
module Liquid module Liquid
# Assign sets a variable in your template. # Assign sets a variable in your template.
# #
# {% assign foo = 'monkey' %} # {% assign foo = 'monkey' %}
@@ -11,12 +10,13 @@ module Liquid
class Assign < Tag class Assign < Tag
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
attr_reader :to, :from
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
if markup =~ Syntax if markup =~ Syntax
@to = $1 @to = $1
@from = Variable.new($2,options) @from = Variable.new($2, options)
@from.line_number = line_number
else else
raise SyntaxError.new options[:locale].t("errors.syntax.assign".freeze) raise SyntaxError.new options[:locale].t("errors.syntax.assign".freeze)
end end
@@ -25,13 +25,34 @@ module Liquid
def render(context) def render(context)
val = @from.render(context) val = @from.render(context)
context.scopes.last[@to] = val context.scopes.last[@to] = val
context.increment_used_resources(:assign_score_current, val) context.resource_limits.assign_score += assign_score_of(val)
''.freeze ''.freeze
end end
def blank? def blank?
true true
end end
private
def assign_score_of(val)
if val.instance_of?(String)
val.bytesize
elsif val.instance_of?(Array) || val.instance_of?(Hash)
sum = 1
# Uses #each to avoid extra allocations.
val.each { |child| sum += assign_score_of(child) }
sum
else
1
end
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[@node.from]
end
end
end end
Template.register_tag('assign'.freeze, Assign) Template.register_tag('assign'.freeze, Assign)

View File

@@ -1,5 +1,4 @@
module Liquid module Liquid
# Break tag to be used to break out of a for loop. # Break tag to be used to break out of a for loop.
# #
# == Basic Usage: # == Basic Usage:
@@ -10,11 +9,9 @@ module Liquid
# {% endfor %} # {% endfor %}
# #
class Break < Tag class Break < Tag
def interrupt def interrupt
BreakInterrupt.new BreakInterrupt.new
end end
end end
Template.register_tag('break'.freeze, Break) Template.register_tag('break'.freeze, Break)

View File

@@ -11,7 +11,7 @@ module Liquid
# in a sidebar or footer. # in a sidebar or footer.
# #
class Capture < Block class Capture < Block
Syntax = /(\w+)/ Syntax = /(#{VariableSignature}+)/o
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
@@ -25,7 +25,7 @@ module Liquid
def render(context) def render(context)
output = super output = super
context.scopes.last[@to] = output context.scopes.last[@to] = output
context.increment_used_resources(:assign_score_current, output) context.resource_limits.assign_score += output.bytesize
''.freeze ''.freeze
end end

View File

@@ -3,23 +3,31 @@ module Liquid
Syntax = /(#{QuotedFragment})/o Syntax = /(#{QuotedFragment})/o
WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om
attr_reader :blocks, :left
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
@blocks = [] @blocks = []
if markup =~ Syntax if markup =~ Syntax
@left = $1 @left = Expression.parse($1)
else else
raise SyntaxError.new(options[:locale].t("errors.syntax.case".freeze)) raise SyntaxError.new(options[:locale].t("errors.syntax.case".freeze))
end end
end end
def parse(tokens)
body = BlockBody.new
while parse_body(body, tokens)
body = @blocks.last.attachment
end
end
def nodelist def nodelist
@blocks.map(&:attachment).flatten @blocks.map(&:attachment)
end end
def unknown_tag(tag, markup, tokens) def unknown_tag(tag, markup, tokens)
@nodelist = []
case tag case tag
when 'when'.freeze when 'when'.freeze
record_when_condition(markup) record_when_condition(markup)
@@ -37,10 +45,10 @@ module Liquid
output = '' output = ''
@blocks.each do |block| @blocks.each do |block|
if block.else? if block.else?
return render_all(block.attachment, context) if execute_else_block return block.attachment.render(context) if execute_else_block
elsif block.evaluate(context) elsif block.evaluate(context)
execute_else_block = false execute_else_block = false
output << render_all(block.attachment, context) output << block.attachment.render(context)
end end
end end
output output
@@ -50,29 +58,36 @@ module Liquid
private private
def record_when_condition(markup) def record_when_condition(markup)
body = BlockBody.new
while markup while markup
# Create a new nodelist and assign it to the new block unless markup =~ WhenSyntax
if not markup =~ WhenSyntax
raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_when".freeze)) raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_when".freeze))
end end
markup = $2 markup = $2
block = Condition.new(@left, '=='.freeze, $1) block = Condition.new(@left, '=='.freeze, Expression.parse($1))
block.attach(@nodelist) block.attach(body)
@blocks.push(block) @blocks << block
end end
end end
def record_else_condition(markup) def record_else_condition(markup)
if not markup.strip.empty? unless markup.strip.empty?
raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_else".freeze)) raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_else".freeze))
end end
block = ElseCondition.new block = ElseCondition.new
block.attach(@nodelist) block.attach(BlockBody.new)
@blocks << block @blocks << block
end end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[@node.left] + @node.blocks
end
end
end end
Template.register_tag('case'.freeze, Case) Template.register_tag('case'.freeze, Case)

View File

@@ -1,10 +1,10 @@
module Liquid module Liquid
class Comment < Block class Comment < Block
def render(context) def render(_context)
''.freeze ''.freeze
end end
def unknown_tag(tag, markup, tokens) def unknown_tag(_tag, _markup, _tokens)
end end
def blank? def blank?

View File

@@ -15,29 +15,31 @@ module Liquid
SimpleSyntax = /\A#{QuotedFragment}+/o SimpleSyntax = /\A#{QuotedFragment}+/o
NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om
attr_reader :variables
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
case markup case markup
when NamedSyntax when NamedSyntax
@variables = variables_from_string($2) @variables = variables_from_string($2)
@name = $1 @name = Expression.parse($1)
when SimpleSyntax when SimpleSyntax
@variables = variables_from_string(markup) @variables = variables_from_string(markup)
@name = "'#{@variables.to_s}'" @name = @variables.to_s
else else
raise SyntaxError.new(options[:locale].t("errors.syntax.cycle".freeze)) raise SyntaxError.new(options[:locale].t("errors.syntax.cycle".freeze))
end end
end end
def render(context) def render(context)
context.registers[:cycle] ||= Hash.new(0) context.registers[:cycle] ||= {}
context.stack do context.stack do
key = context[@name] key = context.evaluate(@name)
iteration = context.registers[:cycle][key] iteration = context.registers[:cycle][key].to_i
result = context[@variables[iteration]] result = context.evaluate(@variables[iteration])
iteration += 1 iteration += 1
iteration = 0 if iteration >= @variables.size iteration = 0 if iteration >= @variables.size
context.registers[:cycle][key] = iteration context.registers[:cycle][key] = iteration
result result
end end
@@ -48,9 +50,15 @@ module Liquid
def variables_from_string(markup) def variables_from_string(markup)
markup.split(',').collect do |var| markup.split(',').collect do |var|
var =~ /\s*(#{QuotedFragment})\s*/o var =~ /\s*(#{QuotedFragment})\s*/o
$1 ? $1 : nil $1 ? Expression.parse($1) : nil
end.compact end.compact
end end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
Array(@node.variables)
end
end
end end
Template.register_tag('cycle', Cycle) Template.register_tag('cycle', Cycle)

View File

@@ -1,5 +1,4 @@
module Liquid module Liquid
# decrement is used in a place where one needs to insert a counter # decrement is used in a place where one needs to insert a counter
# into a template, and needs the counter to survive across # into a template, and needs the counter to survive across
# multiple instantiations of the template. # multiple instantiations of the template.
@@ -26,12 +25,10 @@ module Liquid
def render(context) def render(context)
value = context.environments.first[@variable] ||= 0 value = context.environments.first[@variable] ||= 0
value = value - 1 value -= 1
context.environments.first[@variable] = value context.environments.first[@variable] = value
value.to_s value.to_s
end end
private
end end
Template.register_tag('decrement'.freeze, Decrement) Template.register_tag('decrement'.freeze, Decrement)

View File

@@ -1,5 +1,4 @@
module Liquid module Liquid
# "For" iterates over an array or collection. # "For" iterates over an array or collection.
# Several useful variables are available to you within the loop. # Several useful variables are available to you within the loop.
# #
@@ -24,7 +23,7 @@ module Liquid
# {{ item.name }} # {{ item.name }}
# {% end %} # {% end %}
# #
# To reverse the for loop simply use {% for item in collection reversed %} # To reverse the for loop simply use {% for item in collection reversed %} (note that the flag's spelling is different to the filter `reverse`)
# #
# == Available variables: # == Available variables:
# #
@@ -42,85 +41,43 @@ module Liquid
# where 0 is the last item. # where 0 is the last item.
# forloop.first:: Returns true if the item is the first item. # forloop.first:: Returns true if the item is the first item.
# forloop.last:: Returns true if the item is the last item. # forloop.last:: Returns true if the item is the last item.
# forloop.parentloop:: Provides access to the parent loop, if present.
# #
class For < Block class For < Block
Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
attr_reader :collection_name, :variable_name, :limit, :from
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
@from = @limit = nil
parse_with_selected_parser(markup) parse_with_selected_parser(markup)
@nodelist = @for_block = [] @for_block = BlockBody.new
@else_block = nil
end
def parse(tokens)
return unless parse_body(@for_block, tokens)
parse_body(@else_block, tokens)
end end
def nodelist def nodelist
if @else_block @else_block ? [@for_block, @else_block] : [@for_block]
@for_block + @else_block
else
@for_block
end
end end
def unknown_tag(tag, markup, tokens) def unknown_tag(tag, markup, tokens)
return super unless tag == 'else'.freeze return super unless tag == 'else'.freeze
@nodelist = @else_block = [] @else_block = BlockBody.new
end end
def render(context) def render(context)
context.registers[:for] ||= Hash.new(0) segment = collection_segment(context)
collection = context[@collection_name] if segment.empty?
collection = collection.to_a if collection.is_a?(Range) render_else(context)
# Maintains Ruby 1.8.7 String#each behaviour on 1.9
return render_else(context) unless iterable?(collection)
from = if @attributes['offset'.freeze] == 'continue'.freeze
context.registers[:for][@name].to_i
else else
context[@attributes['offset'.freeze]].to_i render_segment(context, segment)
end end
limit = context[@attributes['limit'.freeze]]
to = limit ? limit.to_i + from : nil
segment = Utils.slice_collection(collection, from, to)
return render_else(context) if segment.empty?
segment.reverse! if @reversed
result = ''
length = segment.length
# Store our progress through the collection for the continue flag
context.registers[:for][@name] = from + segment.length
context.stack do
segment.each_with_index do |item, index|
context[@variable_name] = item
context['forloop'.freeze] = {
'name'.freeze => @name,
'length'.freeze => length,
'index'.freeze => index + 1,
'index0'.freeze => index,
'rindex'.freeze => length - index,
'rindex0'.freeze => length - index - 1,
'first'.freeze => (index == 0),
'last'.freeze => (index == length - 1)
}
result << render_all(@for_block, context)
# Handle any interrupts if they exist.
if context.has_interrupt?
interrupt = context.pop_interrupt
break if interrupt.is_a? BreakInterrupt
next if interrupt.is_a? ContinueInterrupt
end
end
end
result
end end
protected protected
@@ -128,12 +85,12 @@ module Liquid
def lax_parse(markup) def lax_parse(markup)
if markup =~ Syntax if markup =~ Syntax
@variable_name = $1 @variable_name = $1
@collection_name = $2 collection_name = $2
@name = "#{$1}-#{$2}" @reversed = !!$3
@reversed = $3 @name = "#{@variable_name}-#{collection_name}"
@attributes = {} @collection_name = Expression.parse(collection_name)
markup.scan(TagAttributes) do |key, value| markup.scan(TagAttributes) do |key, value|
@attributes[key] = value set_attribute(key, value)
end end
else else
raise SyntaxError.new(options[:locale].t("errors.syntax.for".freeze)) raise SyntaxError.new(options[:locale].t("errors.syntax.for".freeze))
@@ -143,31 +100,110 @@ module Liquid
def strict_parse(markup) def strict_parse(markup)
p = Parser.new(markup) p = Parser.new(markup)
@variable_name = p.consume(:id) @variable_name = p.consume(:id)
raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_in".freeze)) unless p.id?('in'.freeze) raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_in".freeze)) unless p.id?('in'.freeze)
@collection_name = p.expression collection_name = p.expression
@name = "#{@variable_name}-#{@collection_name}" @name = "#{@variable_name}-#{collection_name}"
@collection_name = Expression.parse(collection_name)
@reversed = p.id?('reversed'.freeze) @reversed = p.id?('reversed'.freeze)
@attributes = {}
while p.look(:id) && p.look(:colon, 1) while p.look(:id) && p.look(:colon, 1)
unless attribute = p.id?('limit'.freeze) || p.id?('offset'.freeze) unless attribute = p.id?('limit'.freeze) || p.id?('offset'.freeze)
raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_attribute".freeze)) raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_attribute".freeze))
end end
p.consume p.consume
val = p.expression set_attribute(attribute, p.expression)
@attributes[attribute] = val
end end
p.consume(:end_of_string) p.consume(:end_of_string)
end end
private private
def render_else(context) def collection_segment(context)
return @else_block ? [render_all(@else_block, context)] : ''.freeze offsets = context.registers[:for] ||= {}
from = if @from == :continue
offsets[@name].to_i
else
from_value = context.evaluate(@from)
if from_value.nil?
0
else
Utils.to_integer(from_value)
end
end
collection = context.evaluate(@collection_name)
limit_value = context.evaluate(@limit)
to = if limit_value.nil?
nil
else
Utils.to_integer(limit_value) + from
end
segment = Utils.slice_collection(collection, from, to)
segment.reverse! if @reversed
offsets[@name] = from + segment.size
segment
end end
def iterable?(collection) def render_segment(context, segment)
collection.respond_to?(:each) || Utils.non_blank_string?(collection) for_stack = context.registers[:for_stack] ||= []
length = segment.size
result = ''
context.stack do
loop_vars = Liquid::ForloopDrop.new(@name, length, for_stack[-1])
for_stack.push(loop_vars)
begin
context['forloop'.freeze] = loop_vars
segment.each do |item|
context[@variable_name] = item
result << @for_block.render(context)
loop_vars.send(:increment!)
# Handle any interrupts if they exist.
if context.interrupt?
interrupt = context.pop_interrupt
break if interrupt.is_a? BreakInterrupt
next if interrupt.is_a? ContinueInterrupt
end
end
ensure
for_stack.pop
end
end
result
end
def set_attribute(key, expr)
case key
when 'offset'.freeze
@from = if expr == 'continue'.freeze
:continue
else
Expression.parse(expr)
end
when 'limit'.freeze
@limit = Expression.parse(expr)
end
end
def render_else(context)
@else_block ? @else_block.render(context) : ''.freeze
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
(super + [@node.limit, @node.from, @node.collection_name]).compact
end
end end
end end

View File

@@ -12,7 +12,9 @@ module Liquid
class If < Block class If < Block
Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o
ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o
BOOLEAN_OPERATORS = %w(and or) BOOLEAN_OPERATORS = %w(and or).freeze
attr_reader :blocks
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
@@ -21,7 +23,12 @@ module Liquid
end end
def nodelist def nodelist
@blocks.map(&:attachment).flatten @blocks.map(&:attachment)
end
def parse(tokens)
while parse_body(@blocks.last.attachment, tokens)
end
end end
def unknown_tag(tag, markup, tokens) def unknown_tag(tag, markup, tokens)
@@ -36,7 +43,7 @@ module Liquid
context.stack do context.stack do
@blocks.each do |block| @blocks.each do |block|
if block.evaluate(context) if block.evaluate(context)
return render_all(block.attachment, context) return block.attachment.render(context)
end end
end end
''.freeze ''.freeze
@@ -45,61 +52,70 @@ module Liquid
private private
def push_block(tag, markup) def push_block(tag, markup)
block = if tag == 'else'.freeze block = if tag == 'else'.freeze
ElseCondition.new ElseCondition.new
else else
parse_with_selected_parser(markup) parse_with_selected_parser(markup)
end
@blocks.push(block)
@nodelist = block.attach(Array.new)
end end
def lax_parse(markup) @blocks.push(block)
expressions = markup.scan(ExpressionsAndOperators).reverse block.attach(BlockBody.new)
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.shift =~ Syntax end
condition = Condition.new($1, $2, $3) def lax_parse(markup)
expressions = markup.scan(ExpressionsAndOperators)
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop =~ Syntax
while not expressions.empty? condition = Condition.new(Expression.parse($1), $2, Expression.parse($3))
operator = (expressions.shift).to_s.strip
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.shift.to_s =~ Syntax until expressions.empty?
operator = expressions.pop.to_s.strip
new_condition = Condition.new($1, $2, $3) raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop.to_s =~ Syntax
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless BOOLEAN_OPERATORS.include?(operator)
new_condition.send(operator, condition)
condition = new_condition
end
condition new_condition = Condition.new(Expression.parse($1), $2, Expression.parse($3))
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless BOOLEAN_OPERATORS.include?(operator)
new_condition.send(operator, condition)
condition = new_condition
end end
def strict_parse(markup) condition
p = Parser.new(markup) end
condition = parse_comparison(p) def strict_parse(markup)
p = Parser.new(markup)
condition = parse_binary_comparisons(p)
p.consume(:end_of_string)
condition
end
while op = (p.id?('and'.freeze) || p.id?('or'.freeze)) def parse_binary_comparisons(p)
new_cond = parse_comparison(p) condition = parse_comparison(p)
new_cond.send(op, condition) first_condition = condition
condition = new_cond while op = (p.id?('and'.freeze) || p.id?('or'.freeze))
end child_condition = parse_comparison(p)
p.consume(:end_of_string) condition.send(op, child_condition)
condition = child_condition
condition
end end
first_condition
end
def parse_comparison(p) def parse_comparison(p)
a = p.expression a = Expression.parse(p.expression)
if op = p.consume?(:comparison) if op = p.consume?(:comparison)
b = p.expression b = Expression.parse(p.expression)
Condition.new(a, op, b) Condition.new(a, op, b)
else else
Condition.new(a) Condition.new(a)
end
end end
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
@node.blocks
end
end
end end
Template.register_tag('if'.freeze, If) Template.register_tag('if'.freeze, If)

View File

@@ -1,10 +1,8 @@
module Liquid module Liquid
class Ifchanged < Block class Ifchanged < Block
def render(context) def render(context)
context.stack do context.stack do
output = super
output = render_all(@nodelist, context)
if output != context.registers[:ifchanged] if output != context.registers[:ifchanged]
context.registers[:ifchanged] = output context.registers[:ifchanged] = output

View File

@@ -1,5 +1,4 @@
module Liquid module Liquid
# Include allows templates to relate with other templates # Include allows templates to relate with other templates
# #
# Simply include another template: # Simply include another template:
@@ -17,22 +16,22 @@ module Liquid
class Include < Tag class Include < Tag
Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/o Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/o
attr_reader :template_name_expr, :variable_name_expr, :attributes
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
if markup =~ Syntax if markup =~ Syntax
@template_name = $1 template_name = $1
@variable_name = $3 variable_name = $3
@attributes = {}
@variable_name_expr = variable_name ? Expression.parse(variable_name) : nil
@template_name_expr = Expression.parse(template_name)
@attributes = {}
markup.scan(TagAttributes) do |key, value| markup.scan(TagAttributes) do |key, value|
@attributes[key] = value @attributes[key] = Expression.parse(value)
end
if partial_parsable_at_parse_time?
source = read_template_from_file_system_at_parse
@partial = Liquid::Template.parse(source, pass_options)
end end
else else
@@ -40,90 +39,85 @@ module Liquid
end end
end end
def parse(tokens) def parse(_tokens)
end end
def render(context) def render(context)
partial = load_cached_partial(context) template_name = context.evaluate(@template_name_expr)
variable = context[@variable_name || @template_name[1..-2]] raise ArgumentError.new(options[:locale].t("errors.argument.include")) unless template_name
context.stack do partial = load_cached_partial(template_name, context)
@attributes.each do |key, value| context_variable_name = template_name.split('/'.freeze).last
context[key] = context[value]
end
context_variable_name = @template_name[1..-2].split('/'.freeze).last variable = if @variable_name_expr
if variable.is_a?(Array) context.evaluate(@variable_name_expr)
variable.collect do |var| else
context[context_variable_name] = var context.find_variable(template_name, raise_on_not_found: false)
end
old_template_name = context.template_name
old_partial = context.partial
begin
context.template_name = template_name
context.partial = true
context.stack do
@attributes.each do |key, value|
context[key] = context.evaluate(value)
end
if variable.is_a?(Array)
variable.collect do |var|
context[context_variable_name] = var
partial.render(context)
end
else
context[context_variable_name] = variable
partial.render(context) partial.render(context)
end end
else
context[context_variable_name] = variable
partial.render(context)
end end
ensure
context.template_name = old_template_name
context.partial = old_partial
end end
end end
private private
def load_cached_partial(context)
cached_partials = context.registers[:cached_partials] || {}
template_name = context[@template_name]
if cached = cached_partials[template_name] alias_method :parse_context, :options
cached private :parse_context
else
if @partial && context.registers[:file_system].nil? def load_cached_partial(template_name, context)
partial = @partial cached_partials = context.registers[:cached_partials] || {}
else
partial = Liquid::Template.parse(read_template_from_file_system(context), pass_options) if cached = cached_partials[template_name]
end return cached
cached_partials[template_name] = partial
context.registers[:cached_partials] = cached_partials
partial
end
end end
source = read_template_from_file_system(context)
def read_template_from_file_system(context) begin
file_system = context.registers[:file_system] || parsed_file_system || Liquid::Template.file_system parse_context.partial = true
partial = Liquid::Template.parse(source, parse_context)
# make read_template_file call backwards-compatible. ensure
case file_system.method(:read_template_file).arity parse_context.partial = false
when 1
file_system.read_template_file(context[@template_name])
when 2
file_system.read_template_file(context[@template_name], context)
else
raise ArgumentError, "file_system.read_template_file expects two parameters: (template_name, context)"
end
end end
cached_partials[template_name] = partial
context.registers[:cached_partials] = cached_partials
partial
end
def read_template_from_file_system_at_parse def read_template_from_file_system(context)
parsed_file_system.read_template_file(parsed_template_name) file_system = context.registers[:file_system] || Liquid::Template.file_system
end
def parsed_file_system file_system.read_template_file(context.evaluate(@template_name_expr))
options[:file_system] end
end
def partial_parsable_at_parse_time? class ParseTreeVisitor < Liquid::ParseTreeVisitor
template_name_is_string_constant = parsed_template_name.is_a?(String) def children
options[:file_system] && template_name_is_string_constant [
end @node.template_name_expr,
@node.variable_name_expr
def parsed_template_name ] + @node.attributes.values
Expression.parse(@template_name)
end
def pass_options
dont_pass = @options[:include_options_blacklist]
return {locale: @options[:locale]} if dont_pass == true
opts = @options.merge(included: true, include_options_blacklist: false)
if dont_pass.is_a?(Array)
dont_pass.each {|o| opts.delete(o)}
end
opts
end end
end
end end
Template.register_tag('include'.freeze, Include) Template.register_tag('include'.freeze, Include)

View File

@@ -1,19 +1,44 @@
module Liquid module Liquid
class Raw < Block class Raw < Block
Syntax = /\A\s*\z/
FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
def initialize(tag_name, markup, parse_context)
super
ensure_valid_markup(tag_name, markup, parse_context)
end
def parse(tokens) def parse(tokens)
@nodelist ||= [] @body = ''
@nodelist.clear
while token = tokens.shift while token = tokens.shift
if token =~ FullTokenPossiblyInvalid if token =~ FullTokenPossiblyInvalid
@nodelist << $1 if $1 != "".freeze @body << $1 if $1 != "".freeze
if block_delimiter == $2 return if block_delimiter == $2
end_tag
return
end
end end
@nodelist << token if not token.empty? @body << token unless token.empty?
end
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name))
end
def render(_context)
@body
end
def nodelist
[@body]
end
def blank?
@body.empty?
end
protected
def ensure_valid_markup(tag_name, markup, parse_context)
unless markup =~ Syntax
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_unexpected_args".freeze, tag: tag_name))
end end
end end
end end

View File

@@ -2,14 +2,16 @@ module Liquid
class TableRow < Block class TableRow < Block
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
attr_reader :variable_name, :collection_name, :attributes
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
if markup =~ Syntax if markup =~ Syntax
@variable_name = $1 @variable_name = $1
@collection_name = $2 @collection_name = Expression.parse($2)
@attributes = {} @attributes = {}
markup.scan(TagAttributes) do |key, value| markup.scan(TagAttributes) do |key, value|
@attributes[key] = value @attributes[key] = Expression.parse(value)
end end
else else
raise SyntaxError.new(options[:locale].t("errors.syntax.table_row".freeze)) raise SyntaxError.new(options[:locale].t("errors.syntax.table_row".freeze))
@@ -17,56 +19,43 @@ module Liquid
end end
def render(context) def render(context)
collection = context[@collection_name] or return ''.freeze collection = context.evaluate(@collection_name) or return ''.freeze
from = @attributes['offset'.freeze] ? context[@attributes['offset'.freeze]].to_i : 0 from = @attributes.key?('offset'.freeze) ? context.evaluate(@attributes['offset'.freeze]).to_i : 0
to = @attributes['limit'.freeze] ? from + context[@attributes['limit'.freeze]].to_i : nil to = @attributes.key?('limit'.freeze) ? from + context.evaluate(@attributes['limit'.freeze]).to_i : nil
collection = Utils.slice_collection(collection, from, to) collection = Utils.slice_collection(collection, from, to)
length = collection.length length = collection.length
cols = context[@attributes['cols'.freeze]].to_i cols = context.evaluate(@attributes['cols'.freeze]).to_i
row = 1
col = 0
result = "<tr class=\"row1\">\n" result = "<tr class=\"row1\">\n"
context.stack do context.stack do
tablerowloop = Liquid::TablerowloopDrop.new(length, cols)
context['tablerowloop'.freeze] = tablerowloop
collection.each_with_index do |item, index| collection.each do |item|
context[@variable_name] = item context[@variable_name] = item
context['tablerowloop'.freeze] = {
'length'.freeze => length,
'index'.freeze => index + 1,
'index0'.freeze => index,
'col'.freeze => col + 1,
'col0'.freeze => col,
'index0'.freeze => index,
'rindex'.freeze => length - index,
'rindex0'.freeze => length - index - 1,
'first'.freeze => (index == 0),
'last'.freeze => (index == length - 1),
'col_first'.freeze => (col == 0),
'col_last'.freeze => (col == cols - 1)
}
result << "<td class=\"col#{tablerowloop.col}\">" << super << '</td>'
col += 1 if tablerowloop.col_last && !tablerowloop.last
result << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">"
result << "<td class=\"col#{col}\">" << render_all(@nodelist, context) << '</td>'
if col == cols and (index != length - 1)
col = 0
row += 1
result << "</tr>\n<tr class=\"row#{row}\">"
end end
tablerowloop.send(:increment!)
end end
end end
result << "</tr>\n" result << "</tr>\n"
result result
end end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
super + @node.attributes.values + [@node.collection_name]
end
end
end end
Template.register_tag('tablerow'.freeze, TableRow) Template.register_tag('tablerow'.freeze, TableRow)

View File

@@ -1,24 +1,23 @@
require File.dirname(__FILE__) + '/if' require_relative 'if'
module Liquid module Liquid
# Unless is a conditional just like 'if' but works on the inverse logic. # Unless is a conditional just like 'if' but works on the inverse logic.
# #
# {% unless x < 0 %} x is greater than zero {% end %} # {% unless x < 0 %} x is greater than zero {% endunless %}
# #
class Unless < If class Unless < If
def render(context) def render(context)
context.stack do context.stack do
# First condition is interpreted backwards ( if not ) # First condition is interpreted backwards ( if not )
first_block = @blocks.first first_block = @blocks.first
unless first_block.evaluate(context) unless first_block.evaluate(context)
return render_all(first_block.attachment, context) return first_block.attachment.render(context)
end end
# After the first condition unless works just like if # After the first condition unless works just like if
@blocks[1..-1].each do |block| @blocks[1..-1].each do |block|
if block.evaluate(context) if block.evaluate(context)
return render_all(block.attachment, context) return block.attachment.render(context)
end end
end end

View File

@@ -1,5 +1,4 @@
module Liquid module Liquid
# Templates are central to liquid. # Templates are central to liquid.
# Interpretating templates is a two step process. First you compile the # Interpretating templates is a two step process. First you compile the
# source code you got. During compile time some extensive error checking is performed. # source code you got. During compile time some extensive error checking is performed.
@@ -14,21 +13,21 @@ module Liquid
# template.render('user_name' => 'bob') # template.render('user_name' => 'bob')
# #
class Template class Template
DEFAULT_OPTIONS = { attr_accessor :root
:locale => I18n.new attr_reader :resource_limits, :warnings
}
attr_accessor :root, :resource_limits
@@file_system = BlankFileSystem.new @@file_system = BlankFileSystem.new
class TagRegistry class TagRegistry
include Enumerable
def initialize def initialize
@tags = {} @tags = {}
@cache = {} @cache = {}
end end
def [](tag_name) def [](tag_name)
return nil unless @tags.has_key?(tag_name) return nil unless @tags.key?(tag_name)
return @cache[tag_name] if Liquid.cache_classes return @cache[tag_name] if Liquid.cache_classes
lookup_class(@tags[tag_name]).tap { |o| @cache[tag_name] = o } lookup_class(@tags[tag_name]).tap { |o| @cache[tag_name] = o }
@@ -44,6 +43,10 @@ module Liquid
@cache.delete(tag_name) @cache.delete(tag_name)
end end
def each(&block)
@tags.each(&block)
end
private private
def lookup_class(name) def lookup_class(name)
@@ -60,6 +63,17 @@ module Liquid
# :strict will enforce correct syntax. # :strict will enforce correct syntax.
attr_writer :error_mode attr_writer :error_mode
# Sets how strict the taint checker should be.
# :lax is the default, and ignores the taint flag completely
# :warn adds a warning, but does not interrupt the rendering
# :error raises an error when tainted output is used
attr_writer :taint_mode
attr_accessor :default_exception_renderer
Template.default_exception_renderer = lambda do |exception|
exception
end
def file_system def file_system
@@file_system @@file_system
end end
@@ -77,7 +91,11 @@ module Liquid
end end
def error_mode def error_mode
@error_mode || :lax @error_mode ||= :lax
end
def taint_mode
@taint_mode ||= :lax
end end
# Pass a module with filter methods which should be available # Pass a module with filter methods which should be available
@@ -100,7 +118,8 @@ module Liquid
end end
def initialize def initialize
@resource_limits = self.class.default_resource_limits.dup @rethrow_errors = false
@resource_limits = ResourceLimits.new(self.class.default_resource_limits)
end end
# Parse source code. # Parse source code.
@@ -109,16 +128,12 @@ module Liquid
@options = options @options = options
@profiling = options[:profile] @profiling = options[:profile]
@line_numbers = options[:line_numbers] || @profiling @line_numbers = options[:line_numbers] || @profiling
@root = Document.parse(tokenize(source), DEFAULT_OPTIONS.merge(options)) parse_context = options.is_a?(ParseContext) ? options : ParseContext.new(options)
@warnings = nil @root = Document.parse(tokenize(source), parse_context)
@warnings = parse_context.warnings
self self
end end
def warnings
return [] unless @root
@warnings ||= @root.warnings
end
def registers def registers
@registers ||= {} @registers ||= {}
end end
@@ -157,7 +172,7 @@ module Liquid
c = args.shift c = args.shift
if @rethrow_errors if @rethrow_errors
c.exception_handler = ->(e) { true } c.exception_renderer = ->(e) { raise }
end end
c c
@@ -176,27 +191,20 @@ module Liquid
when Hash when Hash
options = args.pop options = args.pop
if options[:registers].is_a?(Hash) registers.merge!(options[:registers]) if options[:registers].is_a?(Hash)
self.registers.merge!(options[:registers])
end
if options[:filters] apply_options_to_context(context, options)
context.add_filters(options[:filters]) when Module, Array
end
if options[:exception_handler]
context.exception_handler = options[:exception_handler]
end
when Module
context.add_filters(args.pop)
when Array
context.add_filters(args.pop) context.add_filters(args.pop)
end end
# Retrying a render resets resource usage
context.resource_limits.reset
begin begin
# render the nodelist. # render the nodelist.
# for performance reasons we get an array back here. join will make a string out of it. # for performance reasons we get an array back here. join will make a string out of it.
result = with_profiling do result = with_profiling(context) do
@root.render(context) @root.render(context)
end end
result.respond_to?(:join) ? result.join : result result.respond_to?(:join) ? result.join : result
@@ -214,32 +222,14 @@ module Liquid
private private
# Uses the <tt>Liquid::TemplateParser</tt> regexp to tokenize the passed source
def tokenize(source) def tokenize(source)
source = source.source if source.respond_to?(:source) Tokenizer.new(source, @line_numbers)
return [] if source.to_s.empty?
tokens = calculate_line_numbers(source.split(TemplateParser))
# removes the rogue empty element at the beginning of the array
tokens.shift if tokens[0] and tokens[0].empty?
tokens
end end
def calculate_line_numbers(raw_tokens) def with_profiling(context)
return raw_tokens unless @line_numbers if @profiling && !context.partial
raise "Profiler not loaded, require 'liquid/profiler' first" unless defined?(Liquid::Profiler)
current_line = 1
raw_tokens.map do |token|
Token.new(token, current_line).tap do
current_line += token.count("\n")
end
end
end
def with_profiling
if @profiling && !@options[:included]
@profiler = Profiler.new @profiler = Profiler.new
@profiler.start @profiler.start
@@ -252,5 +242,13 @@ module Liquid
yield yield
end end
end end
def apply_options_to_context(context, options)
context.add_filters(options[:filters]) if options[:filters]
context.global_filter = options[:global_filter] if options[:global_filter]
context.exception_renderer = options[:exception_renderer] if options[:exception_renderer]
context.strict_variables = options[:strict_variables] if options[:strict_variables]
context.strict_filters = options[:strict_filters] if options[:strict_filters]
end
end end
end end

View File

@@ -1,18 +0,0 @@
module Liquid
class Token < String
attr_reader :line_number
def initialize(content, line_number)
super(content)
@line_number = line_number
end
def raw
"<raw>"
end
def child(string)
Token.new(string, @line_number)
end
end
end

31
lib/liquid/tokenizer.rb Normal file
View File

@@ -0,0 +1,31 @@
module Liquid
class Tokenizer
attr_reader :line_number
def initialize(source, line_numbers = false)
@source = source
@line_number = line_numbers ? 1 : nil
@tokens = tokenize
end
def shift
token = @tokens.shift
@line_number += token.count("\n") if @line_number && token
token
end
private
def tokenize
@source = @source.source if @source.respond_to?(:source)
return [] if @source.to_s.empty?
tokens = @source.split(TemplateParser)
# removes the rogue empty element at the beginning of the array
tokens.shift if tokens[0] && tokens[0].empty?
tokens
end
end
end

View File

@@ -1,27 +1,26 @@
module Liquid module Liquid
module Utils module Utils
def self.slice_collection(collection, from, to) def self.slice_collection(collection, from, to)
if (from != 0 || to != nil) && collection.respond_to?(:load_slice) return collection.load_slice(from, to) if collection.is_a?(ReversableRange)
if (from != 0 || !to.nil?) && collection.respond_to?(:load_slice)
collection.load_slice(from, to) collection.load_slice(from, to)
else else
slice_collection_using_each(collection, from, to) slice_collection_using_each(collection, from, to)
end end
end end
def self.non_blank_string?(collection)
collection.is_a?(String) && collection != ''.freeze
end
def self.slice_collection_using_each(collection, from, to) def self.slice_collection_using_each(collection, from, to)
segments = [] segments = []
index = 0 index = 0
# Maintains Ruby 1.8.7 String#each behaviour on 1.9 # Maintains Ruby 1.8.7 String#each behaviour on 1.9
return [collection] if non_blank_string?(collection) if collection.is_a?(String)
return collection.empty? ? [] : [collection]
end
return [] unless collection.respond_to?(:each)
collection.each do |item| collection.each do |item|
if to && to <= index if to && to <= index
break break
end end
@@ -35,5 +34,52 @@ module Liquid
segments segments
end end
def self.to_integer(num)
return num if num.is_a?(Integer)
num = num.to_s
begin
Integer(num)
rescue ::ArgumentError
raise Liquid::ArgumentError, "invalid integer"
end
end
def self.to_number(obj)
case obj
when Float
BigDecimal(obj.to_s)
when Numeric
obj
when String
(obj.strip =~ /\A-?\d+\.\d+\z/) ? BigDecimal(obj) : obj.to_i
else
if obj.respond_to?(:to_number)
obj.to_number
else
0
end
end
end
def self.to_date(obj)
return obj if obj.respond_to?(:strftime)
if obj.is_a?(String)
return nil if obj.empty?
obj = obj.downcase
end
case obj
when 'now'.freeze, 'today'.freeze
Time.now
when /\A\d+\z/, Integer
Time.at(obj.to_i)
when String
Time.parse(obj)
end
rescue ::ArgumentError
nil
end
end end
end end

View File

@@ -1,5 +1,4 @@
module Liquid module Liquid
# Holds variables. Variables are only loaded "just in time" # Holds variables. Variables are only loaded "just in time"
# and are not evaluated as part of the render stage # and are not evaluated as part of the render stage
# #
@@ -11,16 +10,23 @@ module Liquid
# {{ user | link }} # {{ user | link }}
# #
class Variable class Variable
FilterMarkupRegex = /#{FilterSeparator}\s*(.*)/om
FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o
EasyParse = /\A *(\w+(?:\.\w+)*) *\z/ FilterArgsRegex = /(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o
attr_accessor :filters, :name, :warnings JustTagAttributes = /\A#{TagAttributes}\z/o
attr_accessor :line_number MarkupWithQuotedFragment = /(#{QuotedFragment})(.*)/om
attr_accessor :filters, :name, :line_number
attr_reader :parse_context
alias_method :options, :parse_context
include ParserSwitching include ParserSwitching
def initialize(markup, options = {}) def initialize(markup, parse_context)
@markup = markup @markup = markup
@name = nil @name = nil
@options = options || {} @parse_context = parse_context
@line_number = parse_context.line_number
parse_with_selected_parser(markup) parse_with_selected_parser(markup)
end end
@@ -35,37 +41,31 @@ module Liquid
def lax_parse(markup) def lax_parse(markup)
@filters = [] @filters = []
if markup =~ /\s*(#{QuotedFragment})(.*)/om return unless markup =~ MarkupWithQuotedFragment
@name = Regexp.last_match(1)
if Regexp.last_match(2) =~ /#{FilterSeparator}\s*(.*)/om name_markup = $1
filters = Regexp.last_match(1).scan(FilterParser) filter_markup = $2
filters.each do |f| @name = Expression.parse(name_markup)
if f =~ /\w+/ if filter_markup =~ FilterMarkupRegex
filtername = Regexp.last_match(0) filters = $1.scan(FilterParser)
filterargs = f.scan(/(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o).flatten filters.each do |f|
@filters << [filtername, filterargs] next unless f =~ /\w+/
end filtername = Regexp.last_match(0)
end filterargs = f.scan(FilterArgsRegex).flatten
@filters << parse_filter_expressions(filtername, filterargs)
end end
end end
end end
def strict_parse(markup) def strict_parse(markup)
# Very simple valid cases
if markup =~ EasyParse
@name = $1
@filters = []
return
end
@filters = [] @filters = []
p = Parser.new(markup) p = Parser.new(markup)
# Could be just filters with no input
@name = p.look(:pipe) ? ''.freeze : p.expression @name = Expression.parse(p.expression)
while p.consume?(:pipe) while p.consume?(:pipe)
filtername = p.consume(:id) filtername = p.consume(:id)
filterargs = p.consume?(:colon) ? parse_filterargs(p) : [] filterargs = p.consume?(:colon) ? parse_filterargs(p) : []
@filters << [filtername, filterargs] @filters << parse_filter_expressions(filtername, filterargs)
end end
p.consume(:end_of_string) p.consume(:end_of_string)
end end
@@ -74,30 +74,75 @@ module Liquid
# first argument # first argument
filterargs = [p.argument] filterargs = [p.argument]
# followed by comma separated others # followed by comma separated others
while p.consume?(:comma) filterargs << p.argument while p.consume?(:comma)
filterargs << p.argument
end
filterargs filterargs
end end
def render(context) def render(context)
return ''.freeze if @name.nil? obj = @filters.inject(context.evaluate(@name)) do |output, (filter_name, filter_args, filter_kwargs)|
@filters.inject(context[@name]) do |output, filter| filter_args = evaluate_filter_expressions(context, filter_args, filter_kwargs)
filterargs = [] context.invoke(filter_name, output, *filter_args)
keyword_args = {} end
filter[1].to_a.each do |a|
if matches = a.match(/\A#{TagAttributes}\z/o) obj = context.apply_global_filter(obj)
keyword_args[matches[1]] = context[matches[2]]
else taint_check(context, obj)
filterargs << context[a]
end obj
end
private
def parse_filter_expressions(filter_name, unparsed_args)
filter_args = []
keyword_args = nil
unparsed_args.each do |a|
if matches = a.match(JustTagAttributes)
keyword_args ||= {}
keyword_args[matches[1]] = Expression.parse(matches[2])
else
filter_args << Expression.parse(a)
end end
filterargs << keyword_args unless keyword_args.empty? end
begin result = [filter_name, filter_args]
output = context.invoke(filter[0], output, *filterargs) result << keyword_args if keyword_args
rescue FilterNotFound result
raise FilterNotFound, "Error - filter '#{filter[0]}' in '#{@markup.strip}' could not be found." end
def evaluate_filter_expressions(context, filter_args, filter_kwargs)
parsed_args = filter_args.map{ |expr| context.evaluate(expr) }
if filter_kwargs
parsed_kwargs = {}
filter_kwargs.each do |key, expr|
parsed_kwargs[key] = context.evaluate(expr)
end end
parsed_args << parsed_kwargs
end
parsed_args
end
def taint_check(context, obj)
return unless obj.tainted?
return if Template.taint_mode == :lax
@markup =~ QuotedFragment
name = Regexp.last_match(0)
error = TaintedError.new("variable '#{name}' is tainted and was not escaped")
error.line_number = line_number
error.template_name = context.template_name
case Template.taint_mode
when :warn
context.warnings << error
when :error
raise error
end
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[@node.name] + @node.filters.flatten
end end
end end
end end

View File

@@ -1,7 +1,9 @@
module Liquid module Liquid
class VariableLookup class VariableLookup
SQUARE_BRACKETED = /\A\[(.*)\]\z/m SQUARE_BRACKETED = /\A\[(.*)\]\z/m
COMMAND_METHODS = ['size'.freeze, 'first'.freeze, 'last'.freeze] COMMAND_METHODS = ['size'.freeze, 'first'.freeze, 'last'.freeze].freeze
attr_reader :name, :lookups
def self.parse(markup) def self.parse(markup)
new(markup) new(markup)
@@ -39,8 +41,8 @@ module Liquid
# If object is a hash- or array-like object we look for the # If object is a hash- or array-like object we look for the
# presence of the key and if its available we return it # presence of the key and if its available we return it
if object.respond_to?(:[]) && if object.respond_to?(:[]) &&
((object.respond_to?(:has_key?) && object.has_key?(key)) || ((object.respond_to?(:key?) && object.key?(key)) ||
(object.respond_to?(:fetch) && key.is_a?(Integer))) (object.respond_to?(:fetch) && key.is_a?(Integer)))
# if its a proc we will replace the entry with the proc # if its a proc we will replace the entry with the proc
res = context.lookup_and_evaluate(object, key) res = context.lookup_and_evaluate(object, key)
@@ -53,9 +55,11 @@ module Liquid
object = object.send(key).to_liquid object = object.send(key).to_liquid
# No key was present with the desired value and it wasn't one of the directly supported # No key was present with the desired value and it wasn't one of the directly supported
# keywords either. The only thing we got left is to return nil # keywords either. The only thing we got left is to return nil or
# raise an exception if `strict_variables` option is set to true
else else
return nil return nil unless context.strict_variables
raise Liquid::UndefinedVariable, "undefined variable #{key}"
end end
# If we are dealing with a drop here we have to # If we are dealing with a drop here we have to
@@ -64,5 +68,21 @@ module Liquid
object object
end end
def ==(other)
self.class == other.class && state == other.state
end
protected
def state
[@name, @lookups, @command_flags]
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
@node.lookups
end
end
end end
end end

View File

@@ -1,4 +1,5 @@
# encoding: utf-8 # encoding: utf-8
module Liquid module Liquid
VERSION = "3.0.0" VERSION = "4.0.3".freeze
end end

View File

@@ -1,6 +1,7 @@
# encoding: utf-8 # encoding: utf-8
lib = File.expand_path('../lib/', __FILE__) lib = File.expand_path('../lib/', __FILE__)
$:.unshift lib unless $:.include?(lib) $LOAD_PATH.unshift lib unless $LOAD_PATH.include?(lib)
require "liquid/version" require "liquid/version"
@@ -9,21 +10,22 @@ Gem::Specification.new do |s|
s.version = Liquid::VERSION s.version = Liquid::VERSION
s.platform = Gem::Platform::RUBY s.platform = Gem::Platform::RUBY
s.summary = "A secure, non-evaling end user template engine with aesthetic markup." s.summary = "A secure, non-evaling end user template engine with aesthetic markup."
s.authors = ["Tobias Luetke"] s.authors = ["Tobias Lütke"]
s.email = ["tobi@leetsoft.com"] s.email = ["tobi@leetsoft.com"]
s.homepage = "http://www.liquidmarkup.org" s.homepage = "http://www.liquidmarkup.org"
s.license = "MIT" s.license = "MIT"
#s.description = "A secure, non-evaling end user template engine with aesthetic markup." # s.description = "A secure, non-evaling end user template engine with aesthetic markup."
s.required_ruby_version = ">= 2.1.0"
s.required_rubygems_version = ">= 1.3.7" s.required_rubygems_version = ">= 1.3.7"
s.test_files = Dir.glob("{test}/**/*") s.test_files = Dir.glob("{test}/**/*")
s.files = Dir.glob("{lib}/**/*") + %w(MIT-LICENSE README.md) s.files = Dir.glob("{lib}/**/*") + %w(LICENSE README.md)
s.extra_rdoc_files = ["History.md", "README.md"] s.extra_rdoc_files = ["History.md", "README.md"]
s.require_path = "lib" s.require_path = "lib"
s.add_development_dependency 'rake' s.add_development_dependency 'rake', '~> 11.3'
s.add_development_dependency 'minitest' s.add_development_dependency 'minitest'
end end

View File

@@ -1,11 +1,11 @@
require 'benchmark/ips' require 'benchmark/ips'
require File.dirname(__FILE__) + '/theme_runner' require_relative 'theme_runner'
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new profiler = ThemeRunner.new
Benchmark.ips do |x| Benchmark.ips do |x|
x.time = 60 x.time = 10
x.warmup = 5 x.warmup = 5
puts puts
@@ -13,5 +13,6 @@ Benchmark.ips do |x|
puts puts
x.report("parse:") { profiler.compile } x.report("parse:") { profiler.compile }
x.report("parse & run:") { profiler.run } x.report("render:") { profiler.render }
x.report("parse & render:") { profiler.run }
end end

View File

@@ -0,0 +1,26 @@
# frozen_string_literal: true
require 'benchmark/ips'
require 'memory_profiler'
require_relative 'theme_runner'
def profile(phase, &block)
puts
puts "#{phase}:"
puts
report = MemoryProfiler.report(&block)
report.pretty_print(
color_output: true,
scale_bytes: true,
detailed_report: true
)
end
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new
profile("Parsing") { profiler.compile }
profile("Rendering") { profiler.render }

View File

@@ -1,17 +1,24 @@
require 'stackprof' rescue fail("install stackprof extension/gem") require 'stackprof'
require File.dirname(__FILE__) + '/theme_runner' require_relative 'theme_runner'
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new profiler = ThemeRunner.new
profiler.run profiler.run
[:cpu, :object].each do |profile_type| [:cpu, :object].each do |profile_type|
puts "Profiling in #{profile_type.to_s} mode..." puts "Profiling in #{profile_type} mode..."
results = StackProf.run(mode: profile_type) do results = StackProf.run(mode: profile_type) do
100.times do 200.times do
profiler.run profiler.run
end end
end end
if profile_type == :cpu && graph_filename = ENV['GRAPH_FILENAME']
File.open(graph_filename, 'w') do |f|
StackProf::Report.new(results).print_graphviz(nil, f)
end
end
StackProf::Report.new(results).print_text(false, 20) StackProf::Report.new(results).print_text(false, 20)
File.write(ENV['FILENAME'] + "." + profile_type.to_s, Marshal.dump(results)) if ENV['FILENAME'] File.write(ENV['FILENAME'] + "." + profile_type.to_s, Marshal.dump(results)) if ENV['FILENAME']
end end

View File

@@ -28,6 +28,6 @@ class CommentForm < Liquid::Block
end end
def wrap_in_form(article, input) def wrap_in_form(article, input)
%Q{<form id="article-#{article.id}-comment-form" class="comment-form" method="post" action="">\n#{input}\n</form>} %(<form id="article-#{article.id}-comment-form" class="comment-form" method="post" action="">\n#{input}\n</form>)
end end
end end

View File

@@ -5,7 +5,7 @@ module Database
# to liquid as assigns. All this is based on Shopify # to liquid as assigns. All this is based on Shopify
def self.tables def self.tables
@tables ||= begin @tables ||= begin
db = YAML.load_file(File.dirname(__FILE__) + '/vision.database.yml') db = YAML.load_file("#{__dir__}/vision.database.yml")
# From vision source # From vision source
db['products'].each do |product| db['products'].each do |product|
@@ -39,7 +39,7 @@ module Database
end end
end end
if __FILE__ == $0 if __FILE__ == $PROGRAM_NAME
p Database.tables['collections']['frontpage'].keys p Database.tables['collections']['frontpage'].keys
#p Database.tables['blog']['articles'] # p Database.tables['blog']['articles']
end end

View File

@@ -1,9 +1,7 @@
require 'json' require 'json'
module JsonFilter module JsonFilter
def json(object) def json(object)
JSON.dump(object.reject {|k,v| k == "collections" }) JSON.dump(object.reject { |k, v| k == "collections" })
end end
end end

View File

@@ -1,13 +1,13 @@
$:.unshift File.dirname(__FILE__) + '/../../lib' $:.unshift __dir__ + '/../../lib'
require File.dirname(__FILE__) + '/../../lib/liquid' require_relative '../../lib/liquid'
require File.dirname(__FILE__) + '/comment_form' require_relative 'comment_form'
require File.dirname(__FILE__) + '/paginate' require_relative 'paginate'
require File.dirname(__FILE__) + '/json_filter' require_relative 'json_filter'
require File.dirname(__FILE__) + '/money_filter' require_relative 'money_filter'
require File.dirname(__FILE__) + '/shop_filter' require_relative 'shop_filter'
require File.dirname(__FILE__) + '/tag_filter' require_relative 'tag_filter'
require File.dirname(__FILE__) + '/weight_filter' require_relative 'weight_filter'
Liquid::Template.register_tag 'paginate', Paginate Liquid::Template.register_tag 'paginate', Paginate
Liquid::Template.register_tag 'form', CommentForm Liquid::Template.register_tag 'form', CommentForm

View File

@@ -1,13 +1,12 @@
module MoneyFilter module MoneyFilter
def money_with_currency(money) def money_with_currency(money)
return '' if money.nil? return '' if money.nil?
sprintf("$ %.2f USD", money/100.0) sprintf("$ %.2f USD", money / 100.0)
end end
def money(money) def money(money)
return '' if money.nil? return '' if money.nil?
sprintf("$ %.2f", money/100.0) sprintf("$ %.2f", money / 100.0)
end end
private private

View File

@@ -4,8 +4,6 @@ class Paginate < Liquid::Block
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
@nodelist = []
if markup =~ Syntax if markup =~ Syntax
@collection_name = $1 @collection_name = $1
@page_size = if $2 @page_size = if $2
@@ -44,23 +42,22 @@ class Paginate < Liquid::Block
page_count = (collection_size.to_f / @page_size.to_f).to_f.ceil + 1 page_count = (collection_size.to_f / @page_size.to_f).to_f.ceil + 1
pagination['items'] = collection_size pagination['items'] = collection_size
pagination['pages'] = page_count -1 pagination['pages'] = page_count - 1
pagination['previous'] = link('&laquo; Previous', current_page-1 ) unless 1 >= current_page pagination['previous'] = link('&laquo; Previous', current_page - 1) unless 1 >= current_page
pagination['next'] = link('Next &raquo;', current_page+1 ) unless page_count <= current_page+1 pagination['next'] = link('Next &raquo;', current_page + 1) unless page_count <= current_page + 1
pagination['parts'] = [] pagination['parts'] = []
hellip_break = false hellip_break = false
if page_count > 2 if page_count > 2
1.upto(page_count-1) do |page| 1.upto(page_count - 1) do |page|
if current_page == page if current_page == page
pagination['parts'] << no_link(page) pagination['parts'] << no_link(page)
elsif page == 1 elsif page == 1
pagination['parts'] << link(page, page) pagination['parts'] << link(page, page)
elsif page == page_count -1 elsif page == page_count - 1
pagination['parts'] << link(page, page) pagination['parts'] << link(page, page)
elsif page <= current_page - @attributes['window_size'] or page >= current_page + @attributes['window_size'] elsif page <= current_page - @attributes['window_size'] || page >= current_page + @attributes['window_size']
next if hellip_break next if hellip_break
pagination['parts'] << no_link('&hellip;') pagination['parts'] << no_link('&hellip;')
hellip_break = true hellip_break = true
@@ -73,18 +70,18 @@ class Paginate < Liquid::Block
end end
end end
render_all(@nodelist, context) super
end end
end end
private private
def no_link(title) def no_link(title)
{ 'title' => title, 'is_link' => false} { 'title' => title, 'is_link' => false }
end end
def link(title, page) def link(title, page)
{ 'title' => title, 'url' => current_url + "?page=#{page}", 'is_link' => true} { 'title' => title, 'url' => current_url + "?page=#{page}", 'is_link' => true }
end end
def current_url def current_url

View File

@@ -1,5 +1,4 @@
module ShopFilter module ShopFilter
def asset_url(input) def asset_url(input)
"/files/1/[shop_id]/[shop_id]/assets/#{input}" "/files/1/[shop_id]/[shop_id]/assets/#{input}"
end end
@@ -16,16 +15,16 @@ module ShopFilter
%(<script src="#{url}" type="text/javascript"></script>) %(<script src="#{url}" type="text/javascript"></script>)
end end
def stylesheet_tag(url, media="all") def stylesheet_tag(url, media = "all")
%(<link href="#{url}" rel="stylesheet" type="text/css" media="#{media}" />) %(<link href="#{url}" rel="stylesheet" type="text/css" media="#{media}" />)
end end
def link_to(link, url, title="") def link_to(link, url, title = "")
%|<a href="#{url}" title="#{title}">#{link}</a>| %(<a href="#{url}" title="#{title}">#{link}</a>)
end end
def img_tag(url, alt="") def img_tag(url, alt = "")
%|<img src="#{url}" alt="#{alt}" />| %(<img src="#{url}" alt="#{alt}" />)
end end
def link_to_vendor(vendor) def link_to_vendor(vendor)
@@ -53,7 +52,6 @@ module ShopFilter
end end
def product_img_url(url, style = 'small') def product_img_url(url, style = 'small')
unless url =~ /\Aproducts\/([\w\-\_]+)\.(\w{2,4})/ unless url =~ /\Aproducts\/([\w\-\_]+)\.(\w{2,4})/
raise ArgumentError, 'filter "size" can only be called on product images' raise ArgumentError, 'filter "size" can only be called on product images'
end end
@@ -69,7 +67,6 @@ module ShopFilter
end end
def default_pagination(paginate) def default_pagination(paginate)
html = [] html = []
html << %(<span class="prev">#{link_to(paginate['previous']['title'], paginate['previous']['url'])}</span>) if paginate['previous'] html << %(<span class="prev">#{link_to(paginate['previous']['title'], paginate['previous']['url'])}</span>) if paginate['previous']
@@ -106,5 +103,4 @@ module ShopFilter
result.gsub!(/\A-+/, '') if result[0] == '-' result.gsub!(/\A-+/, '') if result[0] == '-'
result result
end end
end end

View File

@@ -1,10 +1,9 @@
module TagFilter module TagFilter
def link_to_tag(label, tag) def link_to_tag(label, tag)
"<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tag}\">#{label}</a>" "<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tag}\">#{label}</a>"
end end
def highlight_active_tag(tag, css_class='active') def highlight_active_tag(tag, css_class = 'active')
if @context['current_tags'].include?(tag) if @context['current_tags'].include?(tag)
"<span class=\"#{css_class}\">#{tag}</span>" "<span class=\"#{css_class}\">#{tag}</span>"
else else
@@ -21,5 +20,4 @@ module TagFilter
tags = (@context['current_tags'] - [tag]).uniq tags = (@context['current_tags'] - [tag]).uniq
"<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tags.join("+")}\">#{label}</a>" "<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tags.join("+")}\">#{label}</a>"
end end
end end

View File

@@ -1,5 +1,4 @@
module WeightFilter module WeightFilter
def weight(grams) def weight(grams)
sprintf("%.2f", grams / 1000) sprintf("%.2f", grams / 1000)
end end
@@ -7,5 +6,4 @@ module WeightFilter
def weight_with_unit(grams) def weight_with_unit(grams)
"#{weight(grams)} kg" "#{weight(grams)} kg"
end end
end end

View File

@@ -6,73 +6,115 @@
# Shopify which is likely the biggest user of liquid in the world which something to the tune of several # Shopify which is likely the biggest user of liquid in the world which something to the tune of several
# million Template#render calls a day. # million Template#render calls a day.
require File.dirname(__FILE__) + '/shopify/liquid' require_relative 'shopify/liquid'
require File.dirname(__FILE__) + '/shopify/database.rb' require_relative 'shopify/database'
class ThemeRunner class ThemeRunner
class FileSystem class FileSystem
def initialize(path) def initialize(path)
@path = path @path = path
end end
# Called by Liquid to retrieve a template file # Called by Liquid to retrieve a template file
def read_template_file(template_path, context) def read_template_file(template_path)
File.read(@path + '/' + template_path + '.liquid') File.read(@path + '/' + template_path + '.liquid')
end end
end end
# Load all templates into memory, do this now so that # Initialize a new liquid ThemeRunner instance
# we don't profile IO. # Will load all templates into memory, do this now so that we don't profile IO.
def initialize def initialize
@tests = Dir[File.dirname(__FILE__) + '/tests/**/*.liquid'].collect do |test| @tests = Dir[__dir__ + '/tests/**/*.liquid'].collect do |test|
next if File.basename(test) == 'theme.liquid' next if File.basename(test) == 'theme.liquid'
theme_path = File.dirname(test) + '/theme.liquid' theme_path = File.dirname(test) + '/theme.liquid'
{
[File.read(test), (File.file?(theme_path) ? File.read(theme_path) : nil), test] liquid: File.read(test),
layout: (File.file?(theme_path) ? File.read(theme_path) : nil),
template_name: test
}
end.compact end.compact
compile_all_tests
end end
# `compile` will test just the compilation portion of liquid without any templates
def compile def compile
# Dup assigns because will make some changes to them @tests.each do |test_hash|
Liquid::Template.new.parse(test_hash[:liquid])
@tests.each do |liquid, layout, template_name| Liquid::Template.new.parse(test_hash[:layout])
tmpl = Liquid::Template.new
tmpl.parse(liquid)
tmpl = Liquid::Template.new
tmpl.parse(layout)
end end
end end
def run # `run` is called to benchmark rendering and compiling at the same time
def run
each_test do |liquid, layout, assigns, page_template, template_name|
compile_and_render(liquid, layout, assigns, page_template, template_name)
end
end
# `render` is called to benchmark just the render portion of liquid
def render
@compiled_tests.each do |test|
tmpl = test[:tmpl]
assigns = test[:assigns]
layout = test[:layout]
if layout
assigns['content_for_layout'] = tmpl.render!(assigns)
layout.render!(assigns)
else
tmpl.render!(assigns)
end
end
end
private
def compile_and_render(template, layout, assigns, page_template, template_file)
compiled_test = compile_test(template, layout, assigns, page_template, template_file)
assigns['content_for_layout'] = compiled_test[:tmpl].render!(assigns)
compiled_test[:layout].render!(assigns) if layout
end
def compile_all_tests
@compiled_tests = []
each_test do |liquid, layout, assigns, page_template, template_name|
@compiled_tests << compile_test(liquid, layout, assigns, page_template, template_name)
end
@compiled_tests
end
def compile_test(template, layout, assigns, page_template, template_file)
tmpl = init_template(page_template, template_file)
parsed_template = tmpl.parse(template).dup
if layout
parsed_layout = tmpl.parse(layout)
{ tmpl: parsed_template, assigns: assigns, layout: parsed_layout }
else
{ tmpl: parsed_template, assigns: assigns }
end
end
# utility method with similar functionality needed in `compile_all_tests` and `run`
def each_test
# Dup assigns because will make some changes to them # Dup assigns because will make some changes to them
assigns = Database.tables.dup assigns = Database.tables.dup
@tests.each do |liquid, layout, template_name| @tests.each do |test_hash|
# Compute page_template outside of profiler run, uninteresting to profiler
# Compute page_tempalte outside of profiler run, uninteresting to profiler page_template = File.basename(test_hash[:template_name], File.extname(test_hash[:template_name]))
page_template = File.basename(template_name, File.extname(template_name)) yield(test_hash[:liquid], test_hash[:layout], assigns, page_template, test_hash[:template_name])
compile_and_render(liquid, layout, assigns, page_template, template_name)
end end
end end
# set up a new Liquid::Template object for use in `compile_and_render` and `compile_test`
def compile_and_render(template, layout, assigns, page_template, template_file) def init_template(page_template, template_file)
tmpl = Liquid::Template.new tmpl = Liquid::Template.new
tmpl.assigns['page_title'] = 'Page title' tmpl.assigns['page_title'] = 'Page title'
tmpl.assigns['template'] = page_template tmpl.assigns['template'] = page_template
tmpl.registers[:file_system] = ThemeRunner::FileSystem.new(File.dirname(template_file)) tmpl.registers[:file_system] = ThemeRunner::FileSystem.new(File.dirname(template_file))
tmpl
content_for_layout = tmpl.parse(template).render!(assigns)
if layout
assigns['content_for_layout'] = content_for_layout
tmpl.parse(layout).render!(assigns)
else
content_for_layout
end
end end
end end

View File

@@ -3,26 +3,36 @@ require 'test_helper'
class AssignTest < Minitest::Test class AssignTest < Minitest::Test
include Liquid include Liquid
def test_assign_with_hyphen_in_variable_name
template_source = <<-END_TEMPLATE
{% assign this-thing = 'Print this-thing' %}
{{ this-thing }}
END_TEMPLATE
template = Template.parse(template_source)
rendered = template.render!
assert_equal "Print this-thing", rendered.strip
end
def test_assigned_variable def test_assigned_variable
assert_template_result('.foo.', assert_template_result('.foo.',
'{% assign foo = values %}.{{ foo[0] }}.', '{% assign foo = values %}.{{ foo[0] }}.',
'values' => %w{foo bar baz}) 'values' => %w(foo bar baz))
assert_template_result('.bar.', assert_template_result('.bar.',
'{% assign foo = values %}.{{ foo[1] }}.', '{% assign foo = values %}.{{ foo[1] }}.',
'values' => %w{foo bar baz}) 'values' => %w(foo bar baz))
end end
def test_assign_with_filter def test_assign_with_filter
assert_template_result('.bar.', assert_template_result('.bar.',
'{% assign foo = values | split: "," %}.{{ foo[1] }}.', '{% assign foo = values | split: "," %}.{{ foo[1] }}.',
'values' => "foo,bar,baz") 'values' => "foo,bar,baz")
end end
def test_assign_syntax_error def test_assign_syntax_error
assert_match_syntax_error(/assign/, assert_match_syntax_error(/assign/,
'{% assign foo not values %}.', '{% assign foo not values %}.',
'values' => "foo,bar,baz") 'values' => "foo,bar,baz")
end end
def test_assign_uses_error_mode def test_assign_uses_error_mode

View File

@@ -9,7 +9,7 @@ class FoobarTag < Liquid::Tag
end end
class BlankTestFileSystem class BlankTestFileSystem
def read_template_file(template_path, context) def read_template_file(template_path)
template_path template_path
end end
end end
@@ -31,7 +31,7 @@ class BlankTest < Minitest::Test
end end
def test_new_tags_are_not_blank_by_default def test_new_tags_are_not_blank_by_default
assert_template_result(" "*N, wrap_in_for("{% foobar %}")) assert_template_result(" " * N, wrap_in_for("{% foobar %}"))
end end
def test_loops_are_blank def test_loops_are_blank
@@ -47,7 +47,7 @@ class BlankTest < Minitest::Test
end end
def test_mark_as_blank_only_during_parsing def test_mark_as_blank_only_during_parsing
assert_template_result(" "*(N+1), wrap(" {% if false %} this never happens, but still, this block is not blank {% endif %}")) assert_template_result(" " * (N + 1), wrap(" {% if false %} this never happens, but still, this block is not blank {% endif %}"))
end end
def test_comments_are_blank def test_comments_are_blank
@@ -60,9 +60,9 @@ class BlankTest < Minitest::Test
def test_nested_blocks_are_blank_but_only_if_all_children_are def test_nested_blocks_are_blank_but_only_if_all_children_are
assert_template_result("", wrap(wrap(" "))) assert_template_result("", wrap(wrap(" ")))
assert_template_result("\n but this is not "*(N+1), assert_template_result("\n but this is not " * (N + 1),
wrap(%q{{% if true %} {% comment %} this is blank {% endcomment %} {% endif %} wrap('{% if true %} {% comment %} this is blank {% endcomment %} {% endif %}
{% if true %} but this is not {% endif %}})) {% if true %} but this is not {% endif %}'))
end end
def test_assigns_are_blank def test_assigns_are_blank
@@ -76,31 +76,31 @@ class BlankTest < Minitest::Test
def test_whitespace_is_not_blank_if_other_stuff_is_present def test_whitespace_is_not_blank_if_other_stuff_is_present
body = " x " body = " x "
assert_template_result(body*(N+1), wrap(body)) assert_template_result(body * (N + 1), wrap(body))
end end
def test_increment_is_not_blank def test_increment_is_not_blank
assert_template_result(" 0"*2*(N+1), wrap("{% assign foo = 0 %} {% increment foo %} {% decrement foo %}")) assert_template_result(" 0" * 2 * (N + 1), wrap("{% assign foo = 0 %} {% increment foo %} {% decrement foo %}"))
end end
def test_cycle_is_not_blank def test_cycle_is_not_blank
assert_template_result(" "*((N+1)/2)+" ", wrap("{% cycle ' ', ' ' %}")) assert_template_result(" " * ((N + 1) / 2) + " ", wrap("{% cycle ' ', ' ' %}"))
end end
def test_raw_is_not_blank def test_raw_is_not_blank
assert_template_result(" "*(N+1), wrap(" {% raw %} {% endraw %}")) assert_template_result(" " * (N + 1), wrap(" {% raw %} {% endraw %}"))
end end
def test_include_is_blank def test_include_is_blank
Liquid::Template.file_system = BlankTestFileSystem.new Liquid::Template.file_system = BlankTestFileSystem.new
assert_template_result "foobar"*(N+1), wrap("{% include 'foobar' %}") assert_template_result "foobar" * (N + 1), wrap("{% include 'foobar' %}")
assert_template_result " foobar "*(N+1), wrap("{% include ' foobar ' %}") assert_template_result " foobar " * (N + 1), wrap("{% include ' foobar ' %}")
assert_template_result " "*(N+1), wrap(" {% include ' ' %} ") assert_template_result " " * (N + 1), wrap(" {% include ' ' %} ")
end end
def test_case_is_blank def test_case_is_blank
assert_template_result("", wrap(" {% assign foo = 'bar' %} {% case foo %} {% when 'bar' %} {% when 'whatever' %} {% else %} {% endcase %} ")) assert_template_result("", wrap(" {% assign foo = 'bar' %} {% case foo %} {% when 'bar' %} {% when 'whatever' %} {% else %} {% endcase %} "))
assert_template_result("", wrap(" {% assign foo = 'else' %} {% case foo %} {% when 'bar' %} {% when 'whatever' %} {% else %} {% endcase %} ")) assert_template_result("", wrap(" {% assign foo = 'else' %} {% case foo %} {% when 'bar' %} {% when 'whatever' %} {% else %} {% endcase %} "))
assert_template_result(" x "*(N+1), wrap(" {% assign foo = 'else' %} {% case foo %} {% when 'bar' %} {% when 'whatever' %} {% else %} x {% endcase %} ")) assert_template_result(" x " * (N + 1), wrap(" {% assign foo = 'else' %} {% case foo %} {% when 'bar' %} {% when 'whatever' %} {% else %} x {% endcase %} "))
end end
end end

View File

@@ -0,0 +1,12 @@
require 'test_helper'
class BlockTest < Minitest::Test
include Liquid
def test_unexpected_end_tag
exc = assert_raises(SyntaxError) do
Template.parse("{% if true %}{% endunless %}")
end
assert_equal exc.message, "Liquid syntax error: 'endunless' is not a valid delimiter for if tags. use endif"
end
end

View File

@@ -7,6 +7,16 @@ class CaptureTest < Minitest::Test
assert_template_result("test string", "{% capture 'var' %}test string{% endcapture %}{{var}}", {}) assert_template_result("test string", "{% capture 'var' %}test string{% endcapture %}{{var}}", {})
end end
def test_capture_with_hyphen_in_variable_name
template_source = <<-END_TEMPLATE
{% capture this-thing %}Print this-thing{% endcapture %}
{{ this-thing }}
END_TEMPLATE
template = Template.parse(template_source)
rendered = template.render!
assert_equal "Print this-thing", rendered.strip
end
def test_capture_to_variable_from_outer_scope_if_existing def test_capture_to_variable_from_outer_scope_if_existing
template_source = <<-END_TEMPLATE template_source = <<-END_TEMPLATE
{% assign var = '' %} {% assign var = '' %}

View File

@@ -18,17 +18,15 @@ class ContextTest < Minitest::Test
with_global_filter(global) do with_global_filter(global) do
assert_equal 'Global test', Template.parse("{{'test' | notice }}").render! assert_equal 'Global test', Template.parse("{{'test' | notice }}").render!
assert_equal 'Local test', Template.parse("{{'test' | notice }}").render!({}, :filters => [local]) assert_equal 'Local test', Template.parse("{{'test' | notice }}").render!({}, filters: [local])
end end
end end
def test_has_key_will_not_add_an_error_for_missing_keys def test_has_key_will_not_add_an_error_for_missing_keys
Template.error_mode = :strict with_error_mode :strict do
context = Context.new
context = Context.new context.key?('unknown')
assert_empty context.errors
context.has_key?('unknown') end
assert_empty context.errors
end end
end end

View File

@@ -0,0 +1,19 @@
require 'test_helper'
class DocumentTest < Minitest::Test
include Liquid
def test_unexpected_outer_tag
exc = assert_raises(SyntaxError) do
Template.parse("{% else %}")
end
assert_equal exc.message, "Liquid syntax error: Unexpected outer 'else' tag"
end
def test_unknown_tag
exc = assert_raises(SyntaxError) do
Template.parse("{% foo %}")
end
assert_equal exc.message, "Liquid syntax error: Unknown tag 'foo'"
end
end

View File

@@ -13,13 +13,12 @@ class ContextDrop < Liquid::Drop
@context['forloop.index'] @context['forloop.index']
end end
def before_method(method) def liquid_method_missing(method)
return @context[method] @context[method]
end end
end end
class ProductDrop < Liquid::Drop class ProductDrop < Liquid::Drop
class TextDrop < Liquid::Drop class TextDrop < Liquid::Drop
def array def array
['text1', 'text2'] ['text1', 'text2']
@@ -31,8 +30,8 @@ class ProductDrop < Liquid::Drop
end end
class CatchallDrop < Liquid::Drop class CatchallDrop < Liquid::Drop
def before_method(method) def liquid_method_missing(method)
return 'method: ' << method.to_s 'catchall_method: ' << method.to_s
end end
end end
@@ -48,14 +47,19 @@ class ProductDrop < Liquid::Drop
ContextDrop.new ContextDrop.new
end end
def user_input
"foo".taint
end
protected protected
def callmenot
"protected" def callmenot
end "protected"
end
end end
class EnumerableDrop < Liquid::Drop class EnumerableDrop < Liquid::Drop
def before_method(method) def liquid_method_missing(method)
method method
end end
@@ -89,7 +93,7 @@ end
class RealEnumerableDrop < Liquid::Drop class RealEnumerableDrop < Liquid::Drop
include Enumerable include Enumerable
def before_method(method) def liquid_method_missing(method)
method method
end end
@@ -108,6 +112,32 @@ class DropsTest < Minitest::Test
assert_equal ' ', tpl.render!('product' => ProductDrop.new) assert_equal ' ', tpl.render!('product' => ProductDrop.new)
end end
def test_rendering_raises_on_tainted_attr
with_taint_mode(:error) do
tpl = Liquid::Template.parse('{{ product.user_input }}')
assert_raises TaintedError do
tpl.render!('product' => ProductDrop.new)
end
end
end
def test_rendering_warns_on_tainted_attr
with_taint_mode(:warn) do
tpl = Liquid::Template.parse('{{ product.user_input }}')
context = Context.new('product' => ProductDrop.new)
tpl.render!(context)
assert_equal [Liquid::TaintedError], context.warnings.map(&:class)
assert_equal "variable 'product.user_input' is tainted and was not escaped", context.warnings.first.to_s(false)
end
end
def test_rendering_doesnt_raise_on_escaped_tainted_attr
with_taint_mode(:error) do
tpl = Liquid::Template.parse('{{ product.user_input | escape }}')
tpl.render!('product' => ProductDrop.new)
end
end
def test_drop_does_only_respond_to_whitelisted_methods def test_drop_does_only_respond_to_whitelisted_methods
assert_equal "", Liquid::Template.parse("{{ product.inspect }}").render!('product' => ProductDrop.new) assert_equal "", Liquid::Template.parse("{{ product.inspect }}").render!('product' => ProductDrop.new)
assert_equal "", Liquid::Template.parse("{{ product.pretty_inspect }}").render!('product' => ProductDrop.new) assert_equal "", Liquid::Template.parse("{{ product.pretty_inspect }}").render!('product' => ProductDrop.new)
@@ -123,37 +153,37 @@ class DropsTest < Minitest::Test
end end
def test_text_drop def test_text_drop
output = Liquid::Template.parse( ' {{ product.texts.text }} ' ).render!('product' => ProductDrop.new) output = Liquid::Template.parse(' {{ product.texts.text }} ').render!('product' => ProductDrop.new)
assert_equal ' text1 ', output assert_equal ' text1 ', output
end end
def test_unknown_method def test_catchall_unknown_method
output = Liquid::Template.parse( ' {{ product.catchall.unknown }} ' ).render!('product' => ProductDrop.new) output = Liquid::Template.parse(' {{ product.catchall.unknown }} ').render!('product' => ProductDrop.new)
assert_equal ' method: unknown ', output assert_equal ' catchall_method: unknown ', output
end end
def test_integer_argument_drop def test_catchall_integer_argument_drop
output = Liquid::Template.parse( ' {{ product.catchall[8] }} ' ).render!('product' => ProductDrop.new) output = Liquid::Template.parse(' {{ product.catchall[8] }} ').render!('product' => ProductDrop.new)
assert_equal ' method: 8 ', output assert_equal ' catchall_method: 8 ', output
end end
def test_text_array_drop def test_text_array_drop
output = Liquid::Template.parse( '{% for text in product.texts.array %} {{text}} {% endfor %}' ).render!('product' => ProductDrop.new) output = Liquid::Template.parse('{% for text in product.texts.array %} {{text}} {% endfor %}').render!('product' => ProductDrop.new)
assert_equal ' text1 text2 ', output assert_equal ' text1 text2 ', output
end end
def test_context_drop def test_context_drop
output = Liquid::Template.parse( ' {{ context.bar }} ' ).render!('context' => ContextDrop.new, 'bar' => "carrot") output = Liquid::Template.parse(' {{ context.bar }} ').render!('context' => ContextDrop.new, 'bar' => "carrot")
assert_equal ' carrot ', output assert_equal ' carrot ', output
end end
def test_nested_context_drop def test_nested_context_drop
output = Liquid::Template.parse( ' {{ product.context.foo }} ' ).render!('product' => ProductDrop.new, 'foo' => "monkey") output = Liquid::Template.parse(' {{ product.context.foo }} ').render!('product' => ProductDrop.new, 'foo' => "monkey")
assert_equal ' monkey ', output assert_equal ' monkey ', output
end end
def test_protected def test_protected
output = Liquid::Template.parse( ' {{ product.callmenot }} ' ).render!('product' => ProductDrop.new) output = Liquid::Template.parse(' {{ product.callmenot }} ').render!('product' => ProductDrop.new)
assert_equal ' ', output assert_equal ' ', output
end end
@@ -165,43 +195,43 @@ class DropsTest < Minitest::Test
end end
def test_scope def test_scope
assert_equal '1', Liquid::Template.parse( '{{ context.scopes }}' ).render!('context' => ContextDrop.new) assert_equal '1', Liquid::Template.parse('{{ context.scopes }}').render!('context' => ContextDrop.new)
assert_equal '2', Liquid::Template.parse( '{%for i in dummy%}{{ context.scopes }}{%endfor%}' ).render!('context' => ContextDrop.new, 'dummy' => [1]) assert_equal '2', Liquid::Template.parse('{%for i in dummy%}{{ context.scopes }}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1])
assert_equal '3', Liquid::Template.parse( '{%for i in dummy%}{%for i in dummy%}{{ context.scopes }}{%endfor%}{%endfor%}' ).render!('context' => ContextDrop.new, 'dummy' => [1]) assert_equal '3', Liquid::Template.parse('{%for i in dummy%}{%for i in dummy%}{{ context.scopes }}{%endfor%}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1])
end end
def test_scope_though_proc def test_scope_though_proc
assert_equal '1', Liquid::Template.parse( '{{ s }}' ).render!('context' => ContextDrop.new, 's' => Proc.new{|c| c['context.scopes'] }) assert_equal '1', Liquid::Template.parse('{{ s }}').render!('context' => ContextDrop.new, 's' => proc{ |c| c['context.scopes'] })
assert_equal '2', Liquid::Template.parse( '{%for i in dummy%}{{ s }}{%endfor%}' ).render!('context' => ContextDrop.new, 's' => Proc.new{|c| c['context.scopes'] }, 'dummy' => [1]) assert_equal '2', Liquid::Template.parse('{%for i in dummy%}{{ s }}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc{ |c| c['context.scopes'] }, 'dummy' => [1])
assert_equal '3', Liquid::Template.parse( '{%for i in dummy%}{%for i in dummy%}{{ s }}{%endfor%}{%endfor%}' ).render!('context' => ContextDrop.new, 's' => Proc.new{|c| c['context.scopes'] }, 'dummy' => [1]) assert_equal '3', Liquid::Template.parse('{%for i in dummy%}{%for i in dummy%}{{ s }}{%endfor%}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc{ |c| c['context.scopes'] }, 'dummy' => [1])
end end
def test_scope_with_assigns def test_scope_with_assigns
assert_equal 'variable', Liquid::Template.parse( '{% assign a = "variable"%}{{a}}' ).render!('context' => ContextDrop.new) assert_equal 'variable', Liquid::Template.parse('{% assign a = "variable"%}{{a}}').render!('context' => ContextDrop.new)
assert_equal 'variable', Liquid::Template.parse( '{% assign a = "variable"%}{%for i in dummy%}{{a}}{%endfor%}' ).render!('context' => ContextDrop.new, 'dummy' => [1]) assert_equal 'variable', Liquid::Template.parse('{% assign a = "variable"%}{%for i in dummy%}{{a}}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1])
assert_equal 'test', Liquid::Template.parse( '{% assign header_gif = "test"%}{{header_gif}}' ).render!('context' => ContextDrop.new) assert_equal 'test', Liquid::Template.parse('{% assign header_gif = "test"%}{{header_gif}}').render!('context' => ContextDrop.new)
assert_equal 'test', Liquid::Template.parse( "{% assign header_gif = 'test'%}{{header_gif}}" ).render!('context' => ContextDrop.new) assert_equal 'test', Liquid::Template.parse("{% assign header_gif = 'test'%}{{header_gif}}").render!('context' => ContextDrop.new)
end end
def test_scope_from_tags def test_scope_from_tags
assert_equal '1', Liquid::Template.parse( '{% for i in context.scopes_as_array %}{{i}}{% endfor %}' ).render!('context' => ContextDrop.new, 'dummy' => [1]) assert_equal '1', Liquid::Template.parse('{% for i in context.scopes_as_array %}{{i}}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1])
assert_equal '12', Liquid::Template.parse( '{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}' ).render!('context' => ContextDrop.new, 'dummy' => [1]) assert_equal '12', Liquid::Template.parse('{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1])
assert_equal '123', Liquid::Template.parse( '{%for a in dummy%}{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}{% endfor %}' ).render!('context' => ContextDrop.new, 'dummy' => [1]) assert_equal '123', Liquid::Template.parse('{%for a in dummy%}{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1])
end end
def test_access_context_from_drop def test_access_context_from_drop
assert_equal '123', Liquid::Template.parse( '{%for a in dummy%}{{ context.loop_pos }}{% endfor %}' ).render!('context' => ContextDrop.new, 'dummy' => [1,2,3]) assert_equal '123', Liquid::Template.parse('{%for a in dummy%}{{ context.loop_pos }}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1, 2, 3])
end end
def test_enumerable_drop def test_enumerable_drop
assert_equal '123', Liquid::Template.parse( '{% for c in collection %}{{c}}{% endfor %}').render!('collection' => EnumerableDrop.new) assert_equal '123', Liquid::Template.parse('{% for c in collection %}{{c}}{% endfor %}').render!('collection' => EnumerableDrop.new)
end end
def test_enumerable_drop_size def test_enumerable_drop_size
assert_equal '3', Liquid::Template.parse( '{{collection.size}}').render!('collection' => EnumerableDrop.new) assert_equal '3', Liquid::Template.parse('{{collection.size}}').render!('collection' => EnumerableDrop.new)
end end
def test_enumerable_drop_will_invoke_before_method_for_clashing_method_names def test_enumerable_drop_will_invoke_liquid_method_missing_for_clashing_method_names
["select", "each", "map", "cycle"].each do |method| ["select", "each", "map", "cycle"].each do |method|
assert_equal method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new) assert_equal method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new)
assert_equal method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new) assert_equal method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new)

View File

@@ -1,24 +1,5 @@
require 'test_helper' require 'test_helper'
class ErrorDrop < Liquid::Drop
def standard_error
raise Liquid::StandardError, 'standard error'
end
def argument_error
raise Liquid::ArgumentError, 'argument error'
end
def syntax_error
raise Liquid::SyntaxError, 'syntax error'
end
def exception
raise Exception, 'exception'
end
end
class ErrorHandlingTest < Minitest::Test class ErrorHandlingTest < Minitest::Test
include Liquid include Liquid
@@ -56,7 +37,7 @@ class ErrorHandlingTest < Minitest::Test
end end
def test_standard_error def test_standard_error
template = Liquid::Template.parse( ' {{ errors.standard_error }} ' ) template = Liquid::Template.parse(' {{ errors.standard_error }} ')
assert_equal ' Liquid error: standard error ', template.render('errors' => ErrorDrop.new) assert_equal ' Liquid error: standard error ', template.render('errors' => ErrorDrop.new)
assert_equal 1, template.errors.size assert_equal 1, template.errors.size
@@ -64,7 +45,7 @@ class ErrorHandlingTest < Minitest::Test
end end
def test_syntax def test_syntax
template = Liquid::Template.parse( ' {{ errors.syntax_error }} ' ) template = Liquid::Template.parse(' {{ errors.syntax_error }} ')
assert_equal ' Liquid syntax error: syntax error ', template.render('errors' => ErrorDrop.new) assert_equal ' Liquid syntax error: syntax error ', template.render('errors' => ErrorDrop.new)
assert_equal 1, template.errors.size assert_equal 1, template.errors.size
@@ -72,7 +53,7 @@ class ErrorHandlingTest < Minitest::Test
end end
def test_argument def test_argument
template = Liquid::Template.parse( ' {{ errors.argument_error }} ' ) template = Liquid::Template.parse(' {{ errors.argument_error }} ')
assert_equal ' Liquid error: argument error ', template.render('errors' => ErrorDrop.new) assert_equal ' Liquid error: argument error ', template.render('errors' => ErrorDrop.new)
assert_equal 1, template.errors.size assert_equal 1, template.errors.size
@@ -94,26 +75,109 @@ class ErrorHandlingTest < Minitest::Test
end end
def test_lax_unrecognized_operator def test_lax_unrecognized_operator
template = Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', :error_mode => :lax) template = Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', error_mode: :lax)
assert_equal ' Liquid error: Unknown operator =! ', template.render assert_equal ' Liquid error: Unknown operator =! ', template.render
assert_equal 1, template.errors.size assert_equal 1, template.errors.size
assert_equal Liquid::ArgumentError, template.errors.first.class assert_equal Liquid::ArgumentError, template.errors.first.class
end end
def test_with_line_numbers_adds_numbers_to_parser_errors
err = assert_raises(SyntaxError) do
Liquid::Template.parse(%q(
foobar
{% "cat" | foobar %}
bla
),
line_numbers: true
)
end
assert_match(/Liquid syntax error \(line 4\)/, err.message)
end
def test_with_line_numbers_adds_numbers_to_parser_errors_with_whitespace_trim
err = assert_raises(SyntaxError) do
Liquid::Template.parse(%q(
foobar
{%- "cat" | foobar -%}
bla
),
line_numbers: true
)
end
assert_match(/Liquid syntax error \(line 4\)/, err.message)
end
def test_parsing_warn_with_line_numbers_adds_numbers_to_lexer_errors
template = Liquid::Template.parse('
foobar
{% if 1 =! 2 %}ok{% endif %}
bla
',
error_mode: :warn,
line_numbers: true
)
assert_equal ['Liquid syntax error (line 4): Unexpected character = in "1 =! 2"'],
template.warnings.map(&:message)
end
def test_parsing_strict_with_line_numbers_adds_numbers_to_lexer_errors
err = assert_raises(SyntaxError) do
Liquid::Template.parse('
foobar
{% if 1 =! 2 %}ok{% endif %}
bla
',
error_mode: :strict,
line_numbers: true
)
end
assert_equal 'Liquid syntax error (line 4): Unexpected character = in "1 =! 2"', err.message
end
def test_syntax_errors_in_nested_blocks_have_correct_line_number
err = assert_raises(SyntaxError) do
Liquid::Template.parse('
foobar
{% if 1 != 2 %}
{% foo %}
{% endif %}
bla
',
line_numbers: true
)
end
assert_equal "Liquid syntax error (line 5): Unknown tag 'foo'", err.message
end
def test_strict_error_messages def test_strict_error_messages
err = assert_raises(SyntaxError) do err = assert_raises(SyntaxError) do
Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', :error_mode => :strict) Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', error_mode: :strict)
end end
assert_equal 'Liquid syntax error: Unexpected character = in "1 =! 2"', err.message assert_equal 'Liquid syntax error: Unexpected character = in "1 =! 2"', err.message
err = assert_raises(SyntaxError) do err = assert_raises(SyntaxError) do
Liquid::Template.parse('{{%%%}}', :error_mode => :strict) Liquid::Template.parse('{{%%%}}', error_mode: :strict)
end end
assert_equal 'Liquid syntax error: Unexpected character % in "{{%%%}}"', err.message assert_equal 'Liquid syntax error: Unexpected character % in "{{%%%}}"', err.message
end end
def test_warnings def test_warnings
template = Liquid::Template.parse('{% if ~~~ %}{{%%%}}{% else %}{{ hello. }}{% endif %}', :error_mode => :warn) template = Liquid::Template.parse('{% if ~~~ %}{{%%%}}{% else %}{{ hello. }}{% endif %}', error_mode: :warn)
assert_equal 3, template.warnings.size assert_equal 3, template.warnings.size
assert_equal 'Unexpected character ~ in "~~~"', template.warnings[0].to_s(false) assert_equal 'Unexpected character ~ in "~~~"', template.warnings[0].to_s(false)
assert_equal 'Unexpected character % in "{{%%%}}"', template.warnings[1].to_s(false) assert_equal 'Unexpected character % in "{{%%%}}"', template.warnings[1].to_s(false)
@@ -122,12 +186,12 @@ class ErrorHandlingTest < Minitest::Test
end end
def test_warning_line_numbers def test_warning_line_numbers
template = Liquid::Template.parse("{% if ~~~ %}\n{{%%%}}{% else %}\n{{ hello. }}{% endif %}", :error_mode => :warn, :line_numbers => true) template = Liquid::Template.parse("{% if ~~~ %}\n{{%%%}}{% else %}\n{{ hello. }}{% endif %}", error_mode: :warn, line_numbers: true)
assert_equal 'Liquid syntax error (line 1): Unexpected character ~ in "~~~"', template.warnings[0].message assert_equal 'Liquid syntax error (line 1): Unexpected character ~ in "~~~"', template.warnings[0].message
assert_equal 'Liquid syntax error (line 2): Unexpected character % in "{{%%%}}"', template.warnings[1].message assert_equal 'Liquid syntax error (line 2): Unexpected character % in "{{%%%}}"', template.warnings[1].message
assert_equal 'Liquid syntax error (line 3): Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].message assert_equal 'Liquid syntax error (line 3): Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].message
assert_equal 3, template.warnings.size assert_equal 3, template.warnings.size
assert_equal [1,2,3], template.warnings.map(&:line_number) assert_equal [1, 2, 3], template.warnings.map(&:line_number)
end end
# Liquid should not catch Exceptions that are not subclasses of StandardError, like Interrupt and NoMemoryError # Liquid should not catch Exceptions that are not subclasses of StandardError, like Interrupt and NoMemoryError
@@ -137,4 +201,60 @@ class ErrorHandlingTest < Minitest::Test
template.render('errors' => ErrorDrop.new) template.render('errors' => ErrorDrop.new)
end end
end end
def test_default_exception_renderer_with_internal_error
template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true)
output = template.render({ 'errors' => ErrorDrop.new })
assert_equal 'This is a runtime error: Liquid error (line 1): internal', output
assert_equal [Liquid::InternalError], template.errors.map(&:class)
end
def test_setting_default_exception_renderer
old_exception_renderer = Liquid::Template.default_exception_renderer
exceptions = []
Liquid::Template.default_exception_renderer = ->(e) { exceptions << e; '' }
template = Liquid::Template.parse('This is a runtime error: {{ errors.argument_error }}')
output = template.render({ 'errors' => ErrorDrop.new })
assert_equal 'This is a runtime error: ', output
assert_equal [Liquid::ArgumentError], template.errors.map(&:class)
ensure
Liquid::Template.default_exception_renderer = old_exception_renderer if old_exception_renderer
end
def test_exception_renderer_exposing_non_liquid_error
template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true)
exceptions = []
handler = ->(e) { exceptions << e; e.cause }
output = template.render({ 'errors' => ErrorDrop.new }, exception_renderer: handler)
assert_equal 'This is a runtime error: runtime error', output
assert_equal [Liquid::InternalError], exceptions.map(&:class)
assert_equal exceptions, template.errors
assert_equal '#<RuntimeError: runtime error>', exceptions.first.cause.inspect
end
class TestFileSystem
def read_template_file(template_path)
"{{ errors.argument_error }}"
end
end
def test_included_template_name_with_line_numbers
old_file_system = Liquid::Template.file_system
begin
Liquid::Template.file_system = TestFileSystem.new
template = Liquid::Template.parse("Argument error:\n{% include 'product' %}", line_numbers: true)
page = template.render('errors' => ErrorDrop.new)
ensure
Liquid::Template.file_system = old_file_system
end
assert_equal "Argument error:\nLiquid error (product line 1): argument error", page
assert_equal "product", template.errors.first.template_name
end
end end

View File

@@ -17,7 +17,7 @@ module CanadianMoneyFilter
end end
module SubstituteFilter module SubstituteFilter
def substitute(input, params={}) def substitute(input, params = {})
input.gsub(/%\{(\w+)\}/) { |match| params[$1] } input.gsub(/%\{(\w+)\}/) { |match| params[$1] }
end end
end end
@@ -25,6 +25,12 @@ end
class FiltersTest < Minitest::Test class FiltersTest < Minitest::Test
include Liquid include Liquid
module OverrideObjectMethodFilter
def tap(input)
"tap overridden"
end
end
def setup def setup
@context = Context.new @context = Context.new
end end
@@ -33,13 +39,13 @@ class FiltersTest < Minitest::Test
@context['var'] = 1000 @context['var'] = 1000
@context.add_filters(MoneyFilter) @context.add_filters(MoneyFilter)
assert_equal ' 1000$ ', Variable.new("var | money").render(@context) assert_equal ' 1000$ ', Template.parse("{{var | money}}").render(@context)
end end
def test_underscore_in_filter_name def test_underscore_in_filter_name
@context['var'] = 1000 @context['var'] = 1000
@context.add_filters(MoneyFilter) @context.add_filters(MoneyFilter)
assert_equal ' 1000$ ', Variable.new("var | money_with_underscore").render(@context) assert_equal ' 1000$ ', Template.parse("{{var | money_with_underscore}}").render(@context)
end end
def test_second_filter_overwrites_first def test_second_filter_overwrites_first
@@ -47,64 +53,104 @@ class FiltersTest < Minitest::Test
@context.add_filters(MoneyFilter) @context.add_filters(MoneyFilter)
@context.add_filters(CanadianMoneyFilter) @context.add_filters(CanadianMoneyFilter)
assert_equal ' 1000$ CAD ', Variable.new("var | money").render(@context) assert_equal ' 1000$ CAD ', Template.parse("{{var | money}}").render(@context)
end end
def test_size def test_size
@context['var'] = 'abcd' @context['var'] = 'abcd'
@context.add_filters(MoneyFilter) @context.add_filters(MoneyFilter)
assert_equal 4, Variable.new("var | size").render(@context) assert_equal '4', Template.parse("{{var | size}}").render(@context)
end end
def test_join def test_join
@context['var'] = [1,2,3,4] @context['var'] = [1, 2, 3, 4]
assert_equal "1 2 3 4", Variable.new("var | join").render(@context) assert_equal "1 2 3 4", Template.parse("{{var | join}}").render(@context)
end end
def test_sort def test_sort
@context['value'] = 3 @context['value'] = 3
@context['numbers'] = [2,1,4,3] @context['numbers'] = [2, 1, 4, 3]
@context['words'] = ['expected', 'as', 'alphabetic'] @context['words'] = ['expected', 'as', 'alphabetic']
@context['arrays'] = ['flower', 'are'] @context['arrays'] = ['flower', 'are']
@context['case_sensitive'] = ['sensitive', 'Expected', 'case']
assert_equal [1,2,3,4], Variable.new("numbers | sort").render(@context) assert_equal '1 2 3 4', Template.parse("{{numbers | sort | join}}").render(@context)
assert_equal ['alphabetic', 'as', 'expected'], Variable.new("words | sort").render(@context) assert_equal 'alphabetic as expected', Template.parse("{{words | sort | join}}").render(@context)
assert_equal [3], Variable.new("value | sort").render(@context) assert_equal '3', Template.parse("{{value | sort}}").render(@context)
assert_equal ['are', 'flower'], Variable.new("arrays | sort").render(@context) assert_equal 'are flower', Template.parse("{{arrays | sort | join}}").render(@context)
assert_equal 'Expected case sensitive', Template.parse("{{case_sensitive | sort | join}}").render(@context)
end
def test_sort_natural
@context['words'] = ['case', 'Assert', 'Insensitive']
@context['hashes'] = [{ 'a' => 'A' }, { 'a' => 'b' }, { 'a' => 'C' }]
@context['objects'] = [TestObject.new('A'), TestObject.new('b'), TestObject.new('C')]
# Test strings
assert_equal 'Assert case Insensitive', Template.parse("{{words | sort_natural | join}}").render(@context)
# Test hashes
assert_equal 'A b C', Template.parse("{{hashes | sort_natural: 'a' | map: 'a' | join}}").render(@context)
# Test objects
assert_equal 'A b C', Template.parse("{{objects | sort_natural: 'a' | map: 'a' | join}}").render(@context)
end
def test_compact
@context['words'] = ['a', nil, 'b', nil, 'c']
@context['hashes'] = [{ 'a' => 'A' }, { 'a' => nil }, { 'a' => 'C' }]
@context['objects'] = [TestObject.new('A'), TestObject.new(nil), TestObject.new('C')]
# Test strings
assert_equal 'a b c', Template.parse("{{words | compact | join}}").render(@context)
# Test hashes
assert_equal 'A C', Template.parse("{{hashes | compact: 'a' | map: 'a' | join}}").render(@context)
# Test objects
assert_equal 'A C', Template.parse("{{objects | compact: 'a' | map: 'a' | join}}").render(@context)
end end
def test_strip_html def test_strip_html
@context['var'] = "<b>bla blub</a>" @context['var'] = "<b>bla blub</a>"
assert_equal "bla blub", Variable.new("var | strip_html").render(@context) assert_equal "bla blub", Template.parse("{{ var | strip_html }}").render(@context)
end end
def test_strip_html_ignore_comments_with_html def test_strip_html_ignore_comments_with_html
@context['var'] = "<!-- split and some <ul> tag --><b>bla blub</a>" @context['var'] = "<!-- split and some <ul> tag --><b>bla blub</a>"
assert_equal "bla blub", Variable.new("var | strip_html").render(@context) assert_equal "bla blub", Template.parse("{{ var | strip_html }}").render(@context)
end end
def test_capitalize def test_capitalize
@context['var'] = "blub" @context['var'] = "blub"
assert_equal "Blub", Variable.new("var | capitalize").render(@context) assert_equal "Blub", Template.parse("{{ var | capitalize }}").render(@context)
end end
def test_nonexistent_filter_is_ignored def test_nonexistent_filter_is_ignored
@context['var'] = 1000 @context['var'] = 1000
assert_equal 1000, Variable.new("var | xyzzy").render(@context) assert_equal '1000', Template.parse("{{ var | xyzzy }}").render(@context)
end end
def test_filter_with_keyword_arguments def test_filter_with_keyword_arguments
@context['surname'] = 'john' @context['surname'] = 'john'
@context['input'] = 'hello %{first_name}, %{last_name}'
@context.add_filters(SubstituteFilter) @context.add_filters(SubstituteFilter)
output = Variable.new(%! 'hello %{first_name}, %{last_name}' | substitute: first_name: surname, last_name: 'doe' !).render(@context) output = Template.parse(%({{ input | substitute: first_name: surname, last_name: 'doe' }})).render(@context)
assert_equal 'hello john, doe', output assert_equal 'hello john, doe', output
end end
def test_override_object_method_in_filter
assert_equal "tap overridden", Template.parse("{{var | tap}}").render!({ 'var' => 1000 }, filters: [OverrideObjectMethodFilter])
# tap still treated as a non-existent filter
assert_equal "1000", Template.parse("{{var | tap}}").render!({ 'var' => 1000 })
end
end end
class FiltersInTemplate < Minitest::Test class FiltersInTemplate < Minitest::Test
@@ -113,8 +159,8 @@ class FiltersInTemplate < Minitest::Test
def test_local_global def test_local_global
with_global_filter(MoneyFilter) do with_global_filter(MoneyFilter) do
assert_equal " 1000$ ", Template.parse("{{1000 | money}}").render!(nil, nil) assert_equal " 1000$ ", Template.parse("{{1000 | money}}").render!(nil, nil)
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, :filters => CanadianMoneyFilter) assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, filters: CanadianMoneyFilter)
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, :filters => [CanadianMoneyFilter]) assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, filters: [CanadianMoneyFilter])
end end
end end
@@ -123,3 +169,10 @@ class FiltersInTemplate < Minitest::Test
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, [CanadianMoneyFilter]) assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, [CanadianMoneyFilter])
end end
end # FiltersTest end # FiltersTest
class TestObject < Liquid::Drop
attr_accessor :a
def initialize(a)
@a = a
end
end

View File

@@ -1,18 +1,18 @@
require 'test_helper' require 'test_helper'
module MoneyFilter
def money(input)
sprintf(' %d$ ', input)
end
end
module CanadianMoneyFilter
def money(input)
sprintf(' %d$ CAD ', input)
end
end
class HashOrderingTest < Minitest::Test class HashOrderingTest < Minitest::Test
module MoneyFilter
def money(input)
sprintf(' %d$ ', input)
end
end
module CanadianMoneyFilter
def money(input)
sprintf(' %d$ CAD ', input)
end
end
include Liquid include Liquid
def test_global_register_order def test_global_register_order

View File

@@ -14,7 +14,7 @@ module FunnyFilter
end end
def add_tag(input, tag = "p", id = "foo") def add_tag(input, tag = "p", id = "foo")
%|<#{tag} id="#{id}">#{input}</#{tag}>| %(<#{tag} id="#{id}">#{input}</#{tag}>)
end end
def paragraph(input) def paragraph(input)
@@ -22,9 +22,8 @@ module FunnyFilter
end end
def link_to(name, url) def link_to(name, url)
%|<a href="#{url}">#{name}</a>| %(<a href="#{url}">#{name}</a>)
end end
end end
class OutputTest < Minitest::Test class OutputTest < Minitest::Test
@@ -33,84 +32,92 @@ class OutputTest < Minitest::Test
def setup def setup
@assigns = { @assigns = {
'best_cars' => 'bmw', 'best_cars' => 'bmw',
'car' => {'bmw' => 'good', 'gm' => 'bad'} 'car' => { 'bmw' => 'good', 'gm' => 'bad' }
} }
end end
def test_variable def test_variable
text = %| {{best_cars}} | text = %( {{best_cars}} )
expected = %| bmw | expected = %( bmw )
assert_equal expected, Template.parse(text).render!(@assigns) assert_equal expected, Template.parse(text).render!(@assigns)
end end
def test_variable_traversing def test_variable_traversing_with_two_brackets
text = %| {{car.bmw}} {{car.gm}} {{car.bmw}} | text = %({{ site.data.menu[include.menu][include.locale] }})
assert_equal "it works!", Template.parse(text).render!(
"site" => { "data" => { "menu" => { "foo" => { "bar" => "it works!" } } } },
"include" => { "menu" => "foo", "locale" => "bar" }
)
end
expected = %| good bad good | def test_variable_traversing
text = %( {{car.bmw}} {{car.gm}} {{car.bmw}} )
expected = %( good bad good )
assert_equal expected, Template.parse(text).render!(@assigns) assert_equal expected, Template.parse(text).render!(@assigns)
end end
def test_variable_piping def test_variable_piping
text = %( {{ car.gm | make_funny }} ) text = %( {{ car.gm | make_funny }} )
expected = %| LOL | expected = %( LOL )
assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter]) assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
end end
def test_variable_piping_with_input def test_variable_piping_with_input
text = %( {{ car.gm | cite_funny }} ) text = %( {{ car.gm | cite_funny }} )
expected = %| LOL: bad | expected = %( LOL: bad )
assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter]) assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
end end
def test_variable_piping_with_args def test_variable_piping_with_args
text = %! {{ car.gm | add_smiley : ':-(' }} ! text = %! {{ car.gm | add_smiley : ':-(' }} !
expected = %| bad :-( | expected = %| bad :-( |
assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter]) assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
end end
def test_variable_piping_with_no_args def test_variable_piping_with_no_args
text = %! {{ car.gm | add_smiley }} ! text = %( {{ car.gm | add_smiley }} )
expected = %| bad :-) | expected = %| bad :-) |
assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter]) assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
end end
def test_multiple_variable_piping_with_args def test_multiple_variable_piping_with_args
text = %! {{ car.gm | add_smiley : ':-(' | add_smiley : ':-('}} ! text = %! {{ car.gm | add_smiley : ':-(' | add_smiley : ':-('}} !
expected = %| bad :-( :-( | expected = %| bad :-( :-( |
assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter]) assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
end end
def test_variable_piping_with_multiple_args def test_variable_piping_with_multiple_args
text = %! {{ car.gm | add_tag : 'span', 'bar'}} ! text = %( {{ car.gm | add_tag : 'span', 'bar'}} )
expected = %| <span id="bar">bad</span> | expected = %( <span id="bar">bad</span> )
assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter]) assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
end end
def test_variable_piping_with_variable_args def test_variable_piping_with_variable_args
text = %! {{ car.gm | add_tag : 'span', car.bmw}} ! text = %( {{ car.gm | add_tag : 'span', car.bmw}} )
expected = %| <span id="good">bad</span> | expected = %( <span id="good">bad</span> )
assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter]) assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
end end
def test_multiple_pipings def test_multiple_pipings
text = %( {{ best_cars | cite_funny | paragraph }} ) text = %( {{ best_cars | cite_funny | paragraph }} )
expected = %| <p>LOL: bmw</p> | expected = %( <p>LOL: bmw</p> )
assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter]) assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
end end
def test_link_to def test_link_to
text = %( {{ 'Typo' | link_to: 'http://typo.leetsoft.com' }} ) text = %( {{ 'Typo' | link_to: 'http://typo.leetsoft.com' }} )
expected = %| <a href="http://typo.leetsoft.com">Typo</a> | expected = %( <a href="http://typo.leetsoft.com">Typo</a> )
assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter]) assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
end end
end # OutputTest end # OutputTest

View File

@@ -0,0 +1,247 @@
# frozen_string_literal: true
require 'test_helper'
class ParseTreeVisitorTest < Minitest::Test
include Liquid
def test_variable
assert_equal(
["test"],
visit(%({{ test }}))
)
end
def test_varible_with_filter
assert_equal(
["test", "infilter"],
visit(%({{ test | split: infilter }}))
)
end
def test_dynamic_variable
assert_equal(
["test", "inlookup"],
visit(%({{ test[inlookup] }}))
)
end
def test_if_condition
assert_equal(
["test"],
visit(%({% if test %}{% endif %}))
)
end
def test_complex_if_condition
assert_equal(
["test"],
visit(%({% if 1 == 1 and 2 == test %}{% endif %}))
)
end
def test_if_body
assert_equal(
["test"],
visit(%({% if 1 == 1 %}{{ test }}{% endif %}))
)
end
def test_unless_condition
assert_equal(
["test"],
visit(%({% unless test %}{% endunless %}))
)
end
def test_complex_unless_condition
assert_equal(
["test"],
visit(%({% unless 1 == 1 and 2 == test %}{% endunless %}))
)
end
def test_unless_body
assert_equal(
["test"],
visit(%({% unless 1 == 1 %}{{ test }}{% endunless %}))
)
end
def test_elsif_condition
assert_equal(
["test"],
visit(%({% if 1 == 1 %}{% elsif test %}{% endif %}))
)
end
def test_complex_elsif_condition
assert_equal(
["test"],
visit(%({% if 1 == 1 %}{% elsif 1 == 1 and 2 == test %}{% endif %}))
)
end
def test_elsif_body
assert_equal(
["test"],
visit(%({% if 1 == 1 %}{% elsif 2 == 2 %}{{ test }}{% endif %}))
)
end
def test_else_body
assert_equal(
["test"],
visit(%({% if 1 == 1 %}{% else %}{{ test }}{% endif %}))
)
end
def test_case_left
assert_equal(
["test"],
visit(%({% case test %}{% endcase %}))
)
end
def test_case_condition
assert_equal(
["test"],
visit(%({% case 1 %}{% when test %}{% endcase %}))
)
end
def test_case_when_body
assert_equal(
["test"],
visit(%({% case 1 %}{% when 2 %}{{ test }}{% endcase %}))
)
end
def test_case_else_body
assert_equal(
["test"],
visit(%({% case 1 %}{% else %}{{ test }}{% endcase %}))
)
end
def test_for_in
assert_equal(
["test"],
visit(%({% for x in test %}{% endfor %}))
)
end
def test_for_limit
assert_equal(
["test"],
visit(%({% for x in (1..5) limit: test %}{% endfor %}))
)
end
def test_for_offset
assert_equal(
["test"],
visit(%({% for x in (1..5) offset: test %}{% endfor %}))
)
end
def test_for_body
assert_equal(
["test"],
visit(%({% for x in (1..5) %}{{ test }}{% endfor %}))
)
end
def test_tablerow_in
assert_equal(
["test"],
visit(%({% tablerow x in test %}{% endtablerow %}))
)
end
def test_tablerow_limit
assert_equal(
["test"],
visit(%({% tablerow x in (1..5) limit: test %}{% endtablerow %}))
)
end
def test_tablerow_offset
assert_equal(
["test"],
visit(%({% tablerow x in (1..5) offset: test %}{% endtablerow %}))
)
end
def test_tablerow_body
assert_equal(
["test"],
visit(%({% tablerow x in (1..5) %}{{ test }}{% endtablerow %}))
)
end
def test_cycle
assert_equal(
["test"],
visit(%({% cycle test %}))
)
end
def test_assign
assert_equal(
["test"],
visit(%({% assign x = test %}))
)
end
def test_capture
assert_equal(
["test"],
visit(%({% capture x %}{{ test }}{% endcapture %}))
)
end
def test_include
assert_equal(
["test"],
visit(%({% include test %}))
)
end
def test_include_with
assert_equal(
["test"],
visit(%({% include "hai" with test %}))
)
end
def test_include_for
assert_equal(
["test"],
visit(%({% include "hai" for test %}))
)
end
def test_preserve_tree_structure
assert_equal(
[[nil, [
[nil, [[nil, [["other", []]]]]],
["test", []],
["xs", []]
]]],
traversal(%({% for x in xs offset: test %}{{ other }}{% endfor %})).visit
)
end
private
def traversal(template)
ParseTreeVisitor
.for(Template.parse(template).root)
.add_callback_for(VariableLookup, &:name)
end
def visit(template)
traversal(template).visit.flatten.compact
end
end

View File

@@ -28,11 +28,14 @@ class ParsingQuirksTest < Minitest::Test
def test_error_on_empty_filter def test_error_on_empty_filter
assert Template.parse("{{test}}") assert Template.parse("{{test}}")
assert Template.parse("{{|test}}")
with_error_mode(:lax) do
assert Template.parse("{{|test}}")
end
with_error_mode(:strict) do with_error_mode(:strict) do
assert_raises(SyntaxError) do assert_raises(SyntaxError) { Template.parse("{{|test}}") }
Template.parse("{{test |a|b|}}") assert_raises(SyntaxError) { Template.parse("{{test |a|b|}}") }
end
end end
end end
@@ -59,25 +62,25 @@ class ParsingQuirksTest < Minitest::Test
end end
def test_no_error_on_lax_empty_filter def test_no_error_on_lax_empty_filter
assert Template.parse("{{test |a|b|}}", :error_mode => :lax) assert Template.parse("{{test |a|b|}}", error_mode: :lax)
assert Template.parse("{{test}}", :error_mode => :lax) assert Template.parse("{{test}}", error_mode: :lax)
assert Template.parse("{{|test|}}", :error_mode => :lax) assert Template.parse("{{|test|}}", error_mode: :lax)
end end
def test_meaningless_parens_lax def test_meaningless_parens_lax
with_error_mode(:lax) do with_error_mode(:lax) do
assigns = {'b' => 'bar', 'c' => 'baz'} assigns = { 'b' => 'bar', 'c' => 'baz' }
markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false" markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false"
assert_template_result(' YES ',"{% if #{markup} %} YES {% endif %}", assigns) assert_template_result(' YES ', "{% if #{markup} %} YES {% endif %}", assigns)
end end
end end
def test_unexpected_characters_silently_eat_logic_lax def test_unexpected_characters_silently_eat_logic_lax
with_error_mode(:lax) do with_error_mode(:lax) do
markup = "true && false" markup = "true && false"
assert_template_result(' YES ',"{% if #{markup} %} YES {% endif %}") assert_template_result(' YES ', "{% if #{markup} %} YES {% endif %}")
markup = "false || true" markup = "false || true"
assert_template_result('',"{% if #{markup} %} YES {% endif %}") assert_template_result('', "{% if #{markup} %} YES {% endif %}")
end end
end end
@@ -89,15 +92,31 @@ class ParsingQuirksTest < Minitest::Test
def test_unanchored_filter_arguments def test_unanchored_filter_arguments
with_error_mode(:lax) do with_error_mode(:lax) do
assert_template_result('hi',"{{ 'hi there' | split$$$:' ' | first }}") assert_template_result('hi', "{{ 'hi there' | split$$$:' ' | first }}")
assert_template_result('x', "{{ 'X' | downcase) }}") assert_template_result('x', "{{ 'X' | downcase) }}")
# After the messed up quotes a filter without parameters (reverse) should work # After the messed up quotes a filter without parameters (reverse) should work
# but one with parameters (remove) shouldn't be detected. # but one with parameters (remove) shouldn't be detected.
assert_template_result('here', "{{ 'hi there' | split:\"t\"\" | reverse | first}}") assert_template_result('here', "{{ 'hi there' | split:\"t\"\" | reverse | first}}")
assert_template_result('hi ', "{{ 'hi there' | split:\"t\"\" | remove:\"i\" | first}}") assert_template_result('hi ', "{{ 'hi there' | split:\"t\"\" | remove:\"i\" | first}}")
end end
end end
def test_invalid_variables_work
with_error_mode(:lax) do
assert_template_result('bar', "{% assign 123foo = 'bar' %}{{ 123foo }}")
assert_template_result('123', "{% assign 123 = 'bar' %}{{ 123 }}")
end
end
def test_extra_dots_in_ranges
with_error_mode(:lax) do
assert_template_result('12345', "{% for i in (1...5) %}{{ i }}{% endfor %}")
end
end
def test_contains_in_id
assert_template_result(' YES ', '{% if containsallshipments == true %} YES {% endif %}', 'containsallshipments' => true)
end
end # ParsingQuirksTest end # ParsingQuirksTest

View File

@@ -4,7 +4,7 @@ class RenderProfilingTest < Minitest::Test
include Liquid include Liquid
class ProfilingFileSystem class ProfilingFileSystem
def read_template_file(template_path, context) def read_template_file(template_path)
"Rendering template {% assign template_name = '#{template_path}'%}\n{{ template_name }}" "Rendering template {% assign template_name = '#{template_path}'%}\n{{ template_name }}"
end end
end end
@@ -21,7 +21,7 @@ class RenderProfilingTest < Minitest::Test
end end
def test_parse_makes_available_simple_profiling def test_parse_makes_available_simple_profiling
t = Template.parse("{{ 'a string' | upcase }}", :profile => true) t = Template.parse("{{ 'a string' | upcase }}", profile: true)
t.render! t.render!
assert_equal 1, t.profiler.length assert_equal 1, t.profiler.length
@@ -31,14 +31,14 @@ class RenderProfilingTest < Minitest::Test
end end
def test_render_ignores_raw_strings_when_profiling def test_render_ignores_raw_strings_when_profiling
t = Template.parse("This is raw string\nstuff\nNewline", :profile => true) t = Template.parse("This is raw string\nstuff\nNewline", profile: true)
t.render! t.render!
assert_equal 0, t.profiler.length assert_equal 0, t.profiler.length
end end
def test_profiling_includes_line_numbers_of_liquid_nodes def test_profiling_includes_line_numbers_of_liquid_nodes
t = Template.parse("{{ 'a string' | upcase }}\n{% increment test %}", :profile => true) t = Template.parse("{{ 'a string' | upcase }}\n{% increment test %}", profile: true)
t.render! t.render!
assert_equal 2, t.profiler.length assert_equal 2, t.profiler.length
@@ -49,7 +49,7 @@ class RenderProfilingTest < Minitest::Test
end end
def test_profiling_includes_line_numbers_of_included_partials def test_profiling_includes_line_numbers_of_included_partials
t = Template.parse("{% include 'a_template' %}", :profile => true) t = Template.parse("{% include 'a_template' %}", profile: true)
t.render! t.render!
included_children = t.profiler[0].children included_children = t.profiler[0].children
@@ -61,7 +61,7 @@ class RenderProfilingTest < Minitest::Test
end end
def test_profiling_times_the_rendering_of_tokens def test_profiling_times_the_rendering_of_tokens
t = Template.parse("{% include 'a_template' %}", :profile => true) t = Template.parse("{% include 'a_template' %}", profile: true)
t.render! t.render!
node = t.profiler[0] node = t.profiler[0]
@@ -69,14 +69,14 @@ class RenderProfilingTest < Minitest::Test
end end
def test_profiling_times_the_entire_render def test_profiling_times_the_entire_render
t = Template.parse("{% include 'a_template' %}", :profile => true) t = Template.parse("{% include 'a_template' %}", profile: true)
t.render! t.render!
assert t.profiler.total_render_time > 0, "Total render time was not calculated" assert t.profiler.total_render_time >= 0, "Total render time was not calculated"
end end
def test_profiling_uses_include_to_mark_children def test_profiling_uses_include_to_mark_children
t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}", :profile => true) t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}", profile: true)
t.render! t.render!
include_node = t.profiler[1] include_node = t.profiler[1]
@@ -84,47 +84,47 @@ class RenderProfilingTest < Minitest::Test
end end
def test_profiling_marks_children_with_the_name_of_included_partial def test_profiling_marks_children_with_the_name_of_included_partial
t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}", :profile => true) t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}", profile: true)
t.render! t.render!
include_node = t.profiler[1] include_node = t.profiler[1]
include_node.children.each do |child| include_node.children.each do |child|
assert_equal "'a_template'", child.partial assert_equal "a_template", child.partial
end end
end end
def test_profiling_supports_multiple_templates def test_profiling_supports_multiple_templates
t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}\n{% include 'b_template' %}", :profile => true) t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}\n{% include 'b_template' %}", profile: true)
t.render! t.render!
a_template = t.profiler[1] a_template = t.profiler[1]
a_template.children.each do |child| a_template.children.each do |child|
assert_equal "'a_template'", child.partial assert_equal "a_template", child.partial
end end
b_template = t.profiler[2] b_template = t.profiler[2]
b_template.children.each do |child| b_template.children.each do |child|
assert_equal "'b_template'", child.partial assert_equal "b_template", child.partial
end end
end end
def test_profiling_supports_rendering_the_same_partial_multiple_times def test_profiling_supports_rendering_the_same_partial_multiple_times
t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}\n{% include 'a_template' %}", :profile => true) t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}\n{% include 'a_template' %}", profile: true)
t.render! t.render!
a_template1 = t.profiler[1] a_template1 = t.profiler[1]
a_template1.children.each do |child| a_template1.children.each do |child|
assert_equal "'a_template'", child.partial assert_equal "a_template", child.partial
end end
a_template2 = t.profiler[2] a_template2 = t.profiler[2]
a_template2.children.each do |child| a_template2.children.each do |child|
assert_equal "'a_template'", child.partial assert_equal "a_template", child.partial
end end
end end
def test_can_iterate_over_each_profiling_entry def test_can_iterate_over_each_profiling_entry
t = Template.parse("{{ 'a string' | upcase }}\n{% increment test %}", :profile => true) t = Template.parse("{{ 'a string' | upcase }}\n{% increment test %}", profile: true)
t.render! t.render!
timing_count = 0 timing_count = 0
@@ -136,7 +136,7 @@ class RenderProfilingTest < Minitest::Test
end end
def test_profiling_marks_children_of_if_blocks def test_profiling_marks_children_of_if_blocks
t = Template.parse("{% if true %} {% increment test %} {{ test }} {% endif %}", :profile => true) t = Template.parse("{% if true %} {% increment test %} {{ test }} {% endif %}", profile: true)
t.render! t.render!
assert_equal 1, t.profiler.length assert_equal 1, t.profiler.length
@@ -144,8 +144,8 @@ class RenderProfilingTest < Minitest::Test
end end
def test_profiling_marks_children_of_for_blocks def test_profiling_marks_children_of_for_blocks
t = Template.parse("{% for item in collection %} {{ item }} {% endfor %}", :profile => true) t = Template.parse("{% for item in collection %} {{ item }} {% endfor %}", profile: true)
t.render!({"collection" => ["one", "two"]}) t.render!({ "collection" => ["one", "two"] })
assert_equal 1, t.profiler.length assert_equal 1, t.profiler.length
# Will profile each invocation of the for block # Will profile each invocation of the for block

View File

@@ -9,34 +9,36 @@ end
class SecurityTest < Minitest::Test class SecurityTest < Minitest::Test
include Liquid include Liquid
def setup
@assigns = {}
end
def test_no_instance_eval def test_no_instance_eval
text = %( {{ '1+1' | instance_eval }} ) text = %( {{ '1+1' | instance_eval }} )
expected = %| 1+1 | expected = %( 1+1 )
assert_equal expected, Template.parse(text).render!(@assigns) assert_equal expected, Template.parse(text).render!(@assigns)
end end
def test_no_existing_instance_eval def test_no_existing_instance_eval
text = %( {{ '1+1' | __instance_eval__ }} ) text = %( {{ '1+1' | __instance_eval__ }} )
expected = %| 1+1 | expected = %( 1+1 )
assert_equal expected, Template.parse(text).render!(@assigns) assert_equal expected, Template.parse(text).render!(@assigns)
end end
def test_no_instance_eval_after_mixing_in_new_filter def test_no_instance_eval_after_mixing_in_new_filter
text = %( {{ '1+1' | instance_eval }} ) text = %( {{ '1+1' | instance_eval }} )
expected = %| 1+1 | expected = %( 1+1 )
assert_equal expected, Template.parse(text).render!(@assigns) assert_equal expected, Template.parse(text).render!(@assigns)
end end
def test_no_instance_eval_later_in_chain def test_no_instance_eval_later_in_chain
text = %( {{ '1+1' | add_one | instance_eval }} ) text = %( {{ '1+1' | add_one | instance_eval }} )
expected = %| 1+1 + 1 | expected = %( 1+1 + 1 )
assert_equal expected, Template.parse(text).render!(@assigns, :filters => SecurityFilter) assert_equal expected, Template.parse(text).render!(@assigns, filters: SecurityFilter)
end end
def test_does_not_add_filters_to_symbol_table def test_does_not_add_filters_to_symbol_table
@@ -61,4 +63,18 @@ class SecurityTest < Minitest::Test
assert_equal [], (Symbol.all_symbols - current_symbols) assert_equal [], (Symbol.all_symbols - current_symbols)
end end
def test_max_depth_nested_blocks_does_not_raise_exception
depth = Liquid::Block::MAX_DEPTH
code = "{% if true %}" * depth + "rendered" + "{% endif %}" * depth
assert_equal "rendered", Template.parse(code).render!
end
def test_more_than_max_depth_nested_blocks_raises_exception
depth = Liquid::Block::MAX_DEPTH + 1
code = "{% if true %}" * depth + "rendered" + "{% endif %}" * depth
assert_raises(Liquid::StackLevelError) do
Template.parse(code).render!
end
end
end # SecurityTest end # SecurityTest

View File

@@ -41,6 +41,16 @@ class TestEnumerable < Liquid::Drop
end end
end end
class NumberLikeThing < Liquid::Drop
def initialize(amount)
@amount = amount
end
def to_number
@amount
end
end
class StandardFiltersTest < Minitest::Test class StandardFiltersTest < Minitest::Test
include Liquid include Liquid
@@ -49,7 +59,7 @@ class StandardFiltersTest < Minitest::Test
end end
def test_size def test_size
assert_equal 3, @filters.size([1,2,3]) assert_equal 3, @filters.size([1, 2, 3])
assert_equal 0, @filters.size([]) assert_equal 0, @filters.size([])
assert_equal 0, @filters.size(nil) assert_equal 0, @filters.size(nil)
end end
@@ -76,20 +86,27 @@ class StandardFiltersTest < Minitest::Test
assert_equal '', @filters.slice(nil, 0) assert_equal '', @filters.slice(nil, 0)
assert_equal '', @filters.slice('foobar', 100, 10) assert_equal '', @filters.slice('foobar', 100, 10)
assert_equal '', @filters.slice('foobar', -100, 10) assert_equal '', @filters.slice('foobar', -100, 10)
assert_equal 'oob', @filters.slice('foobar', '1', '3')
assert_raises(Liquid::ArgumentError) do
@filters.slice('foobar', nil)
end
assert_raises(Liquid::ArgumentError) do
@filters.slice('foobar', 0, "")
end
end end
def test_slice_on_arrays def test_slice_on_arrays
input = 'foobar'.split(//) input = 'foobar'.split(//)
assert_equal %w{o o b}, @filters.slice(input, 1, 3) assert_equal %w(o o b), @filters.slice(input, 1, 3)
assert_equal %w{o o b a r}, @filters.slice(input, 1, 1000) assert_equal %w(o o b a r), @filters.slice(input, 1, 1000)
assert_equal %w{}, @filters.slice(input, 1, 0) assert_equal %w(), @filters.slice(input, 1, 0)
assert_equal %w{o}, @filters.slice(input, 1, 1) assert_equal %w(o), @filters.slice(input, 1, 1)
assert_equal %w{b a r}, @filters.slice(input, 3, 3) assert_equal %w(b a r), @filters.slice(input, 3, 3)
assert_equal %w{a r}, @filters.slice(input, -2, 2) assert_equal %w(a r), @filters.slice(input, -2, 2)
assert_equal %w{a r}, @filters.slice(input, -2, 1000) assert_equal %w(a r), @filters.slice(input, -2, 1000)
assert_equal %w{r}, @filters.slice(input, -1) assert_equal %w(r), @filters.slice(input, -1)
assert_equal %w{}, @filters.slice(input, 100, 10) assert_equal %w(), @filters.slice(input, 100, 10)
assert_equal %w{}, @filters.slice(input, -100, 10) assert_equal %w(), @filters.slice(input, -100, 10)
end end
def test_truncate def test_truncate
@@ -98,20 +115,29 @@ class StandardFiltersTest < Minitest::Test
assert_equal '...', @filters.truncate('1234567890', 0) assert_equal '...', @filters.truncate('1234567890', 0)
assert_equal '1234567890', @filters.truncate('1234567890') assert_equal '1234567890', @filters.truncate('1234567890')
assert_equal "测试...", @filters.truncate("测试测试测试测试", 5) assert_equal "测试...", @filters.truncate("测试测试测试测试", 5)
assert_equal '12341', @filters.truncate("1234567890", 5, 1)
end end
def test_split def test_split
assert_equal ['12','34'], @filters.split('12~34', '~') assert_equal ['12', '34'], @filters.split('12~34', '~')
assert_equal ['A? ',' ,Z'], @filters.split('A? ~ ~ ~ ,Z', '~ ~ ~') assert_equal ['A? ', ' ,Z'], @filters.split('A? ~ ~ ~ ,Z', '~ ~ ~')
assert_equal ['A?Z'], @filters.split('A?Z', '~') assert_equal ['A?Z'], @filters.split('A?Z', '~')
# Regexp works although Liquid does not support.
assert_equal ['A','Z'], @filters.split('AxZ', /x/)
assert_equal [], @filters.split(nil, ' ') assert_equal [], @filters.split(nil, ' ')
assert_equal ['A', 'Z'], @filters.split('A1Z', 1)
end end
def test_escape def test_escape
assert_equal '&lt;strong&gt;', @filters.escape('<strong>') assert_equal '&lt;strong&gt;', @filters.escape('<strong>')
assert_equal '1', @filters.escape(1)
assert_equal '2001-02-03', @filters.escape(Date.new(2001, 2, 3))
assert_nil @filters.escape(nil)
end
def test_h
assert_equal '&lt;strong&gt;', @filters.h('<strong>') assert_equal '&lt;strong&gt;', @filters.h('<strong>')
assert_equal '1', @filters.h(1)
assert_equal '2001-02-03', @filters.h(Date.new(2001, 2, 3))
assert_nil @filters.h(nil)
end end
def test_escape_once def test_escape_once
@@ -120,7 +146,22 @@ class StandardFiltersTest < Minitest::Test
def test_url_encode def test_url_encode
assert_equal 'foo%2B1%40example.com', @filters.url_encode('foo+1@example.com') assert_equal 'foo%2B1%40example.com', @filters.url_encode('foo+1@example.com')
assert_equal nil, @filters.url_encode(nil) assert_equal '1', @filters.url_encode(1)
assert_equal '2001-02-03', @filters.url_encode(Date.new(2001, 2, 3))
assert_nil @filters.url_encode(nil)
end
def test_url_decode
assert_equal 'foo bar', @filters.url_decode('foo+bar')
assert_equal 'foo bar', @filters.url_decode('foo%20bar')
assert_equal 'foo+1@example.com', @filters.url_decode('foo%2B1%40example.com')
assert_equal '1', @filters.url_decode(1)
assert_equal '2001-02-03', @filters.url_decode(Date.new(2001, 2, 3))
assert_nil @filters.url_decode(nil)
exception = assert_raises Liquid::ArgumentError do
@filters.url_decode('%ff')
end
assert_equal 'Liquid error: invalid byte sequence in UTF-8', exception.message
end end
def test_truncatewords def test_truncatewords
@@ -129,6 +170,7 @@ class StandardFiltersTest < Minitest::Test
assert_equal 'one two three', @filters.truncatewords('one two three') assert_equal 'one two three', @filters.truncatewords('one two three')
assert_equal 'Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221;...', @filters.truncatewords('Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221; x 16&#8221; x 10.5&#8221; high) with cover.', 15) assert_equal 'Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221;...', @filters.truncatewords('Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221; x 16&#8221; x 10.5&#8221; high) with cover.', 15)
assert_equal "测试测试测试测试", @filters.truncatewords('测试测试测试测试', 5) assert_equal "测试测试测试测试", @filters.truncatewords('测试测试测试测试', 5)
assert_equal 'one two1', @filters.truncatewords("one two three", 2, 1)
end end
def test_strip_html def test_strip_html
@@ -139,48 +181,191 @@ class StandardFiltersTest < Minitest::Test
assert_equal 'test', @filters.strip_html("<div\nclass='multiline'>test</div>") assert_equal 'test', @filters.strip_html("<div\nclass='multiline'>test</div>")
assert_equal 'test', @filters.strip_html("<!-- foo bar \n test -->test") assert_equal 'test', @filters.strip_html("<!-- foo bar \n test -->test")
assert_equal '', @filters.strip_html(nil) assert_equal '', @filters.strip_html(nil)
# Quirk of the existing implementation
assert_equal 'foo;', @filters.strip_html("<<<script </script>script>foo;</script>")
end end
def test_join def test_join
assert_equal '1 2 3 4', @filters.join([1,2,3,4]) assert_equal '1 2 3 4', @filters.join([1, 2, 3, 4])
assert_equal '1 - 2 - 3 - 4', @filters.join([1,2,3,4], ' - ') assert_equal '1 - 2 - 3 - 4', @filters.join([1, 2, 3, 4], ' - ')
assert_equal '1121314', @filters.join([1, 2, 3, 4], 1)
end end
def test_sort def test_sort
assert_equal [1,2,3,4], @filters.sort([4,3,2,1]) assert_equal [1, 2, 3, 4], @filters.sort([4, 3, 2, 1])
assert_equal [{"a" => 1}, {"a" => 2}, {"a" => 3}, {"a" => 4}], @filters.sort([{"a" => 4}, {"a" => 3}, {"a" => 1}, {"a" => 2}], "a") assert_equal [{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }], @filters.sort([{ "a" => 4 }, { "a" => 3 }, { "a" => 1 }, { "a" => 2 }], "a")
end
def test_sort_with_nils
assert_equal [1, 2, 3, 4, nil], @filters.sort([nil, 4, 3, 2, 1])
assert_equal [{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }, {}], @filters.sort([{ "a" => 4 }, { "a" => 3 }, {}, { "a" => 1 }, { "a" => 2 }], "a")
end
def test_sort_when_property_is_sometimes_missing_puts_nils_last
input = [
{ "price" => 4, "handle" => "alpha" },
{ "handle" => "beta" },
{ "price" => 1, "handle" => "gamma" },
{ "handle" => "delta" },
{ "price" => 2, "handle" => "epsilon" }
]
expectation = [
{ "price" => 1, "handle" => "gamma" },
{ "price" => 2, "handle" => "epsilon" },
{ "price" => 4, "handle" => "alpha" },
{ "handle" => "delta" },
{ "handle" => "beta" }
]
assert_equal expectation, @filters.sort(input, "price")
end
def test_sort_natural
assert_equal ["a", "B", "c", "D"], @filters.sort_natural(["c", "D", "a", "B"])
assert_equal [{ "a" => "a" }, { "a" => "B" }, { "a" => "c" }, { "a" => "D" }], @filters.sort_natural([{ "a" => "D" }, { "a" => "c" }, { "a" => "a" }, { "a" => "B" }], "a")
end
def test_sort_natural_with_nils
assert_equal ["a", "B", "c", "D", nil], @filters.sort_natural([nil, "c", "D", "a", "B"])
assert_equal [{ "a" => "a" }, { "a" => "B" }, { "a" => "c" }, { "a" => "D" }, {}], @filters.sort_natural([{ "a" => "D" }, { "a" => "c" }, {}, { "a" => "a" }, { "a" => "B" }], "a")
end
def test_sort_natural_when_property_is_sometimes_missing_puts_nils_last
input = [
{ "price" => "4", "handle" => "alpha" },
{ "handle" => "beta" },
{ "price" => "1", "handle" => "gamma" },
{ "handle" => "delta" },
{ "price" => 2, "handle" => "epsilon" }
]
expectation = [
{ "price" => "1", "handle" => "gamma" },
{ "price" => 2, "handle" => "epsilon" },
{ "price" => "4", "handle" => "alpha" },
{ "handle" => "delta" },
{ "handle" => "beta" }
]
assert_equal expectation, @filters.sort_natural(input, "price")
end
def test_sort_natural_case_check
input = [
{ "key" => "X" },
{ "key" => "Y" },
{ "key" => "Z" },
{ "fake" => "t" },
{ "key" => "a" },
{ "key" => "b" },
{ "key" => "c" }
]
expectation = [
{ "key" => "a" },
{ "key" => "b" },
{ "key" => "c" },
{ "key" => "X" },
{ "key" => "Y" },
{ "key" => "Z" },
{ "fake" => "t" }
]
assert_equal expectation, @filters.sort_natural(input, "key")
assert_equal ["a", "b", "c", "X", "Y", "Z"], @filters.sort_natural(["X", "Y", "Z", "a", "b", "c"])
end
def test_sort_empty_array
assert_equal [], @filters.sort([], "a")
end
def test_sort_invalid_property
foo = [
[1],
[2],
[3]
]
assert_raises Liquid::ArgumentError do
@filters.sort(foo, "bar")
end
end
def test_sort_natural_empty_array
assert_equal [], @filters.sort_natural([], "a")
end
def test_sort_natural_invalid_property
foo = [
[1],
[2],
[3]
]
assert_raises Liquid::ArgumentError do
@filters.sort_natural(foo, "bar")
end
end end
def test_legacy_sort_hash def test_legacy_sort_hash
assert_equal [{a:1, b:2}], @filters.sort({a:1, b:2}) assert_equal [{ a: 1, b: 2 }], @filters.sort({ a: 1, b: 2 })
end end
def test_numerical_vs_lexicographical_sort def test_numerical_vs_lexicographical_sort
assert_equal [2, 10], @filters.sort([10, 2]) assert_equal [2, 10], @filters.sort([10, 2])
assert_equal [{"a" => 2}, {"a" => 10}], @filters.sort([{"a" => 10}, {"a" => 2}], "a") assert_equal [{ "a" => 2 }, { "a" => 10 }], @filters.sort([{ "a" => 10 }, { "a" => 2 }], "a")
assert_equal ["10", "2"], @filters.sort(["10", "2"]) assert_equal ["10", "2"], @filters.sort(["10", "2"])
assert_equal [{"a" => "10"}, {"a" => "2"}], @filters.sort([{"a" => "10"}, {"a" => "2"}], "a") assert_equal [{ "a" => "10" }, { "a" => "2" }], @filters.sort([{ "a" => "10" }, { "a" => "2" }], "a")
end end
def test_uniq def test_uniq
assert_equal [1,3,2,4], @filters.uniq([1,1,3,2,3,1,4,3,2,1]) assert_equal ["foo"], @filters.uniq("foo")
assert_equal [{"a" => 1}, {"a" => 3}, {"a" => 2}], @filters.uniq([{"a" => 1}, {"a" => 3}, {"a" => 1}, {"a" => 2}], "a") assert_equal [1, 3, 2, 4], @filters.uniq([1, 1, 3, 2, 3, 1, 4, 3, 2, 1])
assert_equal [{ "a" => 1 }, { "a" => 3 }, { "a" => 2 }], @filters.uniq([{ "a" => 1 }, { "a" => 3 }, { "a" => 1 }, { "a" => 2 }], "a")
testdrop = TestDrop.new testdrop = TestDrop.new
assert_equal [testdrop], @filters.uniq([testdrop, TestDrop.new], 'test') assert_equal [testdrop], @filters.uniq([testdrop, TestDrop.new], 'test')
end end
def test_uniq_empty_array
assert_equal [], @filters.uniq([], "a")
end
def test_uniq_invalid_property
foo = [
[1],
[2],
[3]
]
assert_raises Liquid::ArgumentError do
@filters.uniq(foo, "bar")
end
end
def test_compact_empty_array
assert_equal [], @filters.compact([], "a")
end
def test_compact_invalid_property
foo = [
[1],
[2],
[3]
]
assert_raises Liquid::ArgumentError do
@filters.compact(foo, "bar")
end
end
def test_reverse def test_reverse
assert_equal [4,3,2,1], @filters.reverse([1,2,3,4]) assert_equal [4, 3, 2, 1], @filters.reverse([1, 2, 3, 4])
end end
def test_legacy_reverse_hash def test_legacy_reverse_hash
assert_equal [{a:1, b:2}], @filters.reverse(a:1, b:2) assert_equal [{ a: 1, b: 2 }], @filters.reverse(a: 1, b: 2)
end end
def test_map def test_map
assert_equal [1,2,3,4], @filters.map([{"a" => 1}, {"a" => 2}, {"a" => 3}, {"a" => 4}], 'a') assert_equal [1, 2, 3, 4], @filters.map([{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }], 'a')
assert_template_result 'abc', "{{ ary | map:'foo' | map:'bar' }}", assert_template_result 'abc', "{{ ary | map:'foo' | map:'bar' }}",
'ary' => [{'foo' => {'bar' => 'a'}}, {'foo' => {'bar' => 'b'}}, {'foo' => {'bar' => 'c'}}] 'ary' => [{ 'foo' => { 'bar' => 'a' } }, { 'foo' => { 'bar' => 'b' } }, { 'foo' => { 'bar' => 'c' } }]
end end
def test_map_doesnt_call_arbitrary_stuff def test_map_doesnt_call_arbitrary_stuff
@@ -212,15 +397,51 @@ class StandardFiltersTest < Minitest::Test
def test_map_over_proc def test_map_over_proc
drop = TestDrop.new drop = TestDrop.new
p = Proc.new{ drop } p = proc{ drop }
templ = '{{ procs | map: "test" }}' templ = '{{ procs | map: "test" }}'
assert_template_result "testfoo", templ, "procs" => [p] assert_template_result "testfoo", templ, "procs" => [p]
end end
def test_map_over_drops_returning_procs
drops = [
{
"proc" => ->{ "foo" },
},
{
"proc" => ->{ "bar" },
},
]
templ = '{{ drops | map: "proc" }}'
assert_template_result "foobar", templ, "drops" => drops
end
def test_map_works_on_enumerables def test_map_works_on_enumerables
assert_template_result "123", '{{ foo | map: "foo" }}', "foo" => TestEnumerable.new assert_template_result "123", '{{ foo | map: "foo" }}', "foo" => TestEnumerable.new
end end
def test_map_returns_empty_on_2d_input_array
foo = [
[1],
[2],
[3]
]
assert_raises Liquid::ArgumentError do
@filters.map(foo, "bar")
end
end
def test_map_returns_empty_with_no_property
foo = [
[1],
[2],
[3]
]
assert_raises Liquid::ArgumentError do
@filters.map(foo, nil)
end
end
def test_sort_works_on_enumerables def test_sort_works_on_enumerables
assert_template_result "213", '{{ foo | sort: "bar" | map: "foo" }}', "foo" => TestEnumerable.new assert_template_result "213", '{{ foo | sort: "bar" | map: "foo" }}', "foo" => TestEnumerable.new
end end
@@ -230,6 +451,10 @@ class StandardFiltersTest < Minitest::Test
assert_template_result 'foobar', '{{ foo | last }}', 'foo' => [ThingWithToLiquid.new] assert_template_result 'foobar', '{{ foo | last }}', 'foo' => [ThingWithToLiquid.new]
end end
def test_truncate_calls_to_liquid
assert_template_result "wo...", '{{ foo | truncate: 5 }}', "foo" => TestThing.new
end
def test_date def test_date
assert_equal 'May', @filters.date(Time.parse("2006-05-05 10:00:00"), "%B") assert_equal 'May', @filters.date(Time.parse("2006-05-05 10:00:00"), "%B")
assert_equal 'June', @filters.date(Time.parse("2006-06-05 10:00:00"), "%B") assert_equal 'June', @filters.date(Time.parse("2006-06-05 10:00:00"), "%B")
@@ -247,31 +472,40 @@ class StandardFiltersTest < Minitest::Test
assert_equal '07/05/2006', @filters.date("2006-07-05 10:00:00", "%m/%d/%Y") assert_equal '07/05/2006', @filters.date("2006-07-05 10:00:00", "%m/%d/%Y")
assert_equal "07/16/2004", @filters.date("Fri Jul 16 01:00:00 2004", "%m/%d/%Y") assert_equal "07/16/2004", @filters.date("Fri Jul 16 01:00:00 2004", "%m/%d/%Y")
assert_equal "#{Date.today.year}", @filters.date('now', '%Y') assert_equal Date.today.year.to_s, @filters.date('now', '%Y')
assert_equal "#{Date.today.year}", @filters.date('today', '%Y') assert_equal Date.today.year.to_s, @filters.date('today', '%Y')
assert_equal Date.today.year.to_s, @filters.date('Today', '%Y')
assert_equal nil, @filters.date(nil, "%B") assert_nil @filters.date(nil, "%B")
assert_equal "07/05/2006", @filters.date(1152098955, "%m/%d/%Y") assert_equal '', @filters.date('', "%B")
assert_equal "07/05/2006", @filters.date("1152098955", "%m/%d/%Y")
with_timezone("UTC") do
assert_equal "07/05/2006", @filters.date(1152098955, "%m/%d/%Y")
assert_equal "07/05/2006", @filters.date("1152098955", "%m/%d/%Y")
end
end end
def test_first_last def test_first_last
assert_equal 1, @filters.first([1,2,3]) assert_equal 1, @filters.first([1, 2, 3])
assert_equal 3, @filters.last([1,2,3]) assert_equal 3, @filters.last([1, 2, 3])
assert_equal nil, @filters.first([]) assert_nil @filters.first([])
assert_equal nil, @filters.last([]) assert_nil @filters.last([])
end end
def test_replace def test_replace
assert_equal '2 2 2 2', @filters.replace('1 1 1 1', '1', 2) assert_equal '2 2 2 2', @filters.replace('1 1 1 1', '1', 2)
assert_equal '2 2 2 2', @filters.replace('1 1 1 1', 1, 2)
assert_equal '2 1 1 1', @filters.replace_first('1 1 1 1', '1', 2) assert_equal '2 1 1 1', @filters.replace_first('1 1 1 1', '1', 2)
assert_equal '2 1 1 1', @filters.replace_first('1 1 1 1', 1, 2)
assert_template_result '2 1 1 1', "{{ '1 1 1 1' | replace_first: '1', 2 }}" assert_template_result '2 1 1 1', "{{ '1 1 1 1' | replace_first: '1', 2 }}"
end end
def test_remove def test_remove
assert_equal ' ', @filters.remove("a a a a", 'a') assert_equal ' ', @filters.remove("a a a a", 'a')
assert_equal ' ', @filters.remove("1 1 1 1", 1)
assert_equal 'a a a', @filters.remove_first("a a a a", 'a ') assert_equal 'a a a', @filters.remove_first("a a a a", 'a ')
assert_equal ' 1 1 1', @filters.remove_first("1 1 1 1", 1)
assert_template_result 'a a a', "{{ 'a a a a' | remove_first: 'a ' }}" assert_template_result 'a a a', "{{ 'a a a a' | remove_first: 'a ' }}"
end end
@@ -306,20 +540,38 @@ class StandardFiltersTest < Minitest::Test
def test_plus def test_plus
assert_template_result "2", "{{ 1 | plus:1 }}" assert_template_result "2", "{{ 1 | plus:1 }}"
assert_template_result "2.0", "{{ '1' | plus:'1.0' }}" assert_template_result "2.0", "{{ '1' | plus:'1.0' }}"
assert_template_result "5", "{{ price | plus:'2' }}", 'price' => NumberLikeThing.new(3)
end end
def test_minus def test_minus
assert_template_result "4", "{{ input | minus:operand }}", 'input' => 5, 'operand' => 1 assert_template_result "4", "{{ input | minus:operand }}", 'input' => 5, 'operand' => 1
assert_template_result "2.3", "{{ '4.3' | minus:'2' }}" assert_template_result "2.3", "{{ '4.3' | minus:'2' }}"
assert_template_result "5", "{{ price | minus:'2' }}", 'price' => NumberLikeThing.new(7)
end
def test_abs
assert_template_result "17", "{{ 17 | abs }}"
assert_template_result "17", "{{ -17 | abs }}"
assert_template_result "17", "{{ '17' | abs }}"
assert_template_result "17", "{{ '-17' | abs }}"
assert_template_result "0", "{{ 0 | abs }}"
assert_template_result "0", "{{ '0' | abs }}"
assert_template_result "17.42", "{{ 17.42 | abs }}"
assert_template_result "17.42", "{{ -17.42 | abs }}"
assert_template_result "17.42", "{{ '17.42' | abs }}"
assert_template_result "17.42", "{{ '-17.42' | abs }}"
end end
def test_times def test_times
assert_template_result "12", "{{ 3 | times:4 }}" assert_template_result "12", "{{ 3 | times:4 }}"
assert_template_result "0", "{{ 'foo' | times:4 }}" assert_template_result "0", "{{ 'foo' | times:4 }}"
assert_template_result "6", "{{ '2.1' | times:3 | replace: '.','-' | plus:0}}" assert_template_result "6", "{{ '2.1' | times:3 | replace: '.','-' | plus:0}}"
assert_template_result "7.25", "{{ 0.0725 | times:100 }}" assert_template_result "7.25", "{{ 0.0725 | times:100 }}"
assert_template_result "-7.25", '{{ "-0.0725" | times:100 }}'
assert_template_result "7.25", '{{ "-0.0725" | times: -100 }}'
assert_template_result "4", "{{ price | times:2 }}", 'price' => NumberLikeThing.new(2)
end end
def test_divided_by def test_divided_by
@@ -330,38 +582,96 @@ class StandardFiltersTest < Minitest::Test
assert_equal "Liquid error: divided by 0", Template.parse("{{ 5 | divided_by:0 }}").render assert_equal "Liquid error: divided by 0", Template.parse("{{ 5 | divided_by:0 }}").render
assert_template_result "0.5", "{{ 2.0 | divided_by:4 }}" assert_template_result "0.5", "{{ 2.0 | divided_by:4 }}"
assert_raises(Liquid::ZeroDivisionError) do
assert_template_result "4", "{{ 1 | modulo: 0 }}"
end
assert_template_result "5", "{{ price | divided_by:2 }}", 'price' => NumberLikeThing.new(10)
end end
def test_modulo def test_modulo
assert_template_result "1", "{{ 3 | modulo:2 }}" assert_template_result "1", "{{ 3 | modulo:2 }}"
assert_raises(Liquid::ZeroDivisionError) do
assert_template_result "4", "{{ 1 | modulo: 0 }}"
end
assert_template_result "1", "{{ price | modulo:2 }}", 'price' => NumberLikeThing.new(3)
end end
def test_round def test_round
assert_template_result "5", "{{ input | round }}", 'input' => 4.6 assert_template_result "5", "{{ input | round }}", 'input' => 4.6
assert_template_result "4", "{{ '4.3' | round }}" assert_template_result "4", "{{ '4.3' | round }}"
assert_template_result "4.56", "{{ input | round: 2 }}", 'input' => 4.5612 assert_template_result "4.56", "{{ input | round: 2 }}", 'input' => 4.5612
assert_raises(Liquid::FloatDomainError) do
assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | round }}"
end
assert_template_result "5", "{{ price | round }}", 'price' => NumberLikeThing.new(4.6)
assert_template_result "4", "{{ price | round }}", 'price' => NumberLikeThing.new(4.3)
end end
def test_ceil def test_ceil
assert_template_result "5", "{{ input | ceil }}", 'input' => 4.6 assert_template_result "5", "{{ input | ceil }}", 'input' => 4.6
assert_template_result "5", "{{ '4.3' | ceil }}" assert_template_result "5", "{{ '4.3' | ceil }}"
assert_raises(Liquid::FloatDomainError) do
assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | ceil }}"
end
assert_template_result "5", "{{ price | ceil }}", 'price' => NumberLikeThing.new(4.6)
end end
def test_floor def test_floor
assert_template_result "4", "{{ input | floor }}", 'input' => 4.6 assert_template_result "4", "{{ input | floor }}", 'input' => 4.6
assert_template_result "4", "{{ '4.3' | floor }}" assert_template_result "4", "{{ '4.3' | floor }}"
assert_raises(Liquid::FloatDomainError) do
assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | floor }}"
end
assert_template_result "5", "{{ price | floor }}", 'price' => NumberLikeThing.new(5.4)
end
def test_at_most
assert_template_result "4", "{{ 5 | at_most:4 }}"
assert_template_result "5", "{{ 5 | at_most:5 }}"
assert_template_result "5", "{{ 5 | at_most:6 }}"
assert_template_result "4.5", "{{ 4.5 | at_most:5 }}"
assert_template_result "5", "{{ width | at_most:5 }}", 'width' => NumberLikeThing.new(6)
assert_template_result "4", "{{ width | at_most:5 }}", 'width' => NumberLikeThing.new(4)
assert_template_result "4", "{{ 5 | at_most: width }}", 'width' => NumberLikeThing.new(4)
end
def test_at_least
assert_template_result "5", "{{ 5 | at_least:4 }}"
assert_template_result "5", "{{ 5 | at_least:5 }}"
assert_template_result "6", "{{ 5 | at_least:6 }}"
assert_template_result "5", "{{ 4.5 | at_least:5 }}"
assert_template_result "6", "{{ width | at_least:5 }}", 'width' => NumberLikeThing.new(6)
assert_template_result "5", "{{ width | at_least:5 }}", 'width' => NumberLikeThing.new(4)
assert_template_result "6", "{{ 5 | at_least: width }}", 'width' => NumberLikeThing.new(6)
end end
def test_append def test_append
assigns = {'a' => 'bc', 'b' => 'd' } assigns = { 'a' => 'bc', 'b' => 'd' }
assert_template_result('bcd',"{{ a | append: 'd'}}",assigns) assert_template_result('bcd', "{{ a | append: 'd'}}", assigns)
assert_template_result('bcd',"{{ a | append: b}}",assigns) assert_template_result('bcd', "{{ a | append: b}}", assigns)
end
def test_concat
assert_equal [1, 2, 3, 4], @filters.concat([1, 2], [3, 4])
assert_equal [1, 2, 'a'], @filters.concat([1, 2], ['a'])
assert_equal [1, 2, 10], @filters.concat([1, 2], [10])
assert_raises(Liquid::ArgumentError, "concat filter requires an array argument") do
@filters.concat([1, 2], 10)
end
end end
def test_prepend def test_prepend
assigns = {'a' => 'bc', 'b' => 'a' } assigns = { 'a' => 'bc', 'b' => 'a' }
assert_template_result('abc',"{{ a | prepend: 'a'}}",assigns) assert_template_result('abc', "{{ a | prepend: 'a'}}", assigns)
assert_template_result('abc',"{{ a | prepend: b}}",assigns) assert_template_result('abc', "{{ a | prepend: b}}", assigns)
end end
def test_default def test_default
@@ -374,6 +684,93 @@ class StandardFiltersTest < Minitest::Test
end end
def test_cannot_access_private_methods def test_cannot_access_private_methods
assert_template_result('a',"{{ 'a' | to_number }}") assert_template_result('a', "{{ 'a' | to_number }}")
end
def test_date_raises_nothing
assert_template_result('', "{{ '' | date: '%D' }}")
assert_template_result('abc', "{{ 'abc' | date: '%D' }}")
end
def test_where
input = [
{ "handle" => "alpha", "ok" => true },
{ "handle" => "beta", "ok" => false },
{ "handle" => "gamma", "ok" => false },
{ "handle" => "delta", "ok" => true }
]
expectation = [
{ "handle" => "alpha", "ok" => true },
{ "handle" => "delta", "ok" => true }
]
assert_equal expectation, @filters.where(input, "ok", true)
assert_equal expectation, @filters.where(input, "ok")
end
def test_where_no_key_set
input = [
{ "handle" => "alpha", "ok" => true },
{ "handle" => "beta" },
{ "handle" => "gamma" },
{ "handle" => "delta", "ok" => true }
]
expectation = [
{ "handle" => "alpha", "ok" => true },
{ "handle" => "delta", "ok" => true }
]
assert_equal expectation, @filters.where(input, "ok", true)
assert_equal expectation, @filters.where(input, "ok")
end
def test_where_non_array_map_input
assert_equal [{ "a" => "ok" }], @filters.where({ "a" => "ok" }, "a", "ok")
assert_equal [], @filters.where({ "a" => "not ok" }, "a", "ok")
end
def test_where_indexable_but_non_map_value
assert_raises(Liquid::ArgumentError) { @filters.where(1, "ok", true) }
assert_raises(Liquid::ArgumentError) { @filters.where(1, "ok") }
end
def test_where_non_boolean_value
input = [
{ "message" => "Bonjour!", "language" => "French" },
{ "message" => "Hello!", "language" => "English" },
{ "message" => "Hallo!", "language" => "German" }
]
assert_equal [{ "message" => "Bonjour!", "language" => "French" }], @filters.where(input, "language", "French")
assert_equal [{ "message" => "Hallo!", "language" => "German" }], @filters.where(input, "language", "German")
assert_equal [{ "message" => "Hello!", "language" => "English" }], @filters.where(input, "language", "English")
end
def test_where_array_of_only_unindexable_values
assert_nil @filters.where([nil], "ok", true)
assert_nil @filters.where([nil], "ok")
end
def test_where_no_target_value
input = [
{ "foo" => false },
{ "foo" => true },
{ "foo" => "for sure" },
{ "bar" => true }
]
assert_equal [{ "foo" => true }, { "foo" => "for sure" }], @filters.where(input, "foo")
end
private
def with_timezone(tz)
old_tz = ENV['TZ']
ENV['TZ'] = tz
yield
ensure
ENV['TZ'] = old_tz
end end
end # StandardFiltersTest end # StandardFiltersTest

View File

@@ -6,11 +6,10 @@ class BreakTagTest < Minitest::Test
# tests that no weird errors are raised if break is called outside of a # tests that no weird errors are raised if break is called outside of a
# block # block
def test_break_with_no_block def test_break_with_no_block
assigns = {'i' => 1} assigns = { 'i' => 1 }
markup = '{% break %}' markup = '{% break %}'
expected = '' expected = ''
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
end end
end end

View File

@@ -12,5 +12,4 @@ class ContinueTagTest < Minitest::Test
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
end end
end end

View File

@@ -10,10 +10,10 @@ class ForTagTest < Minitest::Test
include Liquid include Liquid
def test_for def test_for
assert_template_result(' yo yo yo yo ','{%for item in array%} yo {%endfor%}','array' => [1,2,3,4]) assert_template_result(' yo yo yo yo ', '{%for item in array%} yo {%endfor%}', 'array' => [1, 2, 3, 4])
assert_template_result('yoyo','{%for item in array%}yo{%endfor%}','array' => [1,2]) assert_template_result('yoyo', '{%for item in array%}yo{%endfor%}', 'array' => [1, 2])
assert_template_result(' yo ','{%for item in array%} yo {%endfor%}','array' => [1]) assert_template_result(' yo ', '{%for item in array%} yo {%endfor%}', 'array' => [1])
assert_template_result('','{%for item in array%}{%endfor%}','array' => [1,2]) assert_template_result('', '{%for item in array%}{%endfor%}', 'array' => [1, 2])
expected = <<HERE expected = <<HERE
yo yo
@@ -28,46 +28,56 @@ HERE
yo yo
{%endfor%} {%endfor%}
HERE HERE
assert_template_result(expected,template,'array' => [1,2,3]) assert_template_result(expected, template, 'array' => [1, 2, 3])
end end
def test_for_reversed def test_for_reversed
assigns = {'array' => [ 1, 2, 3] } assigns = { 'array' => [ 1, 2, 3] }
assert_template_result('321','{%for item in array reversed %}{{item}}{%endfor%}',assigns) assert_template_result('321', '{%for item in array reversed %}{{item}}{%endfor%}', assigns)
end
def test_for_range_reversed
assert_template_result('321', '{%for item in (1..3) reversed %}{{item}}{%endfor%}', {})
end end
def test_for_with_range def test_for_with_range
assert_template_result(' 1 2 3 ','{%for item in (1..3) %} {{item}} {%endfor%}') assert_template_result(' 1 2 3 ', '{%for item in (1..3) %} {{item}} {%endfor%}')
assert_raises(Liquid::ArgumentError) do
Template.parse('{% for i in (a..2) %}{% endfor %}').render!("a" => [1, 2])
end
assert_template_result(' 0 1 2 3 ', '{% for item in (a..3) %} {{item}} {% endfor %}', "a" => "invalid integer")
end end
def test_for_with_variable_range def test_for_with_variable_range
assert_template_result(' 1 2 3 ','{%for item in (1..foobar) %} {{item}} {%endfor%}', "foobar" => 3) assert_template_result(' 1 2 3 ', '{%for item in (1..foobar) %} {{item}} {%endfor%}', "foobar" => 3)
end end
def test_for_with_hash_value_range def test_for_with_hash_value_range
foobar = { "value" => 3 } foobar = { "value" => 3 }
assert_template_result(' 1 2 3 ','{%for item in (1..foobar.value) %} {{item}} {%endfor%}', "foobar" => foobar) assert_template_result(' 1 2 3 ', '{%for item in (1..foobar.value) %} {{item}} {%endfor%}', "foobar" => foobar)
end end
def test_for_with_drop_value_range def test_for_with_drop_value_range
foobar = ThingWithValue.new foobar = ThingWithValue.new
assert_template_result(' 1 2 3 ','{%for item in (1..foobar.value) %} {{item}} {%endfor%}', "foobar" => foobar) assert_template_result(' 1 2 3 ', '{%for item in (1..foobar.value) %} {{item}} {%endfor%}', "foobar" => foobar)
end end
def test_for_with_variable def test_for_with_variable
assert_template_result(' 1 2 3 ','{%for item in array%} {{item}} {%endfor%}','array' => [1,2,3]) assert_template_result(' 1 2 3 ', '{%for item in array%} {{item}} {%endfor%}', 'array' => [1, 2, 3])
assert_template_result('123','{%for item in array%}{{item}}{%endfor%}','array' => [1,2,3]) assert_template_result('123', '{%for item in array%}{{item}}{%endfor%}', 'array' => [1, 2, 3])
assert_template_result('123','{% for item in array %}{{item}}{% endfor %}','array' => [1,2,3]) assert_template_result('123', '{% for item in array %}{{item}}{% endfor %}', 'array' => [1, 2, 3])
assert_template_result('abcd','{%for item in array%}{{item}}{%endfor%}','array' => ['a','b','c','d']) assert_template_result('abcd', '{%for item in array%}{{item}}{%endfor%}', 'array' => ['a', 'b', 'c', 'd'])
assert_template_result('a b c','{%for item in array%}{{item}}{%endfor%}','array' => ['a',' ','b',' ','c']) assert_template_result('a b c', '{%for item in array%}{{item}}{%endfor%}', 'array' => ['a', ' ', 'b', ' ', 'c'])
assert_template_result('abc','{%for item in array%}{{item}}{%endfor%}','array' => ['a','','b','','c']) assert_template_result('abc', '{%for item in array%}{{item}}{%endfor%}', 'array' => ['a', '', 'b', '', 'c'])
end end
def test_for_helpers def test_for_helpers
assigns = {'array' => [1,2,3] } assigns = { 'array' => [1, 2, 3] }
assert_template_result(' 1/3 2/3 3/3 ', assert_template_result(' 1/3 2/3 3/3 ',
'{%for item in array%} {{forloop.index}}/{{forloop.length}} {%endfor%}', '{%for item in array%} {{forloop.index}}/{{forloop.length}} {%endfor%}',
assigns) assigns)
assert_template_result(' 1 2 3 ', '{%for item in array%} {{forloop.index}} {%endfor%}', assigns) assert_template_result(' 1 2 3 ', '{%for item in array%} {{forloop.index}} {%endfor%}', assigns)
assert_template_result(' 0 1 2 ', '{%for item in array%} {{forloop.index0}} {%endfor%}', assigns) assert_template_result(' 0 1 2 ', '{%for item in array%} {{forloop.index0}} {%endfor%}', assigns)
assert_template_result(' 2 1 0 ', '{%for item in array%} {{forloop.rindex0}} {%endfor%}', assigns) assert_template_result(' 2 1 0 ', '{%for item in array%} {{forloop.rindex0}} {%endfor%}', assigns)
@@ -77,28 +87,56 @@ HERE
end end
def test_for_and_if def test_for_and_if
assigns = {'array' => [1,2,3] } assigns = { 'array' => [1, 2, 3] }
assert_template_result('+--', assert_template_result('+--',
'{%for item in array%}{% if forloop.first %}+{% else %}-{% endif %}{%endfor%}', '{%for item in array%}{% if forloop.first %}+{% else %}-{% endif %}{%endfor%}',
assigns) assigns)
end end
def test_for_else def test_for_else
assert_template_result('+++', '{%for item in array%}+{%else%}-{%endfor%}', 'array'=>[1,2,3]) assert_template_result('+++', '{%for item in array%}+{%else%}-{%endfor%}', 'array' => [1, 2, 3])
assert_template_result('-', '{%for item in array%}+{%else%}-{%endfor%}', 'array'=>[]) assert_template_result('-', '{%for item in array%}+{%else%}-{%endfor%}', 'array' => [])
assert_template_result('-', '{%for item in array%}+{%else%}-{%endfor%}', 'array'=>nil) assert_template_result('-', '{%for item in array%}+{%else%}-{%endfor%}', 'array' => nil)
end end
def test_limiting def test_limiting
assigns = {'array' => [1,2,3,4,5,6,7,8,9,0]} assigns = { 'array' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] }
assert_template_result('12', '{%for i in array limit:2 %}{{ i }}{%endfor%}', assigns) assert_template_result('12', '{%for i in array limit:2 %}{{ i }}{%endfor%}', assigns)
assert_template_result('1234', '{%for i in array limit:4 %}{{ i }}{%endfor%}', assigns) assert_template_result('1234', '{%for i in array limit:4 %}{{ i }}{%endfor%}', assigns)
assert_template_result('3456', '{%for i in array limit:4 offset:2 %}{{ i }}{%endfor%}', assigns) assert_template_result('3456', '{%for i in array limit:4 offset:2 %}{{ i }}{%endfor%}', assigns)
assert_template_result('3456', '{%for i in array limit: 4 offset: 2 %}{{ i }}{%endfor%}', assigns) assert_template_result('3456', '{%for i in array limit: 4 offset: 2 %}{{ i }}{%endfor%}', assigns)
end end
def test_limiting_with_invalid_limit
assigns = { 'array' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] }
template = <<-MKUP
{% for i in array limit: true offset: 1 %}
{{ i }}
{% endfor %}
MKUP
exception = assert_raises(Liquid::ArgumentError) do
Template.parse(template).render!(assigns)
end
assert_equal("Liquid error: invalid integer", exception.message)
end
def test_limiting_with_invalid_offset
assigns = { 'array' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] }
template = <<-MKUP
{% for i in array limit: 1 offset: true %}
{{ i }}
{% endfor %}
MKUP
exception = assert_raises(Liquid::ArgumentError) do
Template.parse(template).render!(assigns)
end
assert_equal("Liquid error: invalid integer", exception.message)
end
def test_dynamic_variable_limiting def test_dynamic_variable_limiting
assigns = {'array' => [1,2,3,4,5,6,7,8,9,0]} assigns = { 'array' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] }
assigns['limit'] = 2 assigns['limit'] = 2
assigns['offset'] = 2 assigns['offset'] = 2
@@ -106,17 +144,17 @@ HERE
end end
def test_nested_for def test_nested_for
assigns = {'array' => [[1,2],[3,4],[5,6]] } assigns = { 'array' => [[1, 2], [3, 4], [5, 6]] }
assert_template_result('123456', '{%for item in array%}{%for i in item%}{{ i }}{%endfor%}{%endfor%}', assigns) assert_template_result('123456', '{%for item in array%}{%for i in item%}{{ i }}{%endfor%}{%endfor%}', assigns)
end end
def test_offset_only def test_offset_only
assigns = {'array' => [1,2,3,4,5,6,7,8,9,0]} assigns = { 'array' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] }
assert_template_result('890', '{%for i in array offset:7 %}{{ i }}{%endfor%}', assigns) assert_template_result('890', '{%for i in array offset:7 %}{{ i }}{%endfor%}', assigns)
end end
def test_pause_resume def test_pause_resume
assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,0]}} assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } }
markup = <<-MKUP markup = <<-MKUP
{%for i in array.items limit: 3 %}{{i}}{%endfor%} {%for i in array.items limit: 3 %}{{i}}{%endfor%}
next next
@@ -131,11 +169,11 @@ HERE
next next
789 789
XPCTD XPCTD
assert_template_result(expected,markup,assigns) assert_template_result(expected, markup, assigns)
end end
def test_pause_resume_limit def test_pause_resume_limit
assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,0]}} assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } }
markup = <<-MKUP markup = <<-MKUP
{%for i in array.items limit:3 %}{{i}}{%endfor%} {%for i in array.items limit:3 %}{{i}}{%endfor%}
next next
@@ -150,11 +188,11 @@ HERE
next next
7 7
XPCTD XPCTD
assert_template_result(expected,markup,assigns) assert_template_result(expected, markup, assigns)
end end
def test_pause_resume_BIG_limit def test_pause_resume_big_limit
assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,0]}} assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } }
markup = <<-MKUP markup = <<-MKUP
{%for i in array.items limit:3 %}{{i}}{%endfor%} {%for i in array.items limit:3 %}{{i}}{%endfor%}
next next
@@ -169,103 +207,102 @@ HERE
next next
7890 7890
XPCTD XPCTD
assert_template_result(expected,markup,assigns) assert_template_result(expected, markup, assigns)
end end
def test_pause_resume_big_offset
def test_pause_resume_BIG_offset assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } }
assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,0]}} markup = '{%for i in array.items limit:3 %}{{i}}{%endfor%}
markup = %q({%for i in array.items limit:3 %}{{i}}{%endfor%}
next next
{%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%} {%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%}
next next
{%for i in array.items offset:continue limit:3 offset:1000 %}{{i}}{%endfor%}) {%for i in array.items offset:continue limit:3 offset:1000 %}{{i}}{%endfor%}'
expected = %q(123 expected = '123
next next
456 456
next next
) '
assert_template_result(expected,markup,assigns) assert_template_result(expected, markup, assigns)
end end
def test_for_with_break def test_for_with_break
assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,10]}} assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] } }
markup = '{% for i in array.items %}{% break %}{% endfor %}' markup = '{% for i in array.items %}{% break %}{% endfor %}'
expected = "" expected = ""
assert_template_result(expected,markup,assigns) assert_template_result(expected, markup, assigns)
markup = '{% for i in array.items %}{{ i }}{% break %}{% endfor %}' markup = '{% for i in array.items %}{{ i }}{% break %}{% endfor %}'
expected = "1" expected = "1"
assert_template_result(expected,markup,assigns) assert_template_result(expected, markup, assigns)
markup = '{% for i in array.items %}{% break %}{{ i }}{% endfor %}' markup = '{% for i in array.items %}{% break %}{{ i }}{% endfor %}'
expected = "" expected = ""
assert_template_result(expected,markup,assigns) assert_template_result(expected, markup, assigns)
markup = '{% for i in array.items %}{{ i }}{% if i > 3 %}{% break %}{% endif %}{% endfor %}' markup = '{% for i in array.items %}{{ i }}{% if i > 3 %}{% break %}{% endif %}{% endfor %}'
expected = "1234" expected = "1234"
assert_template_result(expected,markup,assigns) assert_template_result(expected, markup, assigns)
# tests to ensure it only breaks out of the local for loop # tests to ensure it only breaks out of the local for loop
# and not all of them. # and not all of them.
assigns = {'array' => [[1,2],[3,4],[5,6]] } assigns = { 'array' => [[1, 2], [3, 4], [5, 6]] }
markup = '{% for item in array %}' + markup = '{% for item in array %}' \
'{% for i in item %}' + '{% for i in item %}' \
'{% if i == 1 %}' + '{% if i == 1 %}' \
'{% break %}' + '{% break %}' \
'{% endif %}' + '{% endif %}' \
'{{ i }}' + '{{ i }}' \
'{% endfor %}' + '{% endfor %}' \
'{% endfor %}' '{% endfor %}'
expected = '3456' expected = '3456'
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
# test break does nothing when unreached # test break does nothing when unreached
assigns = {'array' => {'items' => [1,2,3,4,5]}} assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5] } }
markup = '{% for i in array.items %}{% if i == 9999 %}{% break %}{% endif %}{{ i }}{% endfor %}' markup = '{% for i in array.items %}{% if i == 9999 %}{% break %}{% endif %}{{ i }}{% endfor %}'
expected = '12345' expected = '12345'
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
end end
def test_for_with_continue def test_for_with_continue
assigns = {'array' => {'items' => [1,2,3,4,5]}} assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5] } }
markup = '{% for i in array.items %}{% continue %}{% endfor %}' markup = '{% for i in array.items %}{% continue %}{% endfor %}'
expected = "" expected = ""
assert_template_result(expected,markup,assigns) assert_template_result(expected, markup, assigns)
markup = '{% for i in array.items %}{{ i }}{% continue %}{% endfor %}' markup = '{% for i in array.items %}{{ i }}{% continue %}{% endfor %}'
expected = "12345" expected = "12345"
assert_template_result(expected,markup,assigns) assert_template_result(expected, markup, assigns)
markup = '{% for i in array.items %}{% continue %}{{ i }}{% endfor %}' markup = '{% for i in array.items %}{% continue %}{{ i }}{% endfor %}'
expected = "" expected = ""
assert_template_result(expected,markup,assigns) assert_template_result(expected, markup, assigns)
markup = '{% for i in array.items %}{% if i > 3 %}{% continue %}{% endif %}{{ i }}{% endfor %}' markup = '{% for i in array.items %}{% if i > 3 %}{% continue %}{% endif %}{{ i }}{% endfor %}'
expected = "123" expected = "123"
assert_template_result(expected,markup,assigns) assert_template_result(expected, markup, assigns)
markup = '{% for i in array.items %}{% if i == 3 %}{% continue %}{% else %}{{ i }}{% endif %}{% endfor %}' markup = '{% for i in array.items %}{% if i == 3 %}{% continue %}{% else %}{{ i }}{% endif %}{% endfor %}'
expected = "1245" expected = "1245"
assert_template_result(expected,markup,assigns) assert_template_result(expected, markup, assigns)
# tests to ensure it only continues the local for loop and not all of them. # tests to ensure it only continues the local for loop and not all of them.
assigns = {'array' => [[1,2],[3,4],[5,6]] } assigns = { 'array' => [[1, 2], [3, 4], [5, 6]] }
markup = '{% for item in array %}' + markup = '{% for item in array %}' \
'{% for i in item %}' + '{% for i in item %}' \
'{% if i == 1 %}' + '{% if i == 1 %}' \
'{% continue %}' + '{% continue %}' \
'{% endif %}' + '{% endif %}' \
'{{ i }}' + '{{ i }}' \
'{% endfor %}' + '{% endfor %}' \
'{% endfor %}' '{% endfor %}'
expected = '23456' expected = '23456'
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
# test continue does nothing when unreached # test continue does nothing when unreached
assigns = {'array' => {'items' => [1,2,3,4,5]}} assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5] } }
markup = '{% for i in array.items %}{% if i == 9999 %}{% continue %}{% endif %}{{ i }}{% endfor %}' markup = '{% for i in array.items %}{% if i == 9999 %}{% continue %}{% endif %}{{ i }}{% endfor %}'
expected = '12345' expected = '12345'
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
@@ -277,25 +314,45 @@ HERE
# the functionality for backwards compatibility # the functionality for backwards compatibility
assert_template_result('test string', assert_template_result('test string',
'{%for val in string%}{{val}}{%endfor%}', '{%for val in string%}{{val}}{%endfor%}',
'string' => "test string") 'string' => "test string")
assert_template_result('test string', assert_template_result('test string',
'{%for val in string limit:1%}{{val}}{%endfor%}', '{%for val in string limit:1%}{{val}}{%endfor%}',
'string' => "test string") 'string' => "test string")
assert_template_result('val-string-1-1-0-1-0-true-true-test string', assert_template_result('val-string-1-1-0-1-0-true-true-test string',
'{%for val in string%}' + '{%for val in string%}' \
'{{forloop.name}}-' + '{{forloop.name}}-' \
'{{forloop.index}}-' + '{{forloop.index}}-' \
'{{forloop.length}}-' + '{{forloop.length}}-' \
'{{forloop.index0}}-' + '{{forloop.index0}}-' \
'{{forloop.rindex}}-' + '{{forloop.rindex}}-' \
'{{forloop.rindex0}}-' + '{{forloop.rindex0}}-' \
'{{forloop.first}}-' + '{{forloop.first}}-' \
'{{forloop.last}}-' + '{{forloop.last}}-' \
'{{val}}{%endfor%}', '{{val}}{%endfor%}',
'string' => "test string") 'string' => "test string")
end
def test_for_parentloop_references_parent_loop
assert_template_result('1.1 1.2 1.3 2.1 2.2 2.3 ',
'{% for inner in outer %}{% for k in inner %}' \
'{{ forloop.parentloop.index }}.{{ forloop.index }} ' \
'{% endfor %}{% endfor %}',
'outer' => [[1, 1, 1], [1, 1, 1]])
end
def test_for_parentloop_nil_when_not_present
assert_template_result('.1 .2 ',
'{% for inner in outer %}' \
'{{ forloop.parentloop.index }}.{{ forloop.index }} ' \
'{% endfor %}',
'outer' => [[1, 1, 1], [1, 1, 1]])
end
def test_inner_for_over_empty_input
assert_template_result 'oo', '{% for a in (1..2) %}o{% for b in empty %}{% endfor %}{% endfor %}'
end end
def test_blank_string_not_iterable def test_blank_string_not_iterable
@@ -311,7 +368,7 @@ HERE
def test_spacing_with_variable_naming_in_for_loop def test_spacing_with_variable_naming_in_for_loop
expected = '12345' expected = '12345'
template = '{% for item in items %}{{item}}{% endfor %}' template = '{% for item in items %}{{item}}{% endfor %}'
assigns = {'items' => [1,2,3,4,5]} assigns = { 'items' => [1, 2, 3, 4, 5] }
assert_template_result(expected, template, assigns) assert_template_result(expected, template, assigns)
end end
@@ -329,13 +386,13 @@ HERE
def load_slice(from, to) def load_slice(from, to)
@load_slice_called = true @load_slice_called = true
@data[(from..to-1)] @data[(from..to - 1)]
end end
end end
def test_iterate_with_each_when_no_limit_applied def test_iterate_with_each_when_no_limit_applied
loader = LoaderDrop.new([1,2,3,4,5]) loader = LoaderDrop.new([1, 2, 3, 4, 5])
assigns = {'items' => loader} assigns = { 'items' => loader }
expected = '12345' expected = '12345'
template = '{% for item in items %}{{item}}{% endfor %}' template = '{% for item in items %}{{item}}{% endfor %}'
assert_template_result(expected, template, assigns) assert_template_result(expected, template, assigns)
@@ -344,8 +401,8 @@ HERE
end end
def test_iterate_with_load_slice_when_limit_applied def test_iterate_with_load_slice_when_limit_applied
loader = LoaderDrop.new([1,2,3,4,5]) loader = LoaderDrop.new([1, 2, 3, 4, 5])
assigns = {'items' => loader} assigns = { 'items' => loader }
expected = '1' expected = '1'
template = '{% for item in items limit:1 %}{{item}}{% endfor %}' template = '{% for item in items limit:1 %}{{item}}{% endfor %}'
assert_template_result(expected, template, assigns) assert_template_result(expected, template, assigns)
@@ -354,8 +411,8 @@ HERE
end end
def test_iterate_with_load_slice_when_limit_and_offset_applied def test_iterate_with_load_slice_when_limit_and_offset_applied
loader = LoaderDrop.new([1,2,3,4,5]) loader = LoaderDrop.new([1, 2, 3, 4, 5])
assigns = {'items' => loader} assigns = { 'items' => loader }
expected = '34' expected = '34'
template = '{% for item in items offset:2 limit:2 %}{{item}}{% endfor %}' template = '{% for item in items offset:2 limit:2 %}{{item}}{% endfor %}'
assert_template_result(expected, template, assigns) assert_template_result(expected, template, assigns)
@@ -364,12 +421,22 @@ HERE
end end
def test_iterate_with_load_slice_returns_same_results_as_without def test_iterate_with_load_slice_returns_same_results_as_without
loader = LoaderDrop.new([1,2,3,4,5]) loader = LoaderDrop.new([1, 2, 3, 4, 5])
loader_assigns = {'items' => loader} loader_assigns = { 'items' => loader }
array_assigns = {'items' => [1,2,3,4,5]} array_assigns = { 'items' => [1, 2, 3, 4, 5] }
expected = '34' expected = '34'
template = '{% for item in items offset:2 limit:2 %}{{item}}{% endfor %}' template = '{% for item in items offset:2 limit:2 %}{{item}}{% endfor %}'
assert_template_result(expected, template, loader_assigns) assert_template_result(expected, template, loader_assigns)
assert_template_result(expected, template, array_assigns) assert_template_result(expected, template, array_assigns)
end end
def test_for_cleans_up_registers
context = Context.new(ErrorDrop.new)
assert_raises(StandardError) do
Liquid::Template.parse('{% for i in (1..2) %}{{ standard_error }}{% endfor %}').render!(context)
end
assert context.registers[:for_stack].empty?
end
end end

View File

@@ -4,96 +4,100 @@ class IfElseTagTest < Minitest::Test
include Liquid include Liquid
def test_if def test_if
assert_template_result(' ',' {% if false %} this text should not go into the output {% endif %} ') assert_template_result(' ', ' {% if false %} this text should not go into the output {% endif %} ')
assert_template_result(' this text should go into the output ', assert_template_result(' this text should go into the output ',
' {% if true %} this text should go into the output {% endif %} ') ' {% if true %} this text should go into the output {% endif %} ')
assert_template_result(' you rock ?','{% if false %} you suck {% endif %} {% if true %} you rock {% endif %}?') assert_template_result(' you rock ?', '{% if false %} you suck {% endif %} {% if true %} you rock {% endif %}?')
end
def test_literal_comparisons
assert_template_result(' NO ', '{% assign v = false %}{% if v %} YES {% else %} NO {% endif %}')
assert_template_result(' YES ', '{% assign v = nil %}{% if v == nil %} YES {% else %} NO {% endif %}')
end end
def test_if_else def test_if_else
assert_template_result(' YES ','{% if false %} NO {% else %} YES {% endif %}') assert_template_result(' YES ', '{% if false %} NO {% else %} YES {% endif %}')
assert_template_result(' YES ','{% if true %} YES {% else %} NO {% endif %}') assert_template_result(' YES ', '{% if true %} YES {% else %} NO {% endif %}')
assert_template_result(' YES ','{% if "foo" %} YES {% else %} NO {% endif %}') assert_template_result(' YES ', '{% if "foo" %} YES {% else %} NO {% endif %}')
end end
def test_if_boolean def test_if_boolean
assert_template_result(' YES ','{% if var %} YES {% endif %}', 'var' => true) assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => true)
end end
def test_if_or def test_if_or
assert_template_result(' YES ','{% if a or b %} YES {% endif %}', 'a' => true, 'b' => true) assert_template_result(' YES ', '{% if a or b %} YES {% endif %}', 'a' => true, 'b' => true)
assert_template_result(' YES ','{% if a or b %} YES {% endif %}', 'a' => true, 'b' => false) assert_template_result(' YES ', '{% if a or b %} YES {% endif %}', 'a' => true, 'b' => false)
assert_template_result(' YES ','{% if a or b %} YES {% endif %}', 'a' => false, 'b' => true) assert_template_result(' YES ', '{% if a or b %} YES {% endif %}', 'a' => false, 'b' => true)
assert_template_result('', '{% if a or b %} YES {% endif %}', 'a' => false, 'b' => false) assert_template_result('', '{% if a or b %} YES {% endif %}', 'a' => false, 'b' => false)
assert_template_result(' YES ','{% if a or b or c %} YES {% endif %}', 'a' => false, 'b' => false, 'c' => true) assert_template_result(' YES ', '{% if a or b or c %} YES {% endif %}', 'a' => false, 'b' => false, 'c' => true)
assert_template_result('', '{% if a or b or c %} YES {% endif %}', 'a' => false, 'b' => false, 'c' => false) assert_template_result('', '{% if a or b or c %} YES {% endif %}', 'a' => false, 'b' => false, 'c' => false)
end end
def test_if_or_with_operators def test_if_or_with_operators
assert_template_result(' YES ','{% if a == true or b == true %} YES {% endif %}', 'a' => true, 'b' => true) assert_template_result(' YES ', '{% if a == true or b == true %} YES {% endif %}', 'a' => true, 'b' => true)
assert_template_result(' YES ','{% if a == true or b == false %} YES {% endif %}', 'a' => true, 'b' => true) assert_template_result(' YES ', '{% if a == true or b == false %} YES {% endif %}', 'a' => true, 'b' => true)
assert_template_result('','{% if a == false or b == false %} YES {% endif %}', 'a' => true, 'b' => true) assert_template_result('', '{% if a == false or b == false %} YES {% endif %}', 'a' => true, 'b' => true)
end end
def test_comparison_of_strings_containing_and_or_or def test_comparison_of_strings_containing_and_or_or
awful_markup = "a == 'and' and b == 'or' and c == 'foo and bar' and d == 'bar or baz' and e == 'foo' and foo and bar" awful_markup = "a == 'and' and b == 'or' and c == 'foo and bar' and d == 'bar or baz' and e == 'foo' and foo and bar"
assigns = {'a' => 'and', 'b' => 'or', 'c' => 'foo and bar', 'd' => 'bar or baz', 'e' => 'foo', 'foo' => true, 'bar' => true} assigns = { 'a' => 'and', 'b' => 'or', 'c' => 'foo and bar', 'd' => 'bar or baz', 'e' => 'foo', 'foo' => true, 'bar' => true }
assert_template_result(' YES ',"{% if #{awful_markup} %} YES {% endif %}", assigns) assert_template_result(' YES ', "{% if #{awful_markup} %} YES {% endif %}", assigns)
end end
def test_comparison_of_expressions_starting_with_and_or_or def test_comparison_of_expressions_starting_with_and_or_or
assigns = {'order' => {'items_count' => 0}, 'android' => {'name' => 'Roy'}} assigns = { 'order' => { 'items_count' => 0 }, 'android' => { 'name' => 'Roy' } }
assert_template_result( "YES", assert_template_result("YES",
"{% if android.name == 'Roy' %}YES{% endif %}", "{% if android.name == 'Roy' %}YES{% endif %}",
assigns) assigns)
assert_template_result( "YES", assert_template_result("YES",
"{% if order.items_count == 0 %}YES{% endif %}", "{% if order.items_count == 0 %}YES{% endif %}",
assigns) assigns)
end end
def test_if_and def test_if_and
assert_template_result(' YES ','{% if true and true %} YES {% endif %}') assert_template_result(' YES ', '{% if true and true %} YES {% endif %}')
assert_template_result('','{% if false and true %} YES {% endif %}') assert_template_result('', '{% if false and true %} YES {% endif %}')
assert_template_result('','{% if false and true %} YES {% endif %}') assert_template_result('', '{% if false and true %} YES {% endif %}')
end end
def test_hash_miss_generates_false def test_hash_miss_generates_false
assert_template_result('','{% if foo.bar %} NO {% endif %}', 'foo' => {}) assert_template_result('', '{% if foo.bar %} NO {% endif %}', 'foo' => {})
end end
def test_if_from_variable def test_if_from_variable
assert_template_result('','{% if var %} NO {% endif %}', 'var' => false) assert_template_result('', '{% if var %} NO {% endif %}', 'var' => false)
assert_template_result('','{% if var %} NO {% endif %}', 'var' => nil) assert_template_result('', '{% if var %} NO {% endif %}', 'var' => nil)
assert_template_result('','{% if foo.bar %} NO {% endif %}', 'foo' => {'bar' => false}) assert_template_result('', '{% if foo.bar %} NO {% endif %}', 'foo' => { 'bar' => false })
assert_template_result('','{% if foo.bar %} NO {% endif %}', 'foo' => {}) assert_template_result('', '{% if foo.bar %} NO {% endif %}', 'foo' => {})
assert_template_result('','{% if foo.bar %} NO {% endif %}', 'foo' => nil) assert_template_result('', '{% if foo.bar %} NO {% endif %}', 'foo' => nil)
assert_template_result('','{% if foo.bar %} NO {% endif %}', 'foo' => true) assert_template_result('', '{% if foo.bar %} NO {% endif %}', 'foo' => true)
assert_template_result(' YES ','{% if var %} YES {% endif %}', 'var' => "text") assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => "text")
assert_template_result(' YES ','{% if var %} YES {% endif %}', 'var' => true) assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => true)
assert_template_result(' YES ','{% if var %} YES {% endif %}', 'var' => 1) assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => 1)
assert_template_result(' YES ','{% if var %} YES {% endif %}', 'var' => {}) assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => {})
assert_template_result(' YES ','{% if var %} YES {% endif %}', 'var' => []) assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => [])
assert_template_result(' YES ','{% if "foo" %} YES {% endif %}') assert_template_result(' YES ', '{% if "foo" %} YES {% endif %}')
assert_template_result(' YES ','{% if foo.bar %} YES {% endif %}', 'foo' => {'bar' => true}) assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => true })
assert_template_result(' YES ','{% if foo.bar %} YES {% endif %}', 'foo' => {'bar' => "text"}) assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => "text" })
assert_template_result(' YES ','{% if foo.bar %} YES {% endif %}', 'foo' => {'bar' => 1 }) assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => 1 })
assert_template_result(' YES ','{% if foo.bar %} YES {% endif %}', 'foo' => {'bar' => {} }) assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => {} })
assert_template_result(' YES ','{% if foo.bar %} YES {% endif %}', 'foo' => {'bar' => [] }) assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => [] })
assert_template_result(' YES ','{% if var %} NO {% else %} YES {% endif %}', 'var' => false) assert_template_result(' YES ', '{% if var %} NO {% else %} YES {% endif %}', 'var' => false)
assert_template_result(' YES ','{% if var %} NO {% else %} YES {% endif %}', 'var' => nil) assert_template_result(' YES ', '{% if var %} NO {% else %} YES {% endif %}', 'var' => nil)
assert_template_result(' YES ','{% if var %} YES {% else %} NO {% endif %}', 'var' => true) assert_template_result(' YES ', '{% if var %} YES {% else %} NO {% endif %}', 'var' => true)
assert_template_result(' YES ','{% if "foo" %} YES {% else %} NO {% endif %}', 'var' => "text") assert_template_result(' YES ', '{% if "foo" %} YES {% else %} NO {% endif %}', 'var' => "text")
assert_template_result(' YES ','{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => {'bar' => false}) assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => { 'bar' => false })
assert_template_result(' YES ','{% if foo.bar %} YES {% else %} NO {% endif %}', 'foo' => {'bar' => true}) assert_template_result(' YES ', '{% if foo.bar %} YES {% else %} NO {% endif %}', 'foo' => { 'bar' => true })
assert_template_result(' YES ','{% if foo.bar %} YES {% else %} NO {% endif %}', 'foo' => {'bar' => "text"}) assert_template_result(' YES ', '{% if foo.bar %} YES {% else %} NO {% endif %}', 'foo' => { 'bar' => "text" })
assert_template_result(' YES ','{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => {'notbar' => true}) assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => { 'notbar' => true })
assert_template_result(' YES ','{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => {}) assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => {})
assert_template_result(' YES ','{% if foo.bar %} NO {% else %} YES {% endif %}', 'notfoo' => {'bar' => true}) assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', 'notfoo' => { 'bar' => true })
end end
def test_nested_if def test_nested_if
@@ -105,31 +109,30 @@ class IfElseTagTest < Minitest::Test
assert_template_result(' YES ', '{% if true %}{% if true %} YES {% else %} NO {% endif %}{% else %} NO {% endif %}') assert_template_result(' YES ', '{% if true %}{% if true %} YES {% else %} NO {% endif %}{% else %} NO {% endif %}')
assert_template_result(' YES ', '{% if true %}{% if false %} NO {% else %} YES {% endif %}{% else %} NO {% endif %}') assert_template_result(' YES ', '{% if true %}{% if false %} NO {% else %} YES {% endif %}{% else %} NO {% endif %}')
assert_template_result(' YES ', '{% if false %}{% if true %} NO {% else %} NONO {% endif %}{% else %} YES {% endif %}') assert_template_result(' YES ', '{% if false %}{% if true %} NO {% else %} NONO {% endif %}{% else %} YES {% endif %}')
end end
def test_comparisons_on_null def test_comparisons_on_null
assert_template_result('','{% if null < 10 %} NO {% endif %}') assert_template_result('', '{% if null < 10 %} NO {% endif %}')
assert_template_result('','{% if null <= 10 %} NO {% endif %}') assert_template_result('', '{% if null <= 10 %} NO {% endif %}')
assert_template_result('','{% if null >= 10 %} NO {% endif %}') assert_template_result('', '{% if null >= 10 %} NO {% endif %}')
assert_template_result('','{% if null > 10 %} NO {% endif %}') assert_template_result('', '{% if null > 10 %} NO {% endif %}')
assert_template_result('','{% if 10 < null %} NO {% endif %}') assert_template_result('', '{% if 10 < null %} NO {% endif %}')
assert_template_result('','{% if 10 <= null %} NO {% endif %}') assert_template_result('', '{% if 10 <= null %} NO {% endif %}')
assert_template_result('','{% if 10 >= null %} NO {% endif %}') assert_template_result('', '{% if 10 >= null %} NO {% endif %}')
assert_template_result('','{% if 10 > null %} NO {% endif %}') assert_template_result('', '{% if 10 > null %} NO {% endif %}')
end end
def test_else_if def test_else_if
assert_template_result('0','{% if 0 == 0 %}0{% elsif 1 == 1%}1{% else %}2{% endif %}') assert_template_result('0', '{% if 0 == 0 %}0{% elsif 1 == 1%}1{% else %}2{% endif %}')
assert_template_result('1','{% if 0 != 0 %}0{% elsif 1 == 1%}1{% else %}2{% endif %}') assert_template_result('1', '{% if 0 != 0 %}0{% elsif 1 == 1%}1{% else %}2{% endif %}')
assert_template_result('2','{% if 0 != 0 %}0{% elsif 1 != 1%}1{% else %}2{% endif %}') assert_template_result('2', '{% if 0 != 0 %}0{% elsif 1 != 1%}1{% else %}2{% endif %}')
assert_template_result('elsif','{% if false %}if{% elsif true %}elsif{% endif %}') assert_template_result('elsif', '{% if false %}if{% elsif true %}elsif{% endif %}')
end end
def test_syntax_error_no_variable def test_syntax_error_no_variable
assert_raises(SyntaxError){ assert_template_result('', '{% if jerry == 1 %}')} assert_raises(SyntaxError){ assert_template_result('', '{% if jerry == 1 %}') }
end end
def test_syntax_error_no_expression def test_syntax_error_no_expression
@@ -151,7 +154,7 @@ class IfElseTagTest < Minitest::Test
Condition.operators['contains'] = :[] Condition.operators['contains'] = :[]
assert_template_result('yes', assert_template_result('yes',
%({% if 'gnomeslab-and-or-liquid' contains 'gnomeslab-and-or-liquid' %}yes{% endif %})) %({% if 'gnomeslab-and-or-liquid' contains 'gnomeslab-and-or-liquid' %}yes{% endif %}))
ensure ensure
Condition.operators['contains'] = original_op Condition.operators['contains'] = original_op
end end
@@ -161,4 +164,25 @@ class IfElseTagTest < Minitest::Test
assert_template_result('', %({% if 1 or throw or or 1 %}yes{% endif %})) assert_template_result('', %({% if 1 or throw or or 1 %}yes{% endif %}))
end end
end end
def test_multiple_conditions
tpl = "{% if a or b and c %}true{% else %}false{% endif %}"
tests = {
[true, true, true] => true,
[true, true, false] => true,
[true, false, true] => true,
[true, false, false] => true,
[false, true, true] => true,
[false, true, false] => false,
[false, false, true] => false,
[false, false, false] => false,
}
tests.each do |vals, expected|
a, b, c = vals
assigns = { 'a' => a, 'b' => b, 'c' => c }
assert_template_result expected.to_s, tpl, assigns, assigns.to_s
end
end
end end

Some files were not shown because too many files have changed in this diff Show More