Compare commits

..

269 Commits

Author SHA1 Message Date
Simon Génier
a61903da54 Add support for iterating ranges with float boundaries.
The meaning I gave is to start at the lower bound and step by one. This has the
advantage of having (n..m) and ((n.to_f)..(m.to_f)) behave the same. Another
thing we could do that have this same property is to cast bounds to integer.
This would do the right thing if there is a floating point rounding error, but I
feel it is more surprising for cases such as (1.5..8).

Note that it is not possible to create ranges with floating point boundaries
directly in Liquid at the moment. However, since there is no distinction between
Ruby and Liquid ranges, a drop can introduce such a value by returning it.
2017-08-09 12:13:00 -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
117 changed files with 3517 additions and 2246 deletions

1
.gitignore vendored
View File

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

125
.rubocop.yml Normal file
View File

@@ -0,0 +1,125 @@
inherit_from: ./.rubocop_todo.yml
AllCops:
Exclude:
- 'performance/shopify/*'
- 'pkg/**'
Metrics/BlockNesting:
Max: 3
Exclude:
- 'lib/liquid/block_body.rb'
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
Lint/EndAlignment:
AlignWith: 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
Style/SpaceInsideBrackets:
Enabled: false
Style/SpaceBeforeBlockBraces:
Enabled: false
Style/Documentation:
Enabled: false
Style/ClassAndModuleChildren:
Enabled: false
Style/TrailingComma:
Enabled: false
Style/IndentHash:
EnforcedStyle: consistent
Style/FormatString:
Enabled: false
Style/AlignParameters:
EnforcedStyle: with_fixed_indentation
Style/MultilineOperationIndentation:
EnforcedStyle: indented
Style/IfUnlessModifier:
Enabled: false
Style/RaiseArgs:
Enabled: false
Style/DeprecatedHashMethods:
Enabled: false
Style/RegexpLiteral:
Enabled: false
Style/SymbolLiteral:
Enabled: false
Performance/Count:
Enabled: false
Style/ConstantName:
Enabled: false
Style/CaseIndentation:
Enabled: false
Style/ClassVars:
Enabled: false
Style/PerlBackrefs:
Enabled: false
Style/TrivialAccessors:
AllowPredicates: true
Style/WordArray:
Enabled: false

72
.rubocop_todo.yml Normal file
View File

@@ -0,0 +1,72 @@
# This configuration was generated by `rubocop --auto-gen-config`
# on 2015-06-08 18:16:16 +0000 using RuboCop version 0.32.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: 5
Lint/NestedMethodDefinition:
Enabled: false
# Offense count: 53
Metrics/AbcSize:
Max: 58
# Offense count: 12
Metrics/CyclomaticComplexity:
Max: 15
# Offense count: 553
# Configuration parameters: AllowURI, URISchemes.
Metrics/LineLength:
Max: 294
# Offense count: 77
# Configuration parameters: CountComments.
Metrics/MethodLength:
Max: 46
# Offense count: 6
Metrics/PerceivedComplexity:
Max: 13
# Offense count: 1
Style/AccessorMethodName:
Enabled: false
# Offense count: 1
# Cop supports --auto-correct.
Style/ClosingParenthesisIndentation:
Enabled: false
# Offense count: 3
# Configuration parameters: MinBodyLength.
Style/GuardClause:
Enabled: false
# Offense count: 4
# Configuration parameters: EnforcedStyle, SupportedStyles.
Style/MethodName:
Enabled: false
# Offense count: 1
Style/MultilineBlockChain:
Enabled: false
# Offense count: 2
# Configuration parameters: EnforcedStyle, MinBodyLength, SupportedStyles.
Style/Next:
Enabled: false
# Offense count: 7
# Cop supports --auto-correct.
# Configuration parameters: AllowAsExpressionSeparator.
Style/Semicolon:
Enabled: false
# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: MaxLineLength.
Style/WhileUntilModifier:
Enabled: false

View File

@@ -1,20 +1,29 @@
language: ruby language: ruby
rvm: rvm:
- 2.0
- 2.1 - 2.1
- 2.2 - 2.2
- 2.3.3
- ruby-head - ruby-head
- jruby-head - jruby-head
- rbx-2 # - rbx-2
sudo: false sudo: false
addons:
apt:
packages:
- libgmp3-dev
matrix: matrix:
allow_failures: allow_failures:
- rvm: jruby-head - rvm: jruby-head
script: "rake test" install:
- gem install rainbow -v 2.2.1
- 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.

14
Gemfile
View File

@@ -1,9 +1,17 @@
source 'https://rubygems.org' source 'https://rubygems.org'
gemspec gemspec
gem 'stackprof', platforms: :mri_21
group :test do gem 'stackprof', platforms: :mri
gem 'spy', '0.4.1'
group :benchmark, :test do
gem 'benchmark-ips' gem 'benchmark-ips'
end end
group :test do
gem 'rubocop', '0.34.2'
platform :mri do
gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'bd53db95de3d44d631e7c5a267c3d934e66107dd'
end
end

View File

@@ -1,26 +1,63 @@
# Liquid Change Log # Liquid Change Log
## 4.0.0 / not yet released / branch "master" ## 4.0.0 / 2016-12-14 / branch "4-0-stable"
### Changed ### Changed
* Add sort_natural filter (#554) [Martin Hanzel, arthanzel] * Render an opaque internal error by default for non-Liquid::Error (#835) [Dylan Thacker-Smith]
* Add forloop.parentloop as a reference to the parent loop (#520) [Justin Li, pushrax] * Ruby 2.0 support dropped (#832) [Dylan Thacker-Smith]
* Block parsing moved to BlockBody class (#458) [Dylan Thacker-Smith, dylanahsmith] * Add to_number Drop method to allow custom drops to work with number filters (#731)
* Add concat filter to concatenate arrays (#429) [Diogo Beato, dvbeato] * Add strict_variables and strict_filters options to detect undefined references (#691)
* Ruby 1.9 support dropped (#491) [Justin Li, pushrax] * Improve loop performance (#681) [Florian Weingarten]
* Liquid::Template.file_system's read_template_file method is no longer passed the context. (#441) [James Reid-Smith, sunblaze] * 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 ### Fixed
* Fix naming of the "context variable" when dynamically including a template (#559) [Justin Li, pushrax]
* Gracefully accept empty strings in the date filter (#555) [Loren Hale, boobooninja]
* Fix capturing into variables with a hyphen in the name (#505) [Florian Weingarten, fw42]
* Fix case sensitivity regression in date standard filter (#499) [Kelley Reynolds, kreynolds]
* Disallow filters with no variable in strict mode (#475) [Justin Li, pushrax]
* Disallow variable names in the strict parser that are not valid in the lax parser (#463) [Justin Li, pushrax]
* Fix BlockBody#warnings taking exponential time to compute (#486) [Justin Li, pushrax]
## 3.0.2 / 2015-04-24 / branch "3-0-stable" * 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]
* Expose VariableLookup private members (#551) [Justin Li, pushrax] ## 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 * Documentation fixes
## 3.0.1 / 2015-01-23 ## 3.0.1 / 2015-01-23
@@ -29,44 +66,52 @@
## 3.0.0 / 2014-11-12 ## 3.0.0 / 2014-11-12
* Removed Block#end_tag. Instead, override parse with `super` followed by your code. See #446 [Dylan Thacker-Smith, dylanahsmith] * 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] * Fixed condition with wrong data types (#423) [Bogdan Gusiev]
* Add url_encode to standard filters (#421) [Derrick Reimer, djreimer] * Add url_encode to standard filters (#421) [Derrick Reimer]
* Add uniq to standard filters [Florian Weingarten, fw42] * Add uniq to standard filters [Florian Weingarten]
* Add exception_handler feature (#397) and #254 [Bogdan Gusiev, bogdan and Florian Weingarten, fw42] * 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, jasonhl] * 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, jasonhl] * Optimize checking for block interrupts to reduce object allocation #380 [Jason Hiltz-Laforge]
* Properly set context rethrow_errors on render! #349 [Thierry Joyal, tjoyal] * Properly set context rethrow_errors on render! #349 [Thierry Joyal]
* Fix broken rendering of variables which are equal to false (#345) [Florian Weingarten, fw42] * Fix broken rendering of variables which are equal to false (#345) [Florian Weingarten]
* Remove ActionView template handler [Dylan Thacker-Smith, dylanahsmith] * Remove ActionView template handler [Dylan Thacker-Smith]
* Freeze lots of string literals for new Ruby 2.1 optimization (#297) [Florian Weingarten, fw42] * Freeze lots of string literals for new Ruby 2.1 optimization (#297) [Florian Weingarten]
* Allow newlines in tags and variables (#324) [Dylan Thacker-Smith, dylanahsmith] * 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, dylanahsmith] * 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, bogdan] * 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, releod] * Add a to_s default for liquid drops (#306) [Adam Doeler]
* Add strip, lstrip, and rstrip to standard filters [Florian Weingarten, fw42] * Add strip, lstrip, and rstrip to standard filters [Florian Weingarten]
* Make if, for & case tags return complete and consistent nodelists (#250) [Nick Jones, dntj] * Make if, for & case tags return complete and consistent nodelists (#250) [Nick Jones]
* Prevent arbitrary method invocation on condition objects (#274) [Dylan Thacker-Smith, dylanahsmith] * 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, bouk] * 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, fw42] * Fix resource counting bug with respond_to?(:length) (#263) [Florian Weingarten]
* Allow specifying custom patterns for template filenames (#284) [Andrei Gladkyi, agladkyi] * Allow specifying custom patterns for template filenames (#284) [Andrei Gladkyi]
* Allow drops to optimize loading a slice of elements (#282) [Tom Burns, boourns] * Allow drops to optimize loading a slice of elements (#282) [Tom Burns]
* Support for passing variables to snippets in subdirs (#271) [Joost Hietbrink, joost] * Support for passing variables to snippets in subdirs (#271) [Joost Hietbrink]
* Add a class cache to avoid runtime extend calls (#249) [James Tucker, raggi] * Add a class cache to avoid runtime extend calls (#249) [James Tucker]
* Remove some legacy Ruby 1.8 compatibility code (#276) [Florian Weingarten, fw42] * Remove some legacy Ruby 1.8 compatibility code (#276) [Florian Weingarten]
* Add default filter to standard filters (#267) [Derrick Reimer, djreimer] * Add default filter to standard filters (#267) [Derrick Reimer]
* Add optional strict parsing and warn parsing (#235) [Tristan Hume, trishume] * Add optional strict parsing and warn parsing (#235) [Tristan Hume]
* Add I18n syntax error translation (#241) [Simon Hørup Eskildsen, Sirupsen] * Add I18n syntax error translation (#241) [Simon Hørup Eskildsen, Sirupsen]
* Make sort filter work on enumerable drops (#239) [Florian Weingarten, fw42] * Make sort filter work on enumerable drops (#239) [Florian Weingarten]
* Fix clashing method names in enumerable drops (#238) [Florian Weingarten, fw42] * Fix clashing method names in enumerable drops (#238) [Florian Weingarten]
* Make map filter work on enumerable drops (#233) [Florian Weingarten, fw42] * Make map filter work on enumerable drops (#233) [Florian Weingarten]
* Improved whitespace stripping for blank blocks, related to #216 [Florian Weingarten, fw42] * Improved whitespace stripping for blank blocks, related to #216 [Florian Weingarten]
## 2.6.1 / 2014-01-10 / branch "2-6-stable" ## 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 (#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 (#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
@@ -74,37 +119,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 (#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 (#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" (#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
@@ -112,7 +157,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

@@ -73,3 +73,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: [:rubocop, :test]
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'
Rake::Task['base_test'].reenable
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' 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
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,13 @@ namespace :profile do
task :strict do task :strict do
ruby "./performance/profile.rb strict" ruby "./performance/profile.rb strict"
end 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

3
circle.yml Normal file
View File

@@ -0,0 +1,3 @@
machine:
ruby:
version: ruby-2.1

View File

@@ -17,7 +17,6 @@ module ProductsFilter
end end
class Servlet < LiquidServlet class Servlet < LiquidServlet
def index def index
{ 'date' => Time.now } { 'date' => Time.now }
end end
@@ -42,5 +41,4 @@ class Servlet < LiquidServlet
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
@@ -20,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

@@ -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
@@ -48,6 +49,8 @@ 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'
@@ -67,10 +70,10 @@ 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'
# 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 }

View File

@@ -1,5 +1,7 @@
module Liquid module Liquid
class Block < Tag class Block < Tag
MAX_DEPTH = 100
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
@blank = true @blank = true
@@ -7,7 +9,7 @@ module Liquid
def parse(tokens) def parse(tokens)
@body = BlockBody.new @body = BlockBody.new
while more = parse_body(@body, tokens) while parse_body(@body, tokens)
end end
end end
@@ -23,29 +25,17 @@ module Liquid
@body.nodelist @body.nodelist
end end
# warnings of this block and all sub-tags def unknown_tag(tag, _params, _tokens)
def warnings
all_warnings = []
all_warnings.concat(@warnings) if @warnings
(nodelist || []).each do |node|
all_warnings.concat(node.warnings || []) if node.respond_to?(:warnings)
end
all_warnings
end
def unknown_tag(tag, params, tokens)
case tag case tag
when 'else'.freeze when 'else'.freeze
raise SyntaxError.new(options[:locale].t("errors.syntax.unexpected_else".freeze, raise SyntaxError.new(parse_context.locale.t("errors.syntax.unexpected_else".freeze,
:block_name => block_name)) block_name: block_name))
when 'end'.freeze when 'end'.freeze
raise SyntaxError.new(options[:locale].t("errors.syntax.invalid_delimiter".freeze, raise SyntaxError.new(parse_context.locale.t("errors.syntax.invalid_delimiter".freeze,
:block_name => block_name, block_name: block_name,
:block_delimiter => block_delimiter)) 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
@@ -57,25 +47,29 @@ module Liquid
@block_delimiter ||= "end#{block_name}" @block_delimiter ||= "end#{block_name}"
end end
def format
"{% #{raw} %}#{@body.format}{% #{block_delimiter} %}"
end
protected protected
def parse_body(body, tokens) def parse_body(body, tokens)
body.parse(tokens, options) do |end_tag_name, end_tag_params| if parse_context.depth >= MAX_DEPTH
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? @blank &&= body.blank?
return false if end_tag_name == block_delimiter return false if end_tag_name == block_delimiter
unless end_tag_name unless end_tag_name
raise SyntaxError.new(@options[:locale].t("errors.syntax.tag_never_closed".freeze, :block_name => block_name)) raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name))
end end
# this tag is not registered with the system # this tag is not registered with the system
# pass it to the current block for special handling or error reporting # pass it to the current block for special handling or error reporting
unknown_tag(end_tag_name, end_tag_params, tokens) unknown_tag(end_tag_name, end_tag_params, tokens)
end end
ensure
parse_context.depth -= 1
end
true true
end end

View File

@@ -1,7 +1,7 @@
module Liquid module Liquid
class BlockBody class BlockBody
FullToken = /\A#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om FullToken = /\A#{TagStart}#{WhitespaceControl}?\s*(\w+)\s*(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
ContentOfVariable = /\A#{VariableStart}(.*)#{VariableEnd}\z/om ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
TAGSTART = "{%".freeze TAGSTART = "{%".freeze
VARSTART = "{{".freeze VARSTART = "{{".freeze
@@ -12,20 +12,19 @@ module Liquid
@blank = true @blank = true
end end
def parse(tokens, options) def parse(tokenizer, parse_context)
while token = tokens.shift parse_context.line_number = tokenizer.line_number
begin while token = tokenizer.shift
unless token.empty? unless token.empty?
case case
when token.start_with?(TAGSTART) when token.start_with?(TAGSTART)
whitespace_handler(token, parse_context)
if token =~ FullToken if token =~ FullToken
tag_name = $1 tag_name = $1
markup = $2 markup = $2
# fetch the tag from registered blocks # fetch the tag from registered blocks
if tag = Template.tags[tag_name] if tag = registered_tags[tag_name]
markup = token.child(markup) if token.is_a?(Token) new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
new_tag = tag.parse(tag_name, markup, tokens, options)
new_tag.line_number = token.line_number if token.is_a?(Token)
@blank &&= new_tag.blank? @blank &&= new_tag.blank?
@nodelist << new_tag @nodelist << new_tag
else else
@@ -34,37 +33,39 @@ module Liquid
return yield tag_name, markup return yield tag_name, markup
end end
else else
raise_missing_tag_terminator(token, options) raise_missing_tag_terminator(token, parse_context)
end end
when token.start_with?(VARSTART) when token.start_with?(VARSTART)
new_var = create_variable(token, options) whitespace_handler(token, parse_context)
new_var.line_number = token.line_number if token.is_a?(Token) @nodelist << create_variable(token, parse_context)
@nodelist << new_var
@blank = false @blank = false
else else
if parse_context.trim_whitespace
token.lstrip!
end
parse_context.trim_whitespace = false
@nodelist << token @nodelist << token
@blank &&= !!(token =~ /\A\s*\z/) @blank &&= !!(token =~ /\A\s*\z/)
end end
end end
rescue SyntaxError => e parse_context.line_number = tokenizer.line_number
e.set_line_number_from_token(token)
raise
end
end end
yield nil, nil yield nil, nil
end end
def blank? def whitespace_handler(token, parse_context)
@blank 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 end
def warnings def blank?
all_warnings = [] @blank
nodelist.each do |node|
all_warnings.concat(node.warnings || []) if node.respond_to?(:warnings)
end
all_warnings
end end
def render(context) def render(context)
@@ -73,71 +74,67 @@ module Liquid
@nodelist.each do |token| @nodelist.each do |token|
# Break out if we have any unhanded interrupts. # Break out if we have any unhanded interrupts.
break if context.has_interrupt? break if context.interrupt?
begin begin
# If we get an Interrupt that means the block must stop processing. An # If we get an Interrupt that means the block must stop processing. An
# Interrupt is any command that stops block execution such as {% break %} # Interrupt is any command that stops block execution such as {% break %}
# or {% continue %} # or {% continue %}
if token.is_a?(Continue) or token.is_a?(Break) if token.is_a?(Continue) || token.is_a?(Break)
context.push_interrupt(token.interrupt) context.push_interrupt(token.interrupt)
break break
end end
token_output = render_token(token, context) node_output = render_node(token, context)
unless token.is_a?(Block) && token.blank? unless token.is_a?(Block) && token.blank?
output << token_output output << node_output
end end
rescue MemoryError => e rescue MemoryError => e
raise e raise e
rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
context.handle_error(e, token.line_number)
output << nil
rescue ::StandardError => e rescue ::StandardError => e
output << context.handle_error(e, token) line_number = token.is_a?(String) ? nil : token.line_number
output << context.handle_error(e, line_number)
end end
end end
output.join output.join
end end
def format
@nodelist.map do |node|
if node.is_a?(Variable)
"{{ #{node.format} }}"
elsif node.is_a?(Tag)
node.format
else
node
end
end.join
end
private private
def render_token(token, context) def render_node(node, context)
token_output = (token.respond_to?(:render) ? token.render(context) : token) node_output = node.is_a?(String) ? node : node.render(context)
token_str = token_output.is_a?(Array) ? token_output.join : token_output.to_s node_output = node_output.is_a?(Array) ? node_output.join : node_output.to_s
context.resource_limits.render_length += token_str.length context.resource_limits.render_length += node_output.length
if context.resource_limits.reached? if context.resource_limits.reached?
raise MemoryError.new("Memory limits exceeded".freeze) raise MemoryError.new("Memory limits exceeded".freeze)
end end
token_str node_output
end end
def create_variable(token, options) def create_variable(token, parse_context)
token.scan(ContentOfVariable) do |content| token.scan(ContentOfVariable) do |content|
markup = token.is_a?(Token) ? token.child(content.first) : content.first markup = content.first
return Variable.new(markup, options) return Variable.new(markup, parse_context)
end end
raise_missing_variable_terminator(token, options) raise_missing_variable_terminator(token, parse_context)
end end
def raise_missing_tag_terminator(token, options) def raise_missing_tag_terminator(token, parse_context)
raise SyntaxError.new(options[:locale].t("errors.syntax.tag_termination".freeze, :token => token, :tag_end => TagEnd.inspect)) raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_termination".freeze, token: token, tag_end: TagEnd.inspect))
end end
def raise_missing_variable_terminator(token, options) def raise_missing_variable_terminator(token, parse_context)
raise SyntaxError.new(options[:locale].t("errors.syntax.variable_termination".freeze, :token => token, :tag_end => VariableEnd.inspect)) 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 end
end end

View File

@@ -8,16 +8,21 @@ module Liquid
# #
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
@@ -33,25 +38,25 @@ module Liquid
@right = right @right = right
@child_relation = nil @child_relation = nil
@child_condition = nil @child_condition = nil
if right && right.is_a?(VariableLookup) && right.name.nil?
# Strip blank variable lookups resulting from cases like:
# {% if foo && bar %}
@right = nil
end
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 end
condition = condition.child_condition
end
result
end end
def or(condition) def or(condition)
@@ -76,34 +81,24 @@ module Liquid
"#<Condition #{[@left, @operator, @right].compact.join(' '.freeze)}>" "#<Condition #{[@left, @operator, @right].compact.join(' '.freeze)}>"
end end
def format protected
out = if operator.nil?
Expression.format(left)
else
"#{Expression.format(left)} #{operator} #{Expression.format(right)}"
end
if @child_relation attr_reader :child_relation, :child_condition
"#{out} #{@child_relation.to_s} #{@child_condition.format}"
else
out
end
end
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
@@ -116,7 +111,7 @@ 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.evaluate(left) if op == nil return context.evaluate(left) if op.nil?
left = context.evaluate(left) left = context.evaluate(left)
right = context.evaluate(right) right = context.evaluate(right)
@@ -125,27 +120,23 @@ module Liquid
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
nil
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,24 +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 = []
@partial = false
@strict_variables = false
@resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits) @resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
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
def warnings
@warnings ||= []
end end
def strainer def strainer
@@ -48,8 +55,12 @@ module Liquid
@strainer = nil @strainer = nil
end end
def apply_global_filter(obj)
global_filter.nil? ? obj : global_filter.call(obj)
end
# are there any not handled interrupts? # are there any not handled interrupts?
def has_interrupt? def interrupt?
!@interrupts.empty? !@interrupts.empty?
end end
@@ -63,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)
@@ -81,7 +89,7 @@ module Liquid
# 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
@@ -143,7 +151,7 @@ module Liquid
evaluate(Expression.parse(expression)) evaluate(Expression.parse(expression))
end end
def has_key?(key) def key?(key)
self[key] != nil self[key] != nil
end end
@@ -152,18 +160,17 @@ 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? unless variable.nil?
scope = e scope = e
break break
@@ -172,16 +179,22 @@ module Liquid
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
@@ -189,10 +202,18 @@ module Liquid
end end
private private
def internal_error
# raise and catch to set backtrace and cause on exception
raise Liquid::InternalError, 'internal'
rescue Liquid::InternalError => exc
exc
end
def squash_instance_assigns_with_environments def squash_instance_assigns_with_environments
@scopes.last.each_key do |k| @scopes.last.each_key do |k|
@environments.each do |env| @environments.each do |env|
if env.has_key?(k) if env.key?(k)
scopes.last[k] = lookup_and_evaluate(env, k) scopes.last[k] = lookup_and_evaluate(env, k)
break break
end end

View File

@@ -1,23 +1,26 @@
module Liquid module Liquid
class Document < BlockBody class Document < BlockBody
def self.parse(tokens, options) def self.parse(tokens, parse_context)
doc = new doc = new
doc.parse(tokens, options) doc.parse(tokens, parse_context)
doc doc
end end
def parse(tokens, options) def parse(tokens, parse_context)
super do |end_tag_name, end_tag_params| super do |end_tag_name, end_tag_params|
unknown_tag(end_tag_name, options) if end_tag_name unknown_tag(end_tag_name, parse_context) if end_tag_name
end end
rescue SyntaxError => e
e.line_number ||= parse_context.line_number
raise
end end
def unknown_tag(tag, options) def unknown_tag(tag, parse_context)
case tag case tag
when 'else'.freeze, 'end'.freeze when 'else'.freeze, 'end'.freeze
raise SyntaxError.new(options[:locale].t("errors.syntax.unexpected_outer_tag".freeze, :tag => tag)) raise SyntaxError.new(parse_context.locale.t("errors.syntax.unexpected_outer_tag".freeze, tag: tag))
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
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,26 +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)
self.invokable_methods.include?(method_name.to_s) invokable_methods.include?(method_name.to_s)
end end
def self.invokable_methods def self.invokable_methods
unless @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
end end
end end
end end

View File

@@ -1,6 +1,7 @@
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)
@@ -16,20 +17,6 @@ module Liquid
str str
end end
def set_line_number_from_token(token)
return unless token.respond_to?(:line_number)
return if self.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
@@ -41,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 << ": "
@@ -49,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 FileSystemError < Error; end FileSystemError = Class.new(Error)
class StandardError < Error; end StandardError = Class.new(Error)
class SyntaxError < Error; end SyntaxError = Class.new(Error)
class StackLevelError < Error; end StackLevelError = Class.new(Error)
class TaintedError < 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,15 +1,26 @@
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
} }
INVERTED_LITERALS = LITERALS.invert
def self.parse(markup) def self.parse(markup)
if LITERALS.key?(markup) if LITERALS.key?(markup)
LITERALS[markup] LITERALS[markup]
@@ -30,19 +41,5 @@ module Liquid
end end
end end
end end
def self.format(value)
if INVERTED_LITERALS.key?(value)
INVERTED_LITERALS[value].dup
elsif value.is_a?(VariableLookup) || value.is_a?(RangeLookup)
value.format
elsif value.is_a?(String)
"\"#{value}\""
elsif value.is_a?(Range)
"(#{value.to_s})"
else
value.to_s
end
end
end end
end end

View File

@@ -25,6 +25,12 @@ class Numeric # :nodoc:
end end
end end
class Range # :nodoc:
def to_liquid
self
end
end
class Time # :nodoc: class Time # :nodoc:
def to_liquid def to_liquid
self self

View File

@@ -14,7 +14,7 @@ module Liquid
# 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) 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
@@ -51,7 +51,7 @@ module Liquid
def read_template_file(template_path) 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]}"
} end
end end
def deep_fetch_translation(name) def deep_fetch_translation(name)

View File

@@ -1,5 +1,4 @@
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

View File

@@ -18,17 +18,18 @@ module Liquid
DOUBLE_STRING_LITERAL = /"[^\"]*"/ DOUBLE_STRING_LITERAL = /"[^\"]*"/
NUMBER_LITERAL = /-?\d+(\.\d+)?/ NUMBER_LITERAL = /-?\d+(\.\d+)?/
DOTDOT = /\.\./ DOTDOT = /\.\./
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains/ COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\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(/\s*/)
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]

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]"
@@ -21,3 +22,5 @@
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,64 +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
#
#
# 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

@@ -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,7 +1,6 @@
require 'liquid/profiler/hooks' require 'liquid/profiler/hooks'
module Liquid 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, first require 'liquid/profiler'. # To enable profiling, first require 'liquid/profiler'.
@@ -20,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.
@@ -47,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
@@ -73,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
@@ -135,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
@@ -157,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,13 +1,13 @@
module Liquid module Liquid
class BlockBody class BlockBody
def render_token_with_profiling(token, context) def render_node_with_profiling(node, context)
Profiler.profile_token_render(token) do Profiler.profile_node_render(node) do
render_token_without_profiling(token, context) render_node_without_profiling(node, context)
end end
end end
alias_method :render_token_without_profiling, :render_token alias_method :render_node_without_profiling, :render_node
alias_method :render_token, :render_token_with_profiling alias_method :render_node, :render_node_with_profiling
end end
class Include < Tag class Include < Tag

View File

@@ -16,11 +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))
start_int..end_int
end end
def format private
"(#{Expression.format(@start_obj)}..#{Expression.format(@end_obj)})"
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

@@ -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,
@@ -34,7 +33,7 @@ module Liquid
end end
def escape(input) def escape(input)
CGI.escapeHTML(input).untaint rescue input CGI.escapeHTML(input.to_s).untaint unless input.nil?
end end
alias_method :h, :escape alias_method :h, :escape
@@ -43,12 +42,16 @@ 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
def url_decode(input)
CGI.unescape(input.to_s) unless input.nil?
end end
def slice(input, offset, length = nil) def slice(input, offset, length = nil)
offset = Integer(offset) offset = Utils.to_integer(offset)
length = length ? Integer(length) : 1 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 +62,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 +86,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)
@@ -115,10 +122,18 @@ module Liquid
ary = InputIterator.new(input) ary = InputIterator.new(input)
if property.nil? if property.nil?
ary.sort ary.sort
elsif ary.empty? # The next two cases assume a non-empty array.
[]
elsif ary.first.respond_to?(:[]) && !ary.first[property].nil? elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
ary.sort {|a,b| a[property] <=> b[property] } ary.sort do |a, b|
elsif ary.first.respond_to?(property) a = a[property]
ary.sort {|a,b| a.send(property) <=> b.send(property) } b = b[property]
if a && b
a <=> b
else
a ? -1 : 1
end
end
end end
end end
@@ -129,10 +144,10 @@ module Liquid
if property.nil? if property.nil?
ary.sort { |a, b| a.casecmp(b) } ary.sort { |a, b| a.casecmp(b) }
elsif ary.empty? # The next two cases assume a non-empty array.
[]
elsif ary.first.respond_to?(:[]) && !ary.first[property].nil? elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
ary.sort { |a, b| a[property].casecmp(b[property]) } ary.sort { |a, b| a[property].casecmp(b[property]) }
elsif ary.first.respond_to?(property)
ary.sort {|a,b| a.send(property).casecmp(b.send(property)) }
end end
end end
@@ -140,10 +155,13 @@ 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?(:[])
ary.uniq{ |a| a[property] }
end end
end end
@@ -161,29 +179,44 @@ 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
end end
end 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?(:[])
ary.reject{ |a| a[property].nil? }
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
@@ -192,6 +225,9 @@ module Liquid
end end
def concat(input, array) 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) InputIterator.new(input).concat(array)
end end
@@ -239,7 +275,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
@@ -262,6 +298,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, :+)
@@ -280,71 +322,49 @@ 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 default(input, default_value = ''.freeze)
is_blank = input.respond_to?(:empty?) ? input.empty? : !input if !input || input.respond_to?(:empty?) && input.empty?
is_blank ? default_value : input default_value
else
input
end
end end
private private
def to_number(obj)
case obj
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)
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)
else
nil
end
rescue ArgumentError
nil
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
@@ -364,17 +384,30 @@ module Liquid
end end
def join(glue) def join(glue)
to_a.join(glue) to_a.join(glue.to_s)
end end
def concat(args) def concat(args)
to_a.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,7 +1,6 @@
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.
# #
@@ -22,19 +21,25 @@ module Liquid
@context = context @context = context
end end
def self.filter_methods class << self
@filter_methods attr_reader :filter_methods
end end
def self.add_filter(filter) def self.add_filter(filter)
raise ArgumentError, "Expected module but got: #{f.class}" unless filter.is_a?(Module) raise ArgumentError, "Expected module but got: #{filter.class}" unless filter.is_a?(Module)
unless self.class.include?(filter) unless self.include?(filter)
self.send(:include, filter) invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) }
if invokable_non_public_methods.any?
raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}"
else
send(:include, filter)
@filter_methods.merge(filter.public_instance_methods.map(&:to_s)) @filter_methods.merge(filter.public_instance_methods.map(&:to_s))
end end
end end
end
def self.global_filter(filter) def self.global_filter(filter)
@@strainer_class_cache.clear
@@global_strainer.add_filter(filter) @@global_strainer.add_filter(filter)
end end
@@ -49,11 +54,13 @@ module Liquid
def invoke(method, *args) def invoke(method, *args)
if self.class.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 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,49 +1,43 @@
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
tag = @tag_name.strip "#{@tag_name} #{@markup}"
markup = @markup.strip
tag << " #{markup}" unless markup.empty?
tag
end end
def name def name
self.class.name.downcase self.class.name.downcase
end end
def render(context) def render(_context)
''.freeze ''.freeze
end end
def blank? def blank?
false false
end end
def format
"{% #{raw} %}"
end
end end
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' %}
@@ -16,7 +15,6 @@ module Liquid
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,10 +23,7 @@ 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.resource_limits.assign_score += assign_score_of(val)
inc = val.instance_of?(String) || val.instance_of?(Array) || val.instance_of?(Hash) ? val.length : 1
context.resource_limits.assign_score += inc
''.freeze ''.freeze
end end
@@ -36,8 +31,19 @@ module Liquid
true true
end end
def format private
"{% assign #{@to} = #{@from.format} %}"
def assign_score_of(val)
if val.instance_of?(String)
val.length
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 end
end end

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

@@ -32,10 +32,6 @@ module Liquid
def blank? def blank?
true true
end end
def format
"{% #{block_name} #{@to} %}#{@body.format}{% #{block_delimiter} %}"
end
end end
Template.register_tag('capture'.freeze, Capture) Template.register_tag('capture'.freeze, Capture)

View File

@@ -15,8 +15,8 @@ module Liquid
end end
def parse(tokens) def parse(tokens)
@initial_body = body = BlockBody.new body = BlockBody.new
while more = parse_body(body, tokens) while parse_body(body, tokens)
body = @blocks.last.attachment body = @blocks.last.attachment
end end
end end
@@ -53,28 +53,13 @@ module Liquid
end end
end end
def format
out = "{% case #{Expression.format(@left)} %}#{@initial_body.format}"
@blocks.each do |block|
if block.else?
out << "{% else %}"
else
out << "{% when #{Expression.format(block.right)} %}"
end
out << block.attachment.format
end
out + "{% endcase %}"
end
private private
def record_when_condition(markup) def record_when_condition(markup)
body = BlockBody.new body = BlockBody.new
while markup while markup
if not markup =~ WhenSyntax unless 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
@@ -87,7 +72,7 @@ module Liquid
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

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

@@ -21,7 +21,6 @@ module Liquid
when NamedSyntax when NamedSyntax
@variables = variables_from_string($2) @variables = variables_from_string($2)
@name = Expression.parse($1) @name = Expression.parse($1)
@named = true
when SimpleSyntax when SimpleSyntax
@variables = variables_from_string(markup) @variables = variables_from_string(markup)
@name = @variables.to_s @name = @variables.to_s
@@ -44,15 +43,6 @@ module Liquid
end end
end end
def format
formatted_vars = @variables.map { |v| Expression.format(v) }.join(', ')
if @named
"{% cycle #{Expression.format(@name)}: #{formatted_vars} %}"
else
"{% cycle #{formatted_vars} %}"
end
end
private private
def variables_from_string(markup) def variables_from_string(markup)

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:
# #
@@ -47,17 +46,21 @@ module Liquid
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
attr_reader :variable_name
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)
@for_block = BlockBody.new @for_block = BlockBody.new
@else_block = nil
end end
def parse(tokens) def parse(tokens)
if more = parse_body(@for_block, tokens) return unless parse_body(@for_block, tokens)
parse_body(@else_block, tokens) parse_body(@else_block, tokens)
end end
end
def nodelist def nodelist
@else_block ? [@for_block, @else_block] : [@for_block] @else_block ? [@for_block, @else_block] : [@for_block]
@@ -69,79 +72,13 @@ module Liquid
end end
def render(context) def render(context)
context.registers[:for] ||= Hash.new(0) segment = collection_segment(context)
collection = context.evaluate(@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 @from == :continue
context.registers[:for][@name].to_i
else else
context.evaluate(@from).to_i render_segment(context, segment)
end end
limit = context.evaluate(@limit)
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
parent_loop = context['forloop'.freeze]
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),
'parentloop'.freeze => parent_loop
}
result << @for_block.render(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
def format
args = [Expression.format(@collection_name)]
args << "reversed" if @reversed
args << "limit: #{Expression.format(@limit)}" if @limit
args << "offset: " << (@from == :continue ? 'continue' : Expression.format(@from)) if @from
out = "{% for #{@variable_name} in #{args.join(' ')} %}"
out << @for_block.format
if @else_block
out << "{% else %}"
out << @else_block.format
end
out + "{% endfor %}"
end end
protected protected
@@ -150,7 +87,7 @@ module Liquid
if markup =~ Syntax if markup =~ Syntax
@variable_name = $1 @variable_name = $1
collection_name = $2 collection_name = $2
@reversed = $3 @reversed = !!$3
@name = "#{@variable_name}-#{collection_name}" @name = "#{@variable_name}-#{collection_name}"
@collection_name = Expression.parse(collection_name) @collection_name = Expression.parse(collection_name)
markup.scan(TagAttributes) do |key, value| markup.scan(TagAttributes) do |key, value|
@@ -182,6 +119,63 @@ module Liquid
private private
def collection_segment(context)
offsets = context.registers[:for] ||= Hash.new(0)
from = if @from == :continue
offsets[@name].to_i
else
context.evaluate(@from).to_i
end
collection = context.evaluate(@collection_name)
collection = collection.step(1).to_a if collection.is_a?(Range)
limit = context.evaluate(@limit)
to = limit ? limit.to_i + from : nil
segment = Utils.slice_collection(collection, from, to)
segment.reverse! if @reversed
offsets[@name] = from + segment.length
segment
end
def render_segment(context, segment)
for_stack = context.registers[:for_stack] ||= []
length = segment.length
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_with_index do |item, index|
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) def set_attribute(key, expr)
case key case key
when 'offset'.freeze when 'offset'.freeze
@@ -198,10 +192,6 @@ module Liquid
def render_else(context) def render_else(context)
@else_block ? @else_block.render(context) : ''.freeze @else_block ? @else_block.render(context) : ''.freeze
end end
def iterable?(collection)
collection.respond_to?(:each) || Utils.non_blank_string?(collection)
end
end end
Template.register_tag('for'.freeze, For) Template.register_tag('for'.freeze, For)

View File

@@ -21,7 +21,7 @@ module Liquid
end end
def parse(tokens) def parse(tokens)
while more = parse_body(@blocks.last.attachment, tokens) while parse_body(@blocks.last.attachment, tokens)
end end
end end
@@ -48,25 +48,6 @@ module Liquid
end end
end end
def format
out = ""
first_condition = true
@blocks.each do |block|
if block.else?
out << "{% else %}"
elsif first_condition
out << "{% #{block_name} #{block.format} %}"
first_condition = false
else
out << "{% elsif #{block.format} %}"
end
out << block.attachment.format
end
out + "{% #{block_delimiter} %}"
end
private private
def push_block(tag, markup) def push_block(tag, markup)
@@ -86,7 +67,7 @@ module Liquid
condition = Condition.new(Expression.parse($1), $2, Expression.parse($3)) condition = Condition.new(Expression.parse($1), $2, Expression.parse($3))
while not expressions.empty? until expressions.empty?
operator = expressions.pop.to_s.strip operator = expressions.pop.to_s.strip
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 expressions.pop.to_s =~ Syntax
@@ -102,19 +83,22 @@ module Liquid
def strict_parse(markup) def strict_parse(markup)
p = Parser.new(markup) p = Parser.new(markup)
condition = parse_binary_comparisons(p)
condition = parse_comparison(p)
while op = (p.id?('and'.freeze) || p.id?('or'.freeze))
new_cond = parse_comparison(p)
new_cond.send(op, condition)
condition = new_cond
end
p.consume(:end_of_string) p.consume(:end_of_string)
condition condition
end end
def parse_binary_comparisons(p)
condition = parse_comparison(p)
first_condition = condition
while op = (p.id?('and'.freeze) || p.id?('or'.freeze))
child_condition = parse_comparison(p)
condition.send(op, child_condition)
condition = child_condition
end
first_condition
end
def parse_comparison(p) def parse_comparison(p)
a = Expression.parse(p.expression) a = Expression.parse(p.expression)
if op = p.consume?(:comparison) if op = p.consume?(:comparison)

View File

@@ -1,9 +1,7 @@
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 = super
if output != context.registers[:ifchanged] if output != context.registers[:ifchanged]

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:
@@ -38,21 +37,27 @@ 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) template_name = context.evaluate(@template_name_expr)
raise ArgumentError.new(options[:locale].t("errors.argument.include")) unless template_name
partial = load_cached_partial(template_name, context)
context_variable_name = template_name.split('/'.freeze).last context_variable_name = template_name.split('/'.freeze).last
variable = if @variable_name_expr variable = if @variable_name_expr
context.evaluate(@variable_name_expr) context.evaluate(@variable_name_expr)
else else
context.find_variable(template_name) context.find_variable(template_name, raise_on_not_found: false)
end end
old_template_name = context.template_name
old_partial = context.partial
begin
context.template_name = template_name
context.partial = true
context.stack do context.stack do
@attributes.each do |key, value| @attributes.each do |key, value|
context[key] = context.evaluate(value) context[key] = context.evaluate(value)
@@ -68,33 +73,30 @@ module Liquid
partial.render(context) partial.render(context)
end end
end end
ensure
context.template_name = old_template_name
context.partial = old_partial
end end
def format
segments = [@tag_name, Expression.format(@template_name_expr)]
if @variable_name_expr
segments << "with"
segments << Expression.format(@variable_name_expr)
end
unless @attributes.empty?
segments << @attributes.map { |k, v| "#{k}: #{Expression.format(v)}" }.join(", ")
end
"{% #{segments.join(' ')} %}"
end end
private private
def load_cached_partial(context)
alias_method :parse_context, :options
private :parse_context
def load_cached_partial(template_name, context)
cached_partials = context.registers[:cached_partials] || {} cached_partials = context.registers[:cached_partials] || {}
template_name = context.evaluate(@template_name_expr)
if cached = cached_partials[template_name] if cached = cached_partials[template_name]
return cached return cached
end end
source = read_template_from_file_system(context) source = read_template_from_file_system(context)
partial = Liquid::Template.parse(source, pass_options) begin
parse_context.partial = true
partial = Liquid::Template.parse(source, parse_context)
ensure
parse_context.partial = false
end
cached_partials[template_name] = partial cached_partials[template_name] = partial
context.registers[:cached_partials] = cached_partials context.registers[:cached_partials] = cached_partials
partial partial
@@ -105,16 +107,6 @@ module Liquid
file_system.read_template_file(context.evaluate(@template_name_expr)) file_system.read_template_file(context.evaluate(@template_name_expr))
end 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
Template.register_tag('include'.freeze, Include) Template.register_tag('include'.freeze, Include)

View File

@@ -1,7 +1,14 @@
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)
@body = '' @body = ''
while token = tokens.shift while token = tokens.shift
@@ -9,11 +16,13 @@ module Liquid
@body << $1 if $1 != "".freeze @body << $1 if $1 != "".freeze
return if block_delimiter == $2 return if block_delimiter == $2
end end
@body << token if not token.empty? @body << token unless token.empty?
end
end end
def render(context) raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name))
end
def render(_context)
@body @body
end end
@@ -25,8 +34,12 @@ module Liquid
@body.empty? @body.empty?
end end
def format protected
"{% #{block_name} %}#{@body}{% #{block_delimiter} %}"
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

View File

@@ -28,50 +28,26 @@ module Liquid
cols = context.evaluate(@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_with_index do |item, index|
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,
'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}\">" << super << '</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
def format
tag_main = "#{block_name} #{@variable_name} in #{Expression.format(@collection_name)}"
args = @attributes.map { |k, v| "#{k}: #{Expression.format(v)}" }.join(", ")
"{% #{tag_main} #{args} %}#{@body.format}{% #{block_delimiter} %}"
end
end end
Template.register_tag('tablerow'.freeze, TableRow) Template.register_tag('tablerow'.freeze, TableRow)

View File

@@ -1,4 +1,4 @@
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.
@@ -8,7 +8,6 @@ module Liquid
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)

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,23 +13,21 @@ module Liquid
# template.render('user_name' => 'bob') # template.render('user_name' => 'bob')
# #
class Template class Template
DEFAULT_OPTIONS = {
:locale => I18n.new
}
attr_accessor :root attr_accessor :root
attr_reader :resource_limits attr_reader :resource_limits, :warnings
@@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 }
@@ -46,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)
@@ -68,6 +69,11 @@ module Liquid
# :error raises an error when tainted output is used # :error raises an error when tainted output is used
attr_writer :taint_mode 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
@@ -85,11 +91,11 @@ module Liquid
end end
def error_mode def error_mode
@error_mode || :lax @error_mode ||= :lax
end end
def taint_mode def taint_mode
@taint_mode || :lax @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
@@ -112,6 +118,7 @@ module Liquid
end end
def initialize def initialize
@rethrow_errors = false
@resource_limits = ResourceLimits.new(self.class.default_resource_limits) @resource_limits = ResourceLimits.new(self.class.default_resource_limits)
end end
@@ -121,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
@@ -169,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
@@ -188,20 +191,10 @@ 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
@@ -211,7 +204,7 @@ module Liquid
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
@@ -229,32 +222,12 @@ 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
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]
raise "Profiler not loaded, require 'liquid/profiler' first" unless defined?(Liquid::Profiler) raise "Profiler not loaded, require 'liquid/profiler' first" unless defined?(Liquid::Profiler)
@profiler = Profiler.new @profiler = Profiler.new
@@ -269,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,24 @@
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) 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 +32,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.new(obj.to_s)
when Numeric
obj
when String
(obj.strip =~ /\A-?\d+\.\d+\z/) ? BigDecimal.new(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
# #
@@ -12,14 +11,16 @@ module Liquid
# #
class Variable class Variable
FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o
attr_accessor :filters, :name, :warnings attr_accessor :filters, :name, :line_number
attr_accessor :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
@@ -28,50 +29,27 @@ module Liquid
@markup @markup
end end
def format
out = Expression.format(@name)
@filters.each do |filter|
out << " | "
out << filter[0]
args = filter[1].map { |arg| Expression.format(arg) }
if filter.size > 2
args += filter[2].map { |key, arg| "#{key}: #{Expression.format(arg)}" }
end
unless args.empty?
out << ": "
out << args.join(', ')
end
end
out
end
def markup_context(markup) def markup_context(markup)
"in \"{{#{markup}}}\"" "in \"{{#{markup}}}\""
end end
def lax_parse(markup) def lax_parse(markup)
@filters = [] @filters = []
if markup =~ /(#{QuotedFragment})(.*)/om return unless markup =~ /(#{QuotedFragment})(.*)/om
name_markup = $1 name_markup = $1
filter_markup = $2 filter_markup = $2
@name = Expression.parse(name_markup) @name = Expression.parse(name_markup)
if filter_markup =~ /#{FilterSeparator}\s*(.*)/om if filter_markup =~ /#{FilterSeparator}\s*(.*)/om
filters = $1.scan(FilterParser) filters = $1.scan(FilterParser)
filters.each do |f| filters.each do |f|
if f =~ /\w+/ next unless f =~ /\w+/
filtername = Regexp.last_match(0) filtername = Regexp.last_match(0)
filterargs = f.scan(/(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o).flatten filterargs = f.scan(/(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o).flatten
@filters << parse_filter_expressions(filtername, filterargs) @filters << parse_filter_expressions(filtername, filterargs)
end end
end end
end end
end
end
def strict_parse(markup) def strict_parse(markup)
@filters = [] @filters = []
@@ -90,17 +68,21 @@ 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)
@filters.inject(context.evaluate(@name)) do |output, (filter_name, filter_args, filter_kwargs)| obj = @filters.inject(context.evaluate(@name)) do |output, (filter_name, filter_args, filter_kwargs)|
filter_args = evaluate_filter_expressions(context, filter_args, filter_kwargs) filter_args = evaluate_filter_expressions(context, filter_args, filter_kwargs)
output = context.invoke(filter_name, output, *filter_args) context.invoke(filter_name, output, *filter_args)
end.tap{ |obj| taint_check(obj) } end
obj = context.apply_global_filter(obj)
taint_check(context, obj)
obj
end end
private private
@@ -132,17 +114,22 @@ module Liquid
parsed_args parsed_args
end end
def taint_check(obj) def taint_check(context, obj)
if obj.tainted? return unless obj.tainted?
return if Template.taint_mode == :lax
@markup =~ QuotedFragment @markup =~ QuotedFragment
name = Regexp.last_match(0) 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 case Template.taint_mode
when :warn when :warn
@warnings ||= [] context.warnings << error
@warnings << "variable '#{name}' is tainted and was not escaped"
when :error when :error
raise TaintedError, "Error - variable '#{name}' is tainted and was not escaped" raise error
end
end end
end end
end end

View File

@@ -14,7 +14,6 @@ module Liquid
name = lookups.shift name = lookups.shift
if name =~ SQUARE_BRACKETED if name =~ SQUARE_BRACKETED
@name_expression = true
name = Expression.parse($1) name = Expression.parse($1)
end end
@name = name @name = name
@@ -42,7 +41,7 @@ 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
@@ -56,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
@@ -69,21 +70,7 @@ module Liquid
end end
def ==(other) def ==(other)
self.class == other.class && self.state == other.state self.class == other.class && state == other.state
end
def format
out = @name_expression ? Expression.format(@name) : @name.dup
@lookups.each do |lookup|
if lookup.is_a?(String) && lookup =~ /^#{VariableSegment}+$/
out << ".#{lookup}"
else
out << "[#{Expression.format(lookup)}]"
end
end
out
end end
protected protected

View File

@@ -1,4 +1,4 @@
# encoding: utf-8 # encoding: utf-8
module Liquid module Liquid
VERSION = "4.0.0.alpha" VERSION = "4.0.0"
end end

View File

@@ -1,6 +1,6 @@
# 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"
@@ -15,15 +15,16 @@ Gem::Specification.new do |s|
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

@@ -1,12 +1,12 @@
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
200.times do 200.times do
profiler.run profiler.run

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,5 +1,4 @@
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)

View File

@@ -51,14 +51,13 @@ class Paginate < Liquid::Block
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

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
@@ -21,11 +20,11 @@ module ShopFilter
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,5 +1,4 @@
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
@@ -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,12 +6,11 @@
# 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
@@ -22,57 +21,100 @@ class ThemeRunner
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
# `run` is called to benchmark rendering and compiling at the same time
def run 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

@@ -16,11 +16,11 @@ class AssignTest < Minitest::Test
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

View File

@@ -61,8 +61,8 @@ 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

View File

@@ -18,14 +18,14 @@ 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
with_error_mode :strict do with_error_mode :strict do
context = Context.new context = Context.new
context.has_key?('unknown') context.key?('unknown')
assert_empty context.errors assert_empty context.errors
end end
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
@@ -53,13 +52,14 @@ class ProductDrop < Liquid::Drop
end end
protected protected
def callmenot def callmenot
"protected" "protected"
end end
end end
class EnumerableDrop < Liquid::Drop class EnumerableDrop < Liquid::Drop
def before_method(method) def liquid_method_missing(method)
method method
end end
@@ -93,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
@@ -124,8 +124,10 @@ class DropsTest < Minitest::Test
def test_rendering_warns_on_tainted_attr def test_rendering_warns_on_tainted_attr
with_taint_mode(:warn) do with_taint_mode(:warn) do
tpl = Liquid::Template.parse('{{ product.user_input }}') tpl = Liquid::Template.parse('{{ product.user_input }}')
tpl.render!('product' => ProductDrop.new) context = Context.new('product' => ProductDrop.new)
assert_match /tainted/, tpl.warnings.first 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
end end
@@ -155,14 +157,14 @@ class DropsTest < Minitest::Test
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
@@ -199,9 +201,9 @@ class DropsTest < Minitest::Test
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
@@ -229,7 +231,7 @@ class DropsTest < Minitest::Test
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
@@ -94,7 +75,7 @@ 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
@@ -102,30 +83,46 @@ class ErrorHandlingTest < Minitest::Test
def test_with_line_numbers_adds_numbers_to_parser_errors def test_with_line_numbers_adds_numbers_to_parser_errors
err = assert_raises(SyntaxError) do err = assert_raises(SyntaxError) do
template = Liquid::Template.parse(%q{ Liquid::Template.parse(%q(
foobar foobar
{% "cat" | foobar %} {% "cat" | foobar %}
bla bla
}, ),
:line_numbers => true line_numbers: true
) )
end end
assert_match /Liquid syntax error \(line 4\)/, err.message 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 end
def test_parsing_warn_with_line_numbers_adds_numbers_to_lexer_errors def test_parsing_warn_with_line_numbers_adds_numbers_to_lexer_errors
template = Liquid::Template.parse(%q{ template = Liquid::Template.parse('
foobar foobar
{% if 1 =! 2 %}ok{% endif %} {% if 1 =! 2 %}ok{% endif %}
bla bla
}, ',
:error_mode => :warn, error_mode: :warn,
:line_numbers => true line_numbers: true
) )
assert_equal ['Liquid syntax error (line 4): Unexpected character = in "1 =! 2"'], assert_equal ['Liquid syntax error (line 4): Unexpected character = in "1 =! 2"'],
@@ -134,15 +131,15 @@ class ErrorHandlingTest < Minitest::Test
def test_parsing_strict_with_line_numbers_adds_numbers_to_lexer_errors def test_parsing_strict_with_line_numbers_adds_numbers_to_lexer_errors
err = assert_raises(SyntaxError) do err = assert_raises(SyntaxError) do
Liquid::Template.parse(%q{ Liquid::Template.parse('
foobar foobar
{% if 1 =! 2 %}ok{% endif %} {% if 1 =! 2 %}ok{% endif %}
bla bla
}, ',
:error_mode => :strict, error_mode: :strict,
:line_numbers => true line_numbers: true
) )
end end
@@ -151,7 +148,7 @@ class ErrorHandlingTest < Minitest::Test
def test_syntax_errors_in_nested_blocks_have_correct_line_number def test_syntax_errors_in_nested_blocks_have_correct_line_number
err = assert_raises(SyntaxError) do err = assert_raises(SyntaxError) do
Liquid::Template.parse(%q{ Liquid::Template.parse('
foobar foobar
{% if 1 != 2 %} {% if 1 != 2 %}
@@ -159,8 +156,8 @@ class ErrorHandlingTest < Minitest::Test
{% endif %} {% endif %}
bla bla
}, ',
:line_numbers => true line_numbers: true
) )
end end
@@ -169,18 +166,18 @@ class ErrorHandlingTest < Minitest::Test
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)
@@ -189,7 +186,7 @@ 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
@@ -204,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

@@ -39,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
@@ -53,20 +53,20 @@ 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
@@ -76,11 +76,11 @@ class FiltersTest < Minitest::Test
@context['arrays'] = ['flower', 'are'] @context['arrays'] = ['flower', 'are']
@context['case_sensitive'] = ['sensitive', 'Expected', 'case'] @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'], Variable.new("case_sensitive | sort").render(@context) assert_equal 'Expected case sensitive', Template.parse("{{case_sensitive | sort | join}}").render(@context)
end end
def test_sort_natural def test_sort_natural
@@ -89,54 +89,64 @@ class FiltersTest < Minitest::Test
@context['objects'] = [TestObject.new('A'), TestObject.new('b'), TestObject.new('C')] @context['objects'] = [TestObject.new('A'), TestObject.new('b'), TestObject.new('C')]
# Test strings # Test strings
assert_equal ['Assert', 'case', 'Insensitive'], Variable.new("words | sort_natural").render(@context) assert_equal 'Assert case Insensitive', Template.parse("{{words | sort_natural | join}}").render(@context)
# Test hashes # Test hashes
sorted = Variable.new("hashes | sort_natural: 'a'").render(@context) assert_equal 'A b C', Template.parse("{{hashes | sort_natural: 'a' | map: 'a' | join}}").render(@context)
assert_equal sorted[0]['a'], 'A'
assert_equal sorted[1]['a'], 'b'
assert_equal sorted[2]['a'], 'C'
# Test objects # Test objects
sorted = Variable.new("objects | sort_natural: 'a'").render(@context) assert_equal 'A b C', Template.parse("{{objects | sort_natural: 'a' | map: 'a' | join}}").render(@context)
assert_equal sorted[0].a, 'A' end
assert_equal sorted[1].a, 'b'
assert_equal sorted[2].a, 'C' 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 def test_override_object_method_in_filter
assert_equal "tap overridden", Template.parse("{{var | tap}}").render!({ 'var' => 1000 }, :filters => [OverrideObjectMethodFilter]) assert_equal "tap overridden", Template.parse("{{var | tap}}").render!({ 'var' => 1000 }, filters: [OverrideObjectMethodFilter])
# tap still treated as a non-existent filter # tap still treated as a non-existent filter
assert_equal "1000", Template.parse("{{var | tap}}").render!({ 'var' => 1000 }) assert_equal "1000", Template.parse("{{var | tap}}").render!({ 'var' => 1000 })
@@ -149,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
@@ -160,7 +170,7 @@ class FiltersInTemplate < Minitest::Test
end end
end # FiltersTest end # FiltersTest
class TestObject class TestObject < Liquid::Drop
attr_accessor :a attr_accessor :a
def initialize(a) def initialize(a)
@a = a @a = a

View File

@@ -1,172 +0,0 @@
require 'test_helper'
class FormatterTest < Minitest::Test
def assert_format(expected, src)
with_error_mode(:lax) do
src_ast = Liquid::Template.parse(src).root
assert_equal expected, src_ast.format
fmt_ast = Liquid::Template.parse(src_ast.format).root
assert_equal expected, fmt_ast.format
end
end
def test_filters
assert_format '{{ a | b: foo, c: "foo" }}', '{{a|b:foo,c:"foo"}}'
assert_format '{{ page.attribs.title | downcase }}', "{{page.attribs['title' ]| downcase}}"
assert_format '{{ page.attribs["t.i.t.l.e"] | downcase }}', "{{page.attribs['t.i.t.l.e'] | downcase }}"
assert_format '{{ page.attribs["t&tle"] | downcase }}', "{{page.attribs['t&tle'] | downcase }}"
end
def test_conditionals
src = <<-eof
{% if true && !!%}
cats
{% elsif a or (b and c) && d%}
dogs
{% endif %}
{%unless something%}
cats
{% endunless%}
eof
expected = <<-eof
{% if true %}
cats
{% elsif a or b and c %}
dogs
{% endif %}
{% unless something %}
cats
{% endunless %}
eof
assert_format expected, src
src = <<-eof
{%case var asdf $$^$ %}
{% when true%}
w
{% else%}
e
{%endcase %}
eof
expected = <<-eof
{% case var %}
{% when true %}
w
{% else %}
e
{% endcase %}
eof
assert_format expected, src
end
def test_comments
assert_format "{% comment %} hunter2 {% endcomment %}", "{%comment %} hunter2 {% endcomment ^ %}"
end
def test_assigns
assert_format '{% assign foo = "monkey" %}', "{%assign foo ='monkey' ^ %}"
end
def test_looping
src = <<-eof
{% for i in (1..10) %}
cat
{%ifchanged%}{{i}}{% endifchanged %}
{% continue%}
{% else %}
dog
{%break %}
{% endfor %}
eof
expected = <<-eof
{% for i in (1..10) %}
cat
{% ifchanged %}{{ i }}{% endifchanged %}
{% continue %}
{% else %}
dog
{% break %}
{% endfor %}
eof
assert_format expected, src
src = "{% tablerow n in numbers cols:3 offset : 1 limit:6%} {{n}} {% endtablerow %}"
expected = "{% tablerow n in numbers cols: 3, offset: 1, limit: 6 %} {{ n }} {% endtablerow %}"
assert_format expected, src
end
def test_capture
assert_format "{% capture foo %} foo {% endcapture %}", "{%capture foo %} foo {%endcapture%}"
end
def test_cycle
assert_format '{% cycle "red", 2.8, "green", 1 %}', "{% cycle 'red',2.8,'green',1 %}"
end
def test_augment
assert_format "{% decrement foo %}{% increment foo %}", "{% decrement foo%}{%increment foo %}"
end
def test_raw
assert_format "{% raw %} foo {% endraw %}", "{%raw !!%} foo {%endraw foo%}"
end
def test_include
src = <<-eof
{% include 'foo' %}
{% include 'foo' !!! why! %}
{% include 'foo' with bar %}
{% include 'foo' with bar baz: z qux:f %}
{% include 'foo' baz: z qux:f %}
eof
expected = <<-eof
{% include "foo" %}
{% include "foo" %}
{% include "foo" with bar %}
{% include "foo" with bar baz: z, qux: f %}
{% include "foo" baz: z, qux: f %}
eof
assert_format expected, src
end
def test_quirks
src = <<-eof
{% if a == 'foo' or (b == 'bar' and c == 'baz') or false %} YES {% endif %}
{% if true && false %} YES {% endif %}
{% if false || true %} YES {% endif %}
{{ 'hi there' | split$$$:' ' | first }}""
{{ 'X' | downcase) }}
{{ 'hi there' | split:"t"" | reverse | first}}
{{ 'hi there' | split:"t"" | remove:"i" | first}}
{% for i in (1...5) %}{{ i }}{% endfor %}
{{test |a|b|}}
{{|test|}}
eof
expected = <<-eof
{% if a == "foo" or b == "bar" and c == "baz" or false %} YES {% endif %}
{% if true %} YES {% endif %}
{% if false %} YES {% endif %}
{{ "hi there" | split: " " | first }}""
{{ "X" | downcase }}
{{ "hi there" | split: "t" | reverse | first }}
{{ "hi there" | split: "t" | first }}
{% for i in (1..5) %}{{ i }}{% endfor %}
{{ test | a | b }}
{{ test }}
eof
assert_format expected, src
end
end

View File

@@ -1,5 +1,6 @@
require 'test_helper' require 'test_helper'
class HashOrderingTest < Minitest::Test
module MoneyFilter module MoneyFilter
def money(input) def money(input)
sprintf(' %d$ ', input) sprintf(' %d$ ', input)
@@ -12,7 +13,6 @@ module CanadianMoneyFilter
end end
end end
class HashOrderingTest < Minitest::Test
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
@@ -38,79 +37,87 @@ class OutputTest < Minitest::Test
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

@@ -62,9 +62,9 @@ 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
@@ -116,4 +116,7 @@ class ParsingQuirksTest < Minitest::Test
end end
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

@@ -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,7 +84,7 @@ 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]
@@ -94,7 +94,7 @@ class RenderProfilingTest < Minitest::Test
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]
@@ -109,7 +109,7 @@ class RenderProfilingTest < Minitest::Test
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]
@@ -124,7 +124,7 @@ class RenderProfilingTest < Minitest::Test
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,7 +144,7 @@ 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

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
@@ -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,18 @@ 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)
end end
def test_truncatewords def test_truncatewords
@@ -129,6 +166,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
@@ -144,6 +182,7 @@ class StandardFiltersTest < Minitest::Test
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
@@ -151,6 +190,32 @@ class StandardFiltersTest < Minitest::Test
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 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_empty_array
assert_equal [], @filters.sort([], "a")
end
def test_sort_natural_empty_array
assert_equal [], @filters.sort_natural([], "a")
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
@@ -163,12 +228,21 @@ class StandardFiltersTest < Minitest::Test
end end
def test_uniq def test_uniq
assert_equal ["foo"], @filters.uniq("foo")
assert_equal [1, 3, 2, 4], @filters.uniq([1, 1, 3, 2, 3, 1, 4, 3, 2, 1]) 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") 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_compact_empty_array
assert_equal [], @filters.compact([], "a")
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
@@ -212,11 +286,24 @@ 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
@@ -230,6 +317,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")
@@ -251,30 +342,36 @@ class StandardFiltersTest < Minitest::Test
assert_equal "#{Date.today.year}", @filters.date('today', '%Y') assert_equal "#{Date.today.year}", @filters.date('today', '%Y')
assert_equal "#{Date.today.year}", @filters.date('Today', '%Y') assert_equal "#{Date.today.year}", @filters.date('Today', '%Y')
assert_equal nil, @filters.date(nil, "%B") assert_nil @filters.date(nil, "%B")
assert_equal '', @filters.date('', "%B") assert_equal '', @filters.date('', "%B")
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")
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
@@ -309,20 +406,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
@@ -333,26 +448,52 @@ 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 end
def test_append def test_append
@@ -366,8 +507,7 @@ class StandardFiltersTest < Minitest::Test
assert_equal [1, 2, 'a'], @filters.concat([1, 2], ['a']) assert_equal [1, 2, 'a'], @filters.concat([1, 2], ['a'])
assert_equal [1, 2, 10], @filters.concat([1, 2], [10]) assert_equal [1, 2, 10], @filters.concat([1, 2], [10])
assert_raises(TypeError) do assert_raises(Liquid::ArgumentError, "concat filter requires an array argument") do
# no implicit conversion of Fixnum into Array
@filters.concat([1, 2], 10) @filters.concat([1, 2], 10)
end end
end end
@@ -390,4 +530,19 @@ class StandardFiltersTest < Minitest::Test
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 end
def test_date_raises_nothing
assert_template_result('', "{{ '' | date: '%D' }}")
assert_template_result('abc', "{{ 'abc' | date: '%D' }}")
end
private
def with_timezone(tz)
old_tz = ENV['TZ']
ENV['TZ'] = tz
yield
ensure
ENV['TZ'] = old_tz
end
end # StandardFiltersTest end # StandardFiltersTest

View File

@@ -12,5 +12,4 @@ class BreakTagTest < Minitest::Test
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

@@ -38,10 +38,20 @@ HERE
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)
assert_template_result(' 1.0 2.0 3.0 ', '{%for item in foobar %} {{item}} {%endfor%}', "foobar" => (1..3.0))
assert_template_result(' 1.0 2.0 3.0 ', '{%for item in foobar %} {{item}} {%endfor%}', "foobar" => (1.0..3))
assert_template_result(' 1.0 2.0 3.0 ', '{%for item in foobar %} {{item}} {%endfor%}', "foobar" => (1.0..3.0))
assert_template_result(' 1.5 2.5 ', '{%for item in foobar %} {{item}} {%endfor%}', "foobar" => (1.5..3))
end end
def test_for_with_hash_value_range def test_for_with_hash_value_range
@@ -172,19 +182,18 @@ HERE
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 = %q({%for i in array.items limit:3 %}{{i}}{%endfor%} markup = '{%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
@@ -210,13 +219,13 @@ HERE
# 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)
@@ -253,13 +262,13 @@ HERE
# 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)
@@ -285,35 +294,39 @@ HERE
'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 end
def test_for_parentloop_references_parent_loop def test_for_parentloop_references_parent_loop
assert_template_result('1.1 1.2 1.3 2.1 2.2 2.3 ', assert_template_result('1.1 1.2 1.3 2.1 2.2 2.3 ',
'{% for inner in outer %}{% for k in inner %}' + '{% for inner in outer %}{% for k in inner %}' \
'{{ forloop.parentloop.index }}.{{ forloop.index }} ' + '{{ forloop.parentloop.index }}.{{ forloop.index }} ' \
'{% endfor %}{% endfor %}', '{% endfor %}{% endfor %}',
'outer' => [[1, 1, 1], [1, 1, 1]]) 'outer' => [[1, 1, 1], [1, 1, 1]])
end end
def test_for_parentloop_nil_when_not_present def test_for_parentloop_nil_when_not_present
assert_template_result('.1 .2 ', assert_template_result('.1 .2 ',
'{% for inner in outer %}' + '{% for inner in outer %}' \
'{{ forloop.parentloop.index }}.{{ forloop.index }} ' + '{{ forloop.parentloop.index }}.{{ forloop.index }} ' \
'{% endfor %}', '{% endfor %}',
'outer' => [[1, 1, 1], [1, 1, 1]]) 'outer' => [[1, 1, 1], [1, 1, 1]])
end 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
def test_blank_string_not_iterable def test_blank_string_not_iterable
assert_template_result('', "{% for char in characters %}I WILL NOT BE OUTPUT{% endfor %}", 'characters' => '') assert_template_result('', "{% for char in characters %}I WILL NOT BE OUTPUT{% endfor %}", 'characters' => '')
end end
@@ -388,4 +401,14 @@ HERE
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

@@ -63,7 +63,6 @@ class IfElseTagTest < Minitest::Test
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
@@ -110,7 +109,6 @@ 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
@@ -166,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

View File

@@ -77,10 +77,9 @@ class IncludeTagTest < Minitest::Test
def test_include_tag_looks_for_file_system_in_registers_first def test_include_tag_looks_for_file_system_in_registers_first
assert_equal 'from OtherFileSystem', assert_equal 'from OtherFileSystem',
Template.parse("{% include 'pick_a_source' %}").render!({}, :registers => {:file_system => OtherFileSystem.new}) Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: OtherFileSystem.new })
end end
def test_include_tag_with def test_include_tag_with
assert_template_result "Product: Draft 151cm ", assert_template_result "Product: Draft 151cm ",
"{% include 'product' with products[0] %}", "products" => [ { 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' } ] "{% include 'product' with products[0] %}", "products" => [ { 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' } ]
@@ -130,7 +129,6 @@ class IncludeTagTest < Minitest::Test
end end
def test_recursively_included_template_does_not_produce_endless_loop def test_recursively_included_template_does_not_produce_endless_loop
infinite_file_system = Class.new do infinite_file_system = Class.new do
def read_template_file(template_path) def read_template_file(template_path)
"-{% include 'loop' %}" "-{% include 'loop' %}"
@@ -139,10 +137,9 @@ class IncludeTagTest < Minitest::Test
Liquid::Template.file_system = infinite_file_system.new Liquid::Template.file_system = infinite_file_system.new
assert_raises(Liquid::StackLevelError, SystemStackError) do assert_raises(Liquid::StackLevelError) do
Template.parse("{% include 'loop' %}").render! Template.parse("{% include 'loop' %}").render!
end end
end end
def test_dynamically_choosen_template def test_dynamically_choosen_template
@@ -156,18 +153,18 @@ class IncludeTagTest < Minitest::Test
def test_include_tag_caches_second_read_of_same_partial def test_include_tag_caches_second_read_of_same_partial
file_system = CountingFileSystem.new file_system = CountingFileSystem.new
assert_equal 'from CountingFileSystemfrom CountingFileSystem', assert_equal 'from CountingFileSystemfrom CountingFileSystem',
Template.parse("{% include 'pick_a_source' %}{% include 'pick_a_source' %}").render!({}, :registers => {:file_system => file_system}) Template.parse("{% include 'pick_a_source' %}{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system })
assert_equal 1, file_system.count assert_equal 1, file_system.count
end end
def test_include_tag_doesnt_cache_partials_across_renders def test_include_tag_doesnt_cache_partials_across_renders
file_system = CountingFileSystem.new file_system = CountingFileSystem.new
assert_equal 'from CountingFileSystem', assert_equal 'from CountingFileSystem',
Template.parse("{% include 'pick_a_source' %}").render!({}, :registers => {:file_system => file_system}) Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system })
assert_equal 1, file_system.count assert_equal 1, file_system.count
assert_equal 'from CountingFileSystem', assert_equal 'from CountingFileSystem',
Template.parse("{% include 'pick_a_source' %}").render!({}, :registers => {:file_system => file_system}) Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system })
assert_equal 2, file_system.count assert_equal 2, file_system.count
end end
@@ -220,6 +217,17 @@ class IncludeTagTest < Minitest::Test
end end
end end
def test_render_raise_argument_error_when_template_is_undefined
assert_raises(Liquid::ArgumentError) do
template = Liquid::Template.parse('{% include undefined_variable %}')
template.render!
end
assert_raises(Liquid::ArgumentError) do
template = Liquid::Template.parse('{% include nil %}')
template.render!
end
end
def test_including_via_variable_value def test_including_via_variable_value
assert_template_result "from TestFileSystem", "{% assign page = 'pick_a_source' %}{% include page %}" assert_template_result "from TestFileSystem", "{% assign page = 'pick_a_source' %}{% include page %}"
@@ -227,4 +235,11 @@ class IncludeTagTest < Minitest::Test
assert_template_result "Product: Draft 151cm ", "{% assign page = 'product' %}{% include page for foo %}", "foo" => { 'title' => 'Draft 151cm' } assert_template_result "Product: Draft 151cm ", "{% assign page = 'product' %}{% include page for foo %}", "foo" => { 'title' => 'Draft 151cm' }
end end
def test_including_with_strict_variables
template = Liquid::Template.parse("{% include 'simple' %}", error_mode: :warn)
template.render(nil, strict_variables: true)
assert_equal [], template.errors
end
end # IncludeTagTest end # IncludeTagTest

View File

@@ -7,8 +7,8 @@ class IncrementTagTest < Minitest::Test
assert_template_result('0', '{%increment port %}', {}) assert_template_result('0', '{%increment port %}', {})
assert_template_result('0 1', '{%increment port %} {%increment port%}', {}) assert_template_result('0 1', '{%increment port %} {%increment port%}', {})
assert_template_result('0 0 1 2 1', assert_template_result('0 0 1 2 1',
'{%increment port %} {%increment starboard%} ' + '{%increment port %} {%increment starboard%} ' \
'{%increment port %} {%increment port%} ' + '{%increment port %} {%increment port%} ' \
'{%increment starboard %}', {}) '{%increment starboard %}', {})
end end
@@ -16,9 +16,8 @@ class IncrementTagTest < Minitest::Test
assert_template_result('9', '{%decrement port %}', { 'port' => 10 }) assert_template_result('9', '{%decrement port %}', { 'port' => 10 })
assert_template_result('-1 -2', '{%decrement port %} {%decrement port%}', {}) assert_template_result('-1 -2', '{%decrement port %} {%decrement port%}', {})
assert_template_result('1 5 2 2 5', assert_template_result('1 5 2 2 5',
'{%increment port %} {%increment starboard%} ' + '{%increment port %} {%increment starboard%} ' \
'{%increment port %} {%decrement port%} ' + '{%increment port %} {%decrement port%} ' \
'{%decrement starboard %}', { 'port' => 1, 'starboard' => 5 }) '{%decrement starboard %}', { 'port' => 1, 'starboard' => 5 })
end end
end end

View File

@@ -22,4 +22,10 @@ class RawTagTest < Minitest::Test
assert_template_result ' test {% raw %} {% endraw %}', '{% raw %} test {% raw %} {% {% endraw %}endraw %}' assert_template_result ' test {% raw %} {% endraw %}', '{% raw %} test {% raw %} {% {% endraw %}endraw %}'
assert_template_result ' Foobar {{ invalid 1', '{% raw %} Foobar {{ invalid {% endraw %}{{ 1 }}' assert_template_result ' Foobar {{ invalid 1', '{% raw %} Foobar {{ invalid {% endraw %}{{ 1 }}'
end end
def test_invalid_raw
assert_match_syntax_error(/tag was never closed/, '{% raw %} foo')
assert_match_syntax_error(/Valid syntax/, '{% raw } foo {% endraw %}')
assert_match_syntax_error(/Valid syntax/, '{% raw } foo %}{% endraw %}')
end
end end

View File

@@ -12,14 +12,14 @@ class StandardTagTest < Minitest::Test
assert_template_result('|,.:', '|,.:') assert_template_result('|,.:', '|,.:')
assert_template_result('', '') assert_template_result('', '')
text = %|this shouldnt see any transformation either but has multiple lines text = %(this shouldnt see any transformation either but has multiple lines
as you can clearly see here ...| as you can clearly see here ...)
assert_template_result(text, text) assert_template_result(text, text)
end end
def test_has_a_block_which_does_nothing def test_has_a_block_which_does_nothing
assert_template_result(%|the comment block should be removed .. right?|, assert_template_result(%(the comment block should be removed .. right?),
%|the comment block should be removed {%comment%} be gone.. {%endcomment%} .. right?|) %(the comment block should be removed {%comment%} be gone.. {%endcomment%} .. right?))
assert_template_result('', '{%comment%}{%endcomment%}') assert_template_result('', '{%comment%}{%endcomment%}')
assert_template_result('', '{%comment%}{% endcomment %}') assert_template_result('', '{%comment%}{% endcomment %}')
@@ -172,7 +172,7 @@ class StandardTagTest < Minitest::Test
def test_assign_from_case def test_assign_from_case
# Example from the shopify forums # Example from the shopify forums
code = %q({% case collection.handle %}{% when 'menswear-jackets' %}{% assign ptitle = 'menswear' %}{% when 'menswear-t-shirts' %}{% assign ptitle = 'menswear' %}{% else %}{% assign ptitle = 'womenswear' %}{% endcase %}{{ ptitle }}) code = "{% case collection.handle %}{% when 'menswear-jackets' %}{% assign ptitle = 'menswear' %}{% when 'menswear-t-shirts' %}{% assign ptitle = 'menswear' %}{% else %}{% assign ptitle = 'womenswear' %}{% endcase %}{{ ptitle }}"
template = Liquid::Template.parse(code) template = Liquid::Template.parse(code)
assert_equal "menswear", template.render!("collection" => { 'handle' => 'menswear-jackets' }) assert_equal "menswear", template.render!("collection" => { 'handle' => 'menswear-jackets' })
assert_equal "menswear", template.render!("collection" => { 'handle' => 'menswear-t-shirts' }) assert_equal "menswear", template.render!("collection" => { 'handle' => 'menswear-t-shirts' })
@@ -236,7 +236,6 @@ class StandardTagTest < Minitest::Test
assert_raises(SyntaxError) do assert_raises(SyntaxError) do
assert_template_result('', '{% case false %}{% huh %}true{% endcase %}', {}) assert_template_result('', '{% case false %}{% huh %}true{% endcase %}', {})
end end
end end
def test_cycle def test_cycle
@@ -272,7 +271,7 @@ class StandardTagTest < Minitest::Test
end end
def test_size_of_hash def test_size_of_hash
assigns = {"hash" => {:a => 1, :b => 2, :c=> 3, :d => 4}} assigns = { "hash" => { a: 1, b: 2, c: 3, d: 4 } }
assert_template_result('hash has 4 elements', "hash has {{ hash.size }} elements", assigns) assert_template_result('hash has 4 elements', "hash has {{ hash.size }} elements", assigns)
end end

View File

@@ -37,7 +37,6 @@ class StatementsTest < Minitest::Test
text = ' {% if null <= 0 %} true {% else %} false {% endif %} ' text = ' {% if null <= 0 %} true {% else %} false {% endif %} '
assert_template_result ' false ', text assert_template_result ' false ', text
text = ' {% if 0 <= null %} true {% else %} false {% endif %} ' text = ' {% if 0 <= null %} true {% else %} false {% endif %} '
assert_template_result ' false ', text assert_template_result ' false ', text
end end
@@ -72,7 +71,6 @@ class StatementsTest < Minitest::Test
assert_template_result ' true ', text, 'var' => 'hello there!' assert_template_result ' true ', text, 'var' => 'hello there!'
end end
def test_var_and_long_string_are_equal_backwards def test_var_and_long_string_are_equal_backwards
text = " {% if 'hello there!' == var %} true {% else %} false {% endif %} " text = " {% if 'hello there!' == var %} true {% else %} false {% endif %} "
assert_template_result ' true ', text, 'var' => 'hello there!' assert_template_result ' true ', text, 'var' => 'hello there!'

View File

@@ -16,7 +16,6 @@ class TableRowTest < Minitest::Test
end end
def test_table_row def test_table_row
assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n", assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
'{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}', '{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}',
'numbers' => [1, 2, 3, 4, 5, 6]) 'numbers' => [1, 2, 3, 4, 5, 6])
@@ -30,7 +29,6 @@ class TableRowTest < Minitest::Test
assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td><td class=\"col4\"> 4 </td><td class=\"col5\"> 5 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 6 </td></tr>\n", assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td><td class=\"col4\"> 4 </td><td class=\"col5\"> 5 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 6 </td></tr>\n",
'{% tablerow n in numbers cols:5%} {{n}} {% endtablerow %}', '{% tablerow n in numbers cols:5%} {{n}} {% endtablerow %}',
'numbers' => [1, 2, 3, 4, 5, 6]) 'numbers' => [1, 2, 3, 4, 5, 6])
end end
def test_table_col_counter def test_table_col_counter
@@ -46,7 +44,6 @@ class TableRowTest < Minitest::Test
assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n", assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
"{% tablerow n in collections['frontpage'] cols:3%} {{n}} {% endtablerow %}", "{% tablerow n in collections['frontpage'] cols:3%} {{n}} {% endtablerow %}",
'collections' => { 'frontpage' => [1, 2, 3, 4, 5, 6] }) 'collections' => { 'frontpage' => [1, 2, 3, 4, 5, 6] })
end end
def test_enumerable_drop def test_enumerable_drop
@@ -57,7 +54,11 @@ class TableRowTest < Minitest::Test
def test_offset_and_limit def test_offset_and_limit
assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n", assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
'{% tablerow n in numbers cols:3, offset:1, limit:6%} {{n}} {% endtablerow %}', '{% tablerow n in numbers cols:3 offset:1 limit:6%} {{n}} {% endtablerow %}',
'numbers' => [0, 1, 2, 3, 4, 5, 6, 7]) 'numbers' => [0, 1, 2, 3, 4, 5, 6, 7])
end end
def test_blank_string_not_iterable
assert_template_result("<tr class=\"row1\">\n</tr>\n", "{% tablerow char in characters cols:3 %}I WILL NOT BE OUTPUT{% endtablerow %}", 'characters' => '')
end
end end

View File

@@ -2,7 +2,7 @@ require 'test_helper'
require 'timeout' require 'timeout'
class TemplateContextDrop < Liquid::Drop class TemplateContextDrop < Liquid::Drop
def before_method(method) def liquid_method_missing(method)
method method
end end
@@ -15,12 +15,10 @@ class TemplateContextDrop < Liquid::Drop
end end
end end
class SomethingWithLength class SomethingWithLength < Liquid::Drop
def length def length
nil nil
end end
liquid_methods :length
end end
class ErroneousDrop < Liquid::Drop class ErroneousDrop < Liquid::Drop
@@ -29,6 +27,12 @@ class ErroneousDrop < Liquid::Drop
end end
end end
class DropWithUndefinedMethod < Liquid::Drop
def foo
'foo'
end
end
class TemplateTest < Minitest::Test class TemplateTest < Minitest::Test
include Liquid include Liquid
@@ -45,7 +49,7 @@ class TemplateTest < Minitest::Test
end end
t = Template.parse(str) t = Template.parse(str)
assert_equal [], Timeout::timeout(1) { t.warnings } assert_equal [], Timeout.timeout(1) { t.warnings }
end end
def test_instance_assigns_persist_on_same_template_parsing_between_renders def test_instance_assigns_persist_on_same_template_parsing_between_renders
@@ -75,7 +79,7 @@ class TemplateTest < Minitest::Test
def test_lambda_is_called_once_from_persistent_assigns_over_multiple_parses_and_renders def test_lambda_is_called_once_from_persistent_assigns_over_multiple_parses_and_renders
t = Template.new t = Template.new
t.assigns['number'] = lambda { @global ||= 0; @global += 1 } t.assigns['number'] = -> { @global ||= 0; @global += 1 }
assert_equal '1', t.parse("{{number}}").render! assert_equal '1', t.parse("{{number}}").render!
assert_equal '1', t.parse("{{number}}").render! assert_equal '1', t.parse("{{number}}").render!
assert_equal '1', t.render! assert_equal '1', t.render!
@@ -84,7 +88,7 @@ class TemplateTest < Minitest::Test
def test_lambda_is_called_once_from_custom_assigns_over_multiple_parses_and_renders def test_lambda_is_called_once_from_custom_assigns_over_multiple_parses_and_renders
t = Template.new t = Template.new
assigns = {'number' => lambda { @global ||= 0; @global += 1 }} assigns = { 'number' => -> { @global ||= 0; @global += 1 } }
assert_equal '1', t.parse("{{number}}").render!(assigns) assert_equal '1', t.parse("{{number}}").render!(assigns)
assert_equal '1', t.parse("{{number}}").render!(assigns) assert_equal '1', t.parse("{{number}}").render!(assigns)
assert_equal '1', t.render!(assigns) assert_equal '1', t.render!(assigns)
@@ -100,51 +104,62 @@ class TemplateTest < Minitest::Test
def test_resource_limits_render_length def test_resource_limits_render_length
t = Template.parse("0123456789") t = Template.parse("0123456789")
t.resource_limits.render_length_limit = 5 t.resource_limits.render_length_limit = 5
assert_equal "Liquid error: Memory limits exceeded", t.render() assert_equal "Liquid error: Memory limits exceeded", t.render
assert t.resource_limits.reached? assert t.resource_limits.reached?
t.resource_limits.render_length_limit = 10 t.resource_limits.render_length_limit = 10
assert_equal "0123456789", t.render!() assert_equal "0123456789", t.render!
refute_nil t.resource_limits.render_length refute_nil t.resource_limits.render_length
end end
def test_resource_limits_render_score def test_resource_limits_render_score
t = Template.parse("{% for a in (1..10) %} {% for a in (1..10) %} foo {% endfor %} {% endfor %}") t = Template.parse("{% for a in (1..10) %} {% for a in (1..10) %} foo {% endfor %} {% endfor %}")
t.resource_limits.render_score_limit = 50 t.resource_limits.render_score_limit = 50
assert_equal "Liquid error: Memory limits exceeded", t.render() assert_equal "Liquid error: Memory limits exceeded", t.render
assert t.resource_limits.reached? assert t.resource_limits.reached?
t = Template.parse("{% for a in (1..100) %} foo {% endfor %}") t = Template.parse("{% for a in (1..100) %} foo {% endfor %}")
t.resource_limits.render_score_limit = 50 t.resource_limits.render_score_limit = 50
assert_equal "Liquid error: Memory limits exceeded", t.render() assert_equal "Liquid error: Memory limits exceeded", t.render
assert t.resource_limits.reached? assert t.resource_limits.reached?
t.resource_limits.render_score_limit = 200 t.resource_limits.render_score_limit = 200
assert_equal (" foo " * 100), t.render!() assert_equal (" foo " * 100), t.render!
refute_nil t.resource_limits.render_score refute_nil t.resource_limits.render_score
end end
def test_resource_limits_assign_score def test_resource_limits_assign_score
t = Template.parse("{% assign foo = 42 %}{% assign bar = 23 %}") t = Template.parse("{% assign foo = 42 %}{% assign bar = 23 %}")
t.resource_limits.assign_score_limit = 1 t.resource_limits.assign_score_limit = 1
assert_equal "Liquid error: Memory limits exceeded", t.render() assert_equal "Liquid error: Memory limits exceeded", t.render
assert t.resource_limits.reached? assert t.resource_limits.reached?
t.resource_limits.assign_score_limit = 2 t.resource_limits.assign_score_limit = 2
assert_equal "", t.render!() assert_equal "", t.render!
refute_nil t.resource_limits.assign_score refute_nil t.resource_limits.assign_score
end end
def test_resource_limits_assign_score_nested
t = Template.parse("{% assign foo = 'aaaa' | reverse %}")
t.resource_limits.assign_score_limit = 3
assert_equal "Liquid error: Memory limits exceeded", t.render
assert t.resource_limits.reached?
t.resource_limits.assign_score_limit = 5
assert_equal "", t.render!
end
def test_resource_limits_aborts_rendering_after_first_error def test_resource_limits_aborts_rendering_after_first_error
t = Template.parse("{% for a in (1..100) %} foo1 {% endfor %} bar {% for a in (1..100) %} foo2 {% endfor %}") t = Template.parse("{% for a in (1..100) %} foo1 {% endfor %} bar {% for a in (1..100) %} foo2 {% endfor %}")
t.resource_limits.render_score_limit = 50 t.resource_limits.render_score_limit = 50
assert_equal "Liquid error: Memory limits exceeded", t.render() assert_equal "Liquid error: Memory limits exceeded", t.render
assert t.resource_limits.reached? assert t.resource_limits.reached?
end end
def test_resource_limits_hash_in_template_gets_updated_even_if_no_limits_are_set def test_resource_limits_hash_in_template_gets_updated_even_if_no_limits_are_set
t = Template.parse("{% for a in (1..100) %} {% assign foo = 1 %} {% endfor %}") t = Template.parse("{% for a in (1..100) %} {% assign foo = 1 %} {% endfor %}")
t.render!() t.render!
assert t.resource_limits.assign_score > 0 assert t.resource_limits.assign_score > 0
assert t.resource_limits.render_score > 0 assert t.resource_limits.render_score > 0
assert t.resource_limits.render_length > 0 assert t.resource_limits.render_length > 0
@@ -153,23 +168,23 @@ class TemplateTest < Minitest::Test
def test_render_length_persists_between_blocks def test_render_length_persists_between_blocks
t = Template.parse("{% if true %}aaaa{% endif %}") t = Template.parse("{% if true %}aaaa{% endif %}")
t.resource_limits.render_length_limit = 7 t.resource_limits.render_length_limit = 7
assert_equal "Liquid error: Memory limits exceeded", t.render() assert_equal "Liquid error: Memory limits exceeded", t.render
t.resource_limits.render_length_limit = 8 t.resource_limits.render_length_limit = 8
assert_equal "aaaa", t.render() assert_equal "aaaa", t.render
t = Template.parse("{% if true %}aaaa{% endif %}{% if true %}bbb{% endif %}") t = Template.parse("{% if true %}aaaa{% endif %}{% if true %}bbb{% endif %}")
t.resource_limits.render_length_limit = 13 t.resource_limits.render_length_limit = 13
assert_equal "Liquid error: Memory limits exceeded", t.render() assert_equal "Liquid error: Memory limits exceeded", t.render
t.resource_limits.render_length_limit = 14 t.resource_limits.render_length_limit = 14
assert_equal "aaaabbb", t.render() assert_equal "aaaabbb", t.render
t = Template.parse("{% if true %}a{% endif %}{% if true %}b{% endif %}{% if true %}a{% endif %}{% if true %}b{% endif %}{% if true %}a{% endif %}{% if true %}b{% endif %}") t = Template.parse("{% if true %}a{% endif %}{% if true %}b{% endif %}{% if true %}a{% endif %}{% if true %}b{% endif %}{% if true %}a{% endif %}{% if true %}b{% endif %}")
t.resource_limits.render_length_limit = 5 t.resource_limits.render_length_limit = 5
assert_equal "Liquid error: Memory limits exceeded", t.render() assert_equal "Liquid error: Memory limits exceeded", t.render
t.resource_limits.render_length_limit = 11 t.resource_limits.render_length_limit = 11
assert_equal "Liquid error: Memory limits exceeded", t.render() assert_equal "Liquid error: Memory limits exceeded", t.render
t.resource_limits.render_length_limit = 12 t.resource_limits.render_length_limit = 12
assert_equal "ababab", t.render() assert_equal "ababab", t.render
end end
def test_default_resource_limits_unaffected_by_render_with_context def test_default_resource_limits_unaffected_by_render_with_context
@@ -200,17 +215,109 @@ class TemplateTest < Minitest::Test
assert_equal 'ruby error in drop', e.message assert_equal 'ruby error in drop', e.message
end end
def test_exception_handler_doesnt_reraise_if_it_returns_false def test_exception_renderer_that_returns_string
exception = nil exception = nil
Template.parse("{{ 1 | divided_by: 0 }}").render({}, exception_handler: ->(e) { exception = e; false }) handler = ->(e) { exception = e; '<!-- error -->' }
assert exception.is_a?(ZeroDivisionError)
output = Template.parse("{{ 1 | divided_by: 0 }}").render({}, exception_renderer: handler)
assert exception.is_a?(Liquid::ZeroDivisionError)
assert_equal '<!-- error -->', output
end end
def test_exception_handler_does_reraise_if_it_returns_true def test_exception_renderer_that_raises
exception = nil exception = nil
assert_raises(ZeroDivisionError) do assert_raises(Liquid::ZeroDivisionError) do
Template.parse("{{ 1 | divided_by: 0 }}").render({}, exception_handler: ->(e) { exception = e; true }) Template.parse("{{ 1 | divided_by: 0 }}").render({}, exception_renderer: ->(e) { exception = e; raise })
end end
assert exception.is_a?(ZeroDivisionError) assert exception.is_a?(Liquid::ZeroDivisionError)
end
def test_global_filter_option_on_render
global_filter_proc = ->(output) { "#{output} filtered" }
rendered_template = Template.parse("{{name}}").render({ "name" => "bob" }, global_filter: global_filter_proc)
assert_equal 'bob filtered', rendered_template
end
def test_global_filter_option_when_native_filters_exist
global_filter_proc = ->(output) { "#{output} filtered" }
rendered_template = Template.parse("{{name | upcase}}").render({ "name" => "bob" }, global_filter: global_filter_proc)
assert_equal 'BOB filtered', rendered_template
end
def test_undefined_variables
t = Template.parse("{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}")
result = t.render({ 'x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } } }, { strict_variables: true })
assert_equal '33 32 ', result
assert_equal 3, t.errors.count
assert_instance_of Liquid::UndefinedVariable, t.errors[0]
assert_equal 'Liquid error: undefined variable y', t.errors[0].message
assert_instance_of Liquid::UndefinedVariable, t.errors[1]
assert_equal 'Liquid error: undefined variable b', t.errors[1].message
assert_instance_of Liquid::UndefinedVariable, t.errors[2]
assert_equal 'Liquid error: undefined variable d', t.errors[2].message
end
def test_undefined_variables_raise
t = Template.parse("{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}")
assert_raises UndefinedVariable do
t.render!({ 'x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } } }, { strict_variables: true })
end
end
def test_undefined_drop_methods
d = DropWithUndefinedMethod.new
t = Template.new.parse('{{ foo }} {{ woot }}')
result = t.render(d, { strict_variables: true })
assert_equal 'foo ', result
assert_equal 1, t.errors.count
assert_instance_of Liquid::UndefinedDropMethod, t.errors[0]
end
def test_undefined_drop_methods_raise
d = DropWithUndefinedMethod.new
t = Template.new.parse('{{ foo }} {{ woot }}')
assert_raises UndefinedDropMethod do
t.render!(d, { strict_variables: true })
end
end
def test_undefined_filters
t = Template.parse("{{a}} {{x | upcase | somefilter1 | somefilter2 | somefilter3}}")
filters = Module.new do
def somefilter3(v)
"-#{v}-"
end
end
result = t.render({ 'a' => 123, 'x' => 'foo' }, { filters: [filters], strict_filters: true })
assert_equal '123 ', result
assert_equal 1, t.errors.count
assert_instance_of Liquid::UndefinedFilter, t.errors[0]
assert_equal 'Liquid error: undefined filter somefilter1', t.errors[0].message
end
def test_undefined_filters_raise
t = Template.parse("{{x | somefilter1 | upcase | somefilter2}}")
assert_raises UndefinedFilter do
t.render!({ 'x' => 'foo' }, { strict_filters: true })
end
end
def test_using_range_literal_works_as_expected
t = Template.parse("{% assign foo = (x..y) %}{{ foo }}")
result = t.render({ 'x' => 1, 'y' => 5 })
assert_equal '1..5', result
t = Template.parse("{% assign nums = (x..y) %}{% for num in nums %}{{ num }}{% endfor %}")
result = t.render({ 'x' => 1, 'y' => 5 })
assert_equal '12345', result
end end
end end

View File

@@ -0,0 +1,525 @@
require 'test_helper'
class TrimModeTest < Minitest::Test
include Liquid
# Make sure the trim isn't applied to standard output
def test_standard_output
text = <<-END_TEMPLATE
<div>
<p>
{{ 'John' }}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>
John
</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
def test_variable_output_with_multiple_blank_lines
text = <<-END_TEMPLATE
<div>
<p>
{{- 'John' -}}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>John</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
def test_tag_output_with_multiple_blank_lines
text = <<-END_TEMPLATE
<div>
<p>
{%- if true -%}
yes
{%- endif -%}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>yes</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
# Make sure the trim isn't applied to standard tags
def test_standard_tags
whitespace = ' '
text = <<-END_TEMPLATE
<div>
<p>
{% if true %}
yes
{% endif %}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>
#{whitespace}
yes
#{whitespace}
</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
text = <<-END_TEMPLATE
<div>
<p>
{% if false %}
no
{% endif %}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>
#{whitespace}
</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
# Make sure the trim isn't too agressive
def test_no_trim_output
text = '<p>{{- \'John\' -}}</p>'
expected = '<p>John</p>'
assert_template_result(expected, text)
end
# Make sure the trim isn't too agressive
def test_no_trim_tags
text = '<p>{%- if true -%}yes{%- endif -%}</p>'
expected = '<p>yes</p>'
assert_template_result(expected, text)
text = '<p>{%- if false -%}no{%- endif -%}</p>'
expected = '<p></p>'
assert_template_result(expected, text)
end
def test_single_line_outer_tag
text = '<p> {%- if true %} yes {% endif -%} </p>'
expected = '<p> yes </p>'
assert_template_result(expected, text)
text = '<p> {%- if false %} no {% endif -%} </p>'
expected = '<p></p>'
assert_template_result(expected, text)
end
def test_single_line_inner_tag
text = '<p> {% if true -%} yes {%- endif %} </p>'
expected = '<p> yes </p>'
assert_template_result(expected, text)
text = '<p> {% if false -%} no {%- endif %} </p>'
expected = '<p> </p>'
assert_template_result(expected, text)
end
def test_single_line_post_tag
text = '<p> {% if true -%} yes {% endif -%} </p>'
expected = '<p> yes </p>'
assert_template_result(expected, text)
text = '<p> {% if false -%} no {% endif -%} </p>'
expected = '<p> </p>'
assert_template_result(expected, text)
end
def test_single_line_pre_tag
text = '<p> {%- if true %} yes {%- endif %} </p>'
expected = '<p> yes </p>'
assert_template_result(expected, text)
text = '<p> {%- if false %} no {%- endif %} </p>'
expected = '<p> </p>'
assert_template_result(expected, text)
end
def test_pre_trim_output
text = <<-END_TEMPLATE
<div>
<p>
{{- 'John' }}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>John
</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
def test_pre_trim_tags
text = <<-END_TEMPLATE
<div>
<p>
{%- if true %}
yes
{%- endif %}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>
yes
</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
text = <<-END_TEMPLATE
<div>
<p>
{%- if false %}
no
{%- endif %}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>
</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
def test_post_trim_output
text = <<-END_TEMPLATE
<div>
<p>
{{ 'John' -}}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>
John</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
def test_post_trim_tags
text = <<-END_TEMPLATE
<div>
<p>
{% if true -%}
yes
{% endif -%}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>
yes
</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
text = <<-END_TEMPLATE
<div>
<p>
{% if false -%}
no
{% endif -%}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>
</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
def test_pre_and_post_trim_tags
text = <<-END_TEMPLATE
<div>
<p>
{%- if true %}
yes
{% endif -%}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>
yes
</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
text = <<-END_TEMPLATE
<div>
<p>
{%- if false %}
no
{% endif -%}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p></p>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
def test_post_and_pre_trim_tags
text = <<-END_TEMPLATE
<div>
<p>
{% if true -%}
yes
{%- endif %}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>
yes
</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
whitespace = ' '
text = <<-END_TEMPLATE
<div>
<p>
{% if false -%}
no
{%- endif %}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>
#{whitespace}
</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
def test_trim_output
text = <<-END_TEMPLATE
<div>
<p>
{{- 'John' -}}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>John</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
def test_trim_tags
text = <<-END_TEMPLATE
<div>
<p>
{%- if true -%}
yes
{%- endif -%}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>yes</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
text = <<-END_TEMPLATE
<div>
<p>
{%- if false -%}
no
{%- endif -%}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p></p>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
def test_whitespace_trim_output
text = <<-END_TEMPLATE
<div>
<p>
{{- 'John' -}},
{{- '30' -}}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>John,30</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
def test_whitespace_trim_tags
text = <<-END_TEMPLATE
<div>
<p>
{%- if true -%}
yes
{%- endif -%}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>yes</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
text = <<-END_TEMPLATE
<div>
<p>
{%- if false -%}
no
{%- endif -%}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p></p>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
def test_complex_trim_output
text = <<-END_TEMPLATE
<div>
<p>
{{- 'John' -}}
{{- '30' -}}
</p>
<b>
{{ 'John' -}}
{{- '30' }}
</b>
<i>
{{- 'John' }}
{{ '30' -}}
</i>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>John30</p>
<b>
John30
</b>
<i>John
30</i>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
def test_complex_trim
text = <<-END_TEMPLATE
<div>
{%- if true -%}
{%- if true -%}
<p>
{{- 'John' -}}
</p>
{%- endif -%}
{%- endif -%}
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div><p>John</p></div>
END_EXPECTED
assert_template_result(expected, text)
end
def test_raw_output
whitespace = ' '
text = <<-END_TEMPLATE
<div>
{% raw %}
{%- if true -%}
<p>
{{- 'John' -}}
</p>
{%- endif -%}
{% endraw %}
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
#{whitespace}
{%- if true -%}
<p>
{{- 'John' -}}
</p>
{%- endif -%}
#{whitespace}
</div>
END_EXPECTED
assert_template_result(expected, text)
end
end # TrimModeTest

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