Compare commits

...

114 Commits

Author SHA1 Message Date
Dylan Thacker-Smith
d3e4e4c419 Implement tokenization in a C extension. 2014-03-26 03:20:34 -04:00
Florian Weingarten
3dbb35d823 Merge branch 'freeze_all_the_things'
Conflicts:
	History.md
	lib/liquid/tags/assign.rb
	lib/liquid/tags/capture.rb
	lib/liquid/tags/decrement.rb
	lib/liquid/tags/if.rb
2014-03-24 12:39:34 -04:00
Florian Weingarten
44f29a87a8 Update history 2014-03-24 12:35:47 -04:00
Dylan Thacker-Smith
f0afbc27e0 Add regression test for raw tags with open variable tags. 2014-03-24 10:01:03 -04:00
Dylan Thacker-Smith
fdf03076e0 Revert "Merge pull request #325 from Shopify/remove-variable-incomplete-end"
That pull request broke raw tags with open variable tags. E.g.

{% raw %}
{{
{% endraw %}
{{ 1 }}

This reverts commit fbaabf3b59, reversing
changes made to af24d2c2ab.
2014-03-24 09:59:07 -04:00
Dylan Thacker-Smith
d1bfda15e3 Add profile:strict rake task. 2014-03-21 21:54:53 -04:00
Dylan Thacker-Smith
d8d9984a7b Remove some unused regexes. 2014-03-21 15:50:14 -04:00
Dylan Thacker-Smith
fbaabf3b59 Merge pull request #325 from Shopify/remove-variable-incomplete-end
Allow quoted single curly braces in variables.
2014-03-21 13:48:47 -04:00
Dylan Thacker-Smith
7e0ef867d2 Make tag/variable termination error clearer. 2014-03-21 02:04:01 -04:00
Dylan Thacker-Smith
3682414cc4 Allow quoted single curly braces in variables. 2014-03-21 02:04:01 -04:00
Dylan Thacker-Smith
af24d2c2ab Add missing PR reference and author to a History.md entry. 2014-03-21 02:03:31 -04:00
Dylan Thacker-Smith
4ee43bc5d2 Merge pull request #324 from Shopify/multiline-tags-and-vars
Allow newlines in tags and variables.
2014-03-21 00:26:14 -04:00
Dylan Thacker-Smith
1320a69fca Merge pull request #323 from Shopify/render_bang_in_tests
Use render! in tests to make debugging test failures easier.
2014-03-20 18:33:40 -04:00
Dylan Thacker-Smith
3b14e27f55 Allow newlines in tags and variables. 2014-03-20 17:27:03 -04:00
Dylan Thacker-Smith
face33a137 Merge pull request #321 from Shopify/move-tag-parse-out-of-initialize
Refactor to create tags with a parse class method instead of new.
2014-03-20 16:15:58 -04:00
Dylan Thacker-Smith
d4ecaff8b8 Refactor to create tags with a parse class method instead of new.
By moving parse out of the initializer, we can call super at the start of
the initializers for subclasses, and avoid the nasty allocate hack.
2014-03-20 16:10:10 -04:00
Dylan Thacker-Smith
a5990042ff Use render! in tests to make debugging test failures easier. 2014-03-20 12:04:17 -04:00
Florian Weingarten
e190bbba9e move change to top 2014-03-19 18:15:31 -04:00
Dylan Thacker-Smith
4b5e41d04e Merge pull request #322 from Shopify/use-render-bang-in-benchmarks
Use render! in benchmarks to avoid making it faster by breaking things.
2014-03-19 18:06:15 -04:00
Dylan Thacker-Smith
4b69f6ae83 Use render! in benchmarks to avoid making it faster by breaking things. 2014-03-19 18:01:33 -04:00
Dylan Thacker-Smith
b9feb415f6 Merge pull request #320 from Shopify/move-table-row-tag
Move definition for TableRow to the tags folder.
2014-03-18 17:16:32 -04:00
Dylan Thacker-Smith
92781ec43b Move definition for TableRow to the tags folder. 2014-03-18 17:13:39 -04:00
Dylan Thacker-Smith
ff5c1f83f7 Merge pull request #318 from Shopify/stackprof
Use stackprof for profiling.
2014-03-14 10:27:46 -04:00
Dylan Thacker-Smith
e2b337af2f Merge pull request #317 from Shopify/string-anchors
Use start and end of string rather than line matching in regexes.
2014-03-14 10:17:33 -04:00
Dylan Thacker-Smith
f373b1003d Use stackprof for profiling. 2014-03-14 10:03:50 -04:00
Dylan Thacker-Smith
503d924274 Use start and end of string rather than line matching in regexes. 2014-03-13 17:56:42 -04:00
Dylan Thacker-Smith
3efa8e8762 Merge pull request #310 from Shopify/faster-increment-used-resources
Speed up Context#increment_used_resources
2014-02-25 00:18:27 -05:00
Dylan Thacker-Smith
3c06d837b5 Speed up Context#increment_used_resources 2014-02-24 23:56:39 -05:00
Arthur Nogueira Neves
d3fc30ef85 Merge pull request #309 from bogdan/argument_error_for_filters
Raise liquid argument error instead of ruby argument
2014-02-21 15:13:11 -05:00
Bogdan Gusiev
f23e69d565 Raise liquid argument error instead of ruby argument
Wrong number of arguments for filter invocation now raises
Liuqid::ArgumentError but not ::ArgumentError
2014-02-21 22:12:11 +02:00
Adam Doeler
fa179e811d Merge pull request #306 from Shopify/default_to_s_for_drops
Liquid::Drop should not return a string representation of standard ruby objects
2014-02-13 13:45:01 -05:00
Adam Doeler
18907fc570 Updates History.md 2014-02-10 10:24:37 -05:00
Adam Doeler
5f8a028a56 Liquid::Drop should not return a string representation of standard ruby objects 2014-02-07 14:48:02 -05:00
Florian Weingarten
0a7de51e2b Revert some freezes on non-literals 2014-01-27 14:56:07 -05:00
Florian Weingarten
765751b9cb Merge pull request #303 from Shopify/strip_filter
Add strip, lstrip, rstrip filters
2014-01-24 08:09:19 -08:00
Florian Weingarten
d2827bfa76 Add strip, lstrip, rstrip filters 2014-01-24 11:04:43 -05:00
Florian Weingarten
70d92b84ab Rename test 2014-01-24 10:55:45 -05:00
Florian Weingarten
808fa244ca Merge pull request #250 from wildfireapp/correct-if-nodelists
Correct if-statement nodelist.
2014-01-13 12:47:51 -08:00
Nicholas Jones
5570f697fd Update history 2014-01-13 12:46:43 -08:00
Nicholas Jones
8f9f12e542 Merge remote-tracking branch 'upstream/master' into correct-if-nodelists
Conflicts:
	test/liquid/tags/for_tag_test.rb
	test/liquid/tags/if_else_tag_test.rb
2014-01-13 12:43:43 -08:00
Florian Weingarten
17dae40707 Fix History.md ordering 2014-01-13 15:38:18 -05:00
Nicholas Jones
06e2f2577f Add else blocks to for and case nodelists 2014-01-13 11:53:25 -08:00
Florian Weingarten
ee7edacacc Merge pull request #298 from Shopify/respond_to_resource_counting_bug
Fix resource counting bug with respond_to?(:length)
2014-01-08 10:37:48 -08:00
Florian Weingarten
62a86a25ae update history 2014-01-08 13:37:24 -05:00
Florian Weingarten
c6e0c1e490 Fix resource counting bug with respond_to?(:length) 2014-01-08 13:00:53 -05:00
Florian Weingarten
43ac8d560b Freeze all the things 2014-01-07 12:35:16 -05:00
Arthur Nogueira Neves
0388376925 Merge pull request #296 from Shopify/ruby2.1.0
Ruby2.1.0
2014-01-07 08:55:03 -08:00
Arthur Neves
57c8583dc3 Add 2.1.0 to travis 2014-01-07 11:37:24 -05:00
Arthur Neves
a13f237d3c Remove some 192 tests 2014-01-07 11:37:01 -05:00
Arthur Nogueira Neves
9ed2fa425b Merge pull request #295 from Shopify/remove_ruby_debug
remove ruby-debug
2014-01-07 08:34:19 -08:00
Arthur Neves
208c6c8800 remove ruby-debug 2014-01-07 11:31:46 -05:00
Florian Weingarten
9ec2b9da2d Rename tests because of name clashes (same method name used twice) 2014-01-07 11:20:32 -05:00
Florian Weingarten
be7bef4d0b Merge pull request #284 from agladkyi/custom-patterns-for-template-filenames
Custom patterns for template filenames
2013-12-16 11:28:19 -08:00
Andrei Gladkyi
0ae42bbc32 Added separate test for custom patterns specifying
+ updated History.md
2013-12-16 17:48:43 +02:00
Gaurav Chande
0ec69890ab Merge pull request #287 from Shopify/fix-escape-once
Fix escape_once filter
2013-12-02 08:57:24 -08:00
Gaurav Chande
5e8f2f8bd0 Fix escape_once filter 2013-12-01 20:37:47 -05:00
Andrei Gladkyi
0edb252489 Option to specify custom pattern for template filenames 2013-11-30 17:55:53 +02:00
Florian Weingarten
5ce36f79e9 Add Tom's slice loading change to History.md 2013-11-25 11:12:33 -05:00
Florian Weingarten
2d1f15281b Merge pull request #282 from Shopify/load_slice
allow drops to optimize loading a slice of elements
2013-11-25 08:12:06 -08:00
Florian Weingarten
4647e6d86b Remove unnecessary comment, add joost's change to History.md 2013-11-25 10:52:46 -05:00
Florian Weingarten
f5620d4670 Merge branch 'master' of github.com:joost/liquid into joost-master 2013-11-25 10:51:48 -05:00
Florian Weingarten
f1a5f6899b Add raggi's change to History, remove Ruby 1.8 code from test 2013-11-25 10:48:03 -05:00
Florian Weingarten
de497eaed2 Merge branch 'class_cache' of github.com:wildfireapp/liquid into wildfireapp-class_cache 2013-11-25 10:46:18 -05:00
Tom Burns
30e5f06313 don't make original slice_collection_using_each private 2013-11-25 10:37:10 -05:00
Arthur Neves
d465d5e20c Add ruby 2.1 on travis 2013-11-25 10:35:24 -05:00
Arthur Neves
7989e834f3 Allow rbx failure and not 2.0.0 2013-11-25 10:33:41 -05:00
Arthur Neves
c264459931 Update history log
Conflicts:
	History.md
2013-11-25 10:28:12 -05:00
Tom Burns
e667352629 move slice_collection optimization to utils 2013-11-24 14:00:23 -05:00
Tom Burns
2c26a880f0 add another test showing equivalent functionality 2013-11-24 12:32:32 -05:00
Tom Burns
cf49b06ccc allow drops to optimize loading a slice of elements 2013-11-24 12:29:15 -05:00
Florian Weingarten
f015d804ea Update history 2013-11-11 09:03:39 -05:00
Arthur Nogueira Neves
78c42bce44 Update README.md
Show travis badge from master only.
2013-11-04 18:37:53 -05:00
Florian Weingarten
445f19d454 Merge pull request #276 from Shopify/remove_some_1.8_code
Remove some legacy Ruby 1.8 compatibility code
2013-11-01 05:50:29 -07:00
Florian Weingarten
a599a26f1a Remove some legacy Ruby 1.8 compatibility code 2013-10-31 15:35:12 -04:00
Dylan Thacker-Smith
4e14a651a7 Merge pull request #274 from Shopify/restrict-send-on-conditions
security: Prevent arbitrary method invocation on conditions in if tag.
2013-10-28 10:01:47 -07:00
Dylan Thacker-Smith
cc982e92d0 security: Prevent arbitrary method invocation on conditions in if tag. 2013-10-28 12:20:27 -04:00
Bouke van der Bijl
71a386f723 Merge pull request #273 from Shopify/make-if-less-dangerous
Make if less dangerous
2013-10-28 06:39:05 -07:00
Bouke van der Bijl
2f50a0c422 Add history message 2013-10-28 14:10:13 +01:00
Bouke van der Bijl
a5cd661dd9 Use public_send on condition creation
This makes sure you can't call Kernel methods like `throw`
2013-10-28 13:57:28 +01:00
Bouke van der Bijl
511ee7fbe1 Remove to_sym from condition creation
This prevents a DoS http://www.tricksonrails.com/2010/06/avoid-memory-leaks-in-ruby-rails-code-and-protect-against-denial-of-service/
2013-10-28 13:57:28 +01:00
Joost Hietbrink
5eddfe87d0 Support for passing variables to snippets in subdirs
Now you can use "include 'some/snippet' with variable".
2013-10-16 11:55:12 +02:00
Florian Weingarten
9b910a4e6d Update README.md
Remove old wiki link
2013-10-15 13:14:39 -04:00
Arthur Neves
7e1a0be752 Put travis badge on top 2013-10-11 12:08:19 -04:00
Arthur Neves
c9ea578b64 Drop 1.8 support on travis CI and add 2.0.0 2013-10-11 12:03:01 -04:00
Florian Weingarten
549777ae53 Merge pull request #267 from djreimer/default-filter
Add default filter to standard filters
2013-10-11 07:59:21 -07:00
Derrick Reimer
6710ef60bc Add default filter to history 2013-10-10 09:17:49 -07:00
Arthur Nogueira Neves
5fdab083b0 Merge pull request #268 from Shopify/better_rake
Improve Rakefile
2013-10-10 07:55:08 -07:00
Arthur Neves
0644da02bb Improve Rakefile 2013-10-10 10:44:16 -04:00
Derrick Reimer
5db1695694 Add default filter to standard filters 2013-10-09 16:07:32 -07:00
Florian Weingarten
a25ed17e2b Merge pull request #266 from Shopify/fix_map_on_hashes
Fix map filter on Hash inputs
2013-10-09 14:18:22 -07:00
Arthur Neves
fa3155fdcc Bump version to 3.0.0 2013-10-09 17:12:03 -04:00
Arthur Neves
534338f923 Update history
Add 3.0.0 history
Add changes related to 2.5.x
2013-10-09 17:11:19 -04:00
Florian Weingarten
96b30a89a9 Fix map filter on Hash inputs 2013-10-08 08:18:03 -04:00
Florian Weingarten
81d3733f57 Regression test for change of blank? default behaviour (2efe809e11) 2013-09-23 09:38:45 -04:00
Florian Weingarten
2efe809e11 Make blank? default to false for all tags to maintain backwards compatible 2013-09-23 08:43:26 -04:00
Simon Hørup Eskildsen
322ecca145 Merge pull request #261 from Shopify/fix-i18n-regex-warning
Fix i18n regex warning in Ruby 1.8
2013-09-16 11:36:47 -07:00
Simon Eskildsen
6ce0b9d705 Fix i18n regex warning in Ruby 1.8 2013-09-16 14:35:33 -04:00
Florian Weingarten
736998df64 Merge pull request #257 from Shopify/fix_comment_tags_second_try
Fix unknown tags in comment tags, second try
2013-09-11 13:33:26 -07:00
Florian Weingarten
5b172a4c05 Fix unknown tags in comment tags, second try 2013-09-11 12:31:54 -04:00
Florian Weingarten
bd20595f1a Add regression test for comment tag 2013-09-11 12:14:27 -04:00
Florian Weingarten
f938756a58 Revert "Merge pull request #256 from Shopify/unknown_tags_in_comments"
This reverts commit 1ae8c0e90a, reversing
changes made to 01d352bc51.
2013-09-11 12:13:55 -04:00
Florian Weingarten
1ae8c0e90a Merge pull request #256 from Shopify/unknown_tags_in_comments
Fix handling of unknown tags in comments
2013-09-11 07:59:44 -07:00
Florian Weingarten
45795f8766 Fix handling of unknown tags in comments 2013-09-11 10:40:33 -04:00
Florian Weingarten
01d352bc51 Move stuff in test around 2013-09-11 14:40:11 +02:00
Ishibashi Hideto
70513fccaf remove include Liquid from the class CustomInclude and substitute QuotedFragment with Liquid::QuotedFragment 2013-09-11 03:25:06 +09:00
Ishibashi Hideto
a5285d3d09 test for the Jekyll's issue: [Liquid doesn't render my partial · Issue #1519 · mojombo/jekyll](https://github.com/mojombo/jekyll/issues/1519) 2013-09-10 22:58:56 +09:00
James Tucker
fbfd5712df Merge pull request #3 from dntj/master
Add a test for corrected if-nodelist
2013-09-07 18:30:55 -07:00
Nicholas Jones
90593d3f18 Add a test for corrected if-nodelist 2013-09-07 11:15:49 -07:00
Tristan Hume
beded415cd Merge pull request #253 from trishume/fix-range-parsing
Fix bad range parsing.
2013-09-07 05:17:30 -07:00
James Tucker
13c826933c Update against failed cherry-pick 2013-09-07 01:42:41 +00:00
Tristan Hume
7c5b3e0c3b Fix bad range parsing. 2013-09-04 18:13:31 -04:00
Nick Jones
ca5bc5d75b Correct if-statement nodelist.
The nodelist returned by all tags is a list of containing nodes, except for the if tag.  This correct that inconsistency
2013-08-31 19:03:50 +00:00
James Tucker
d4679cd550 Strainer test now works on 1.8 2013-08-31 18:57:09 +00:00
James Tucker
9b2d5b7dd3 Add a class cache to avoid runtime extend calls
* Strainer has a class cache that creates Strainer subclasses for each filter
   set that is used on .create calls.
 * Context now creates a list of filters and passes this to Strainer.create to
   utilize the class cache in almost all use cases.
 * If add_filter was called after a render, then the method cache may still be
   invalidated.

Conflicts:

	lib/liquid/strainer.rb
2013-08-31 18:56:35 +00:00
80 changed files with 1233 additions and 696 deletions

5
.gitignore vendored
View File

@@ -5,3 +5,8 @@ pkg
*.rbc *.rbc
.rvmrc .rvmrc
.ruby-version .ruby-version
Gemfile.lock
/ext/liquid/Makefile
*.o
*.bundle
/tmp

View File

@@ -1,11 +1,14 @@
rvm: rvm:
- 1.8.7
- 1.9.3 - 1.9.3
- ree - 2.0.0
- jruby-18mode - 2.1.0
- jruby-19mode - jruby-19mode
- rbx-18mode - jruby-head
- rbx-19mode - rbx-19mode
matrix:
allow_failures:
- rvm: rbx-19mode
- rvm: jruby-head
script: "rake test" script: "rake test"

3
Gemfile Normal file
View File

@@ -0,0 +1,3 @@
source 'https://rubygems.org'
gemspec

View File

@@ -1,18 +1,36 @@
# Liquid Version History # Liquid Version History
IMPORTANT: Liquid 2.6 is going to be the last version of Liquid which maintains explicit Ruby 1.8 compatability. ## 3.0.0 / not yet released / branch "master"
The following releases will only be tested against Ruby 1.9 and Ruby 2.0 and are likely to break on Ruby 1.8.
## 2.6.0 / Master branch (not yet released)
* ... * ...
* Freeze lots of string literals for new Ruby 2.1 optimization, see #297 [Florian Weingarten, fw42]
* Allow newlines in tags and variables, see #324 [Dylan Thacker-Smith, dylanahsmith]
* Tag#parse is called after initialize, which now takes options instead of tokens as the 3rd argument. See #321 [Dylan Thacker-Smith, dylanahsmith]
* Raise `Liquid::ArgumentError` instead of `::ArgumentError` when filter has wrong number of arguments #309 [Bogdan Gusiev, bogdan]
* Add a to_s default for liquid drops, see #306 [Adam Doeler, releod]
* Add strip, lstrip, and rstrip to standard filters [Florian Weingarten, fw42]
* Make if, for & case tags return complete and consistent nodelists, see #250 [Nick Jones, dntj]
* Prevent arbitrary method invocation on condition objects, see #274 [Dylan Thacker-Smith, dylanahsmith]
* Don't call to_sym when creating conditions for security reasons, see #273 [Bouke van der Bijl, bouk]
* Fix resource counting bug with respond_to?(:length), see #263 [Florian Weingarten, fw42]
* Allow specifying custom patterns for template filenames, see #284 [Andrei Gladkyi, agladkyi]
* Allow drops to optimize loading a slice of elements, see #282 [Tom Burns, boourns]
* Support for passing variables to snippets in subdirs, see #271 [Joost Hietbrink, joost]
* Add a class cache to avoid runtime extend calls, see #249 [James Tucker, raggi]
* Remove some legacy Ruby 1.8 compatibility code, see #276 [Florian Weingarten, fw42]
* Add default filter to standard filters, see #267 [Derrick Reimer, djreimer]
* Add optional strict parsing and warn parsing, see #235 [Tristan Hume, trishume] * Add optional strict parsing and warn parsing, see #235 [Tristan Hume, trishume]
* Add I18n syntax error translation, see #241 [Simon Hørup Eskildsen, Sirupsen] * Add I18n syntax error translation, see #241 [Simon Hørup Eskildsen, Sirupsen]
* Make sort filter work on enumerable drops, see #239 [Florian Weingarten, fw42] * Make sort filter work on enumerable drops, see #239 [Florian Weingarten, fw42]
* Fix clashing method names in enumerable drops, see #238 [Florian Weingarten, fw42] * Fix clashing method names in enumerable drops, see #238 [Florian Weingarten, fw42]
* Make map filter work on enumerable drops, see #233 [Florian Weingarten, fw42] * Make map filter work on enumerable drops, see #233 [Florian Weingarten, fw42]
* Fix security issue with map filter, see #230, #232, #234, #237 [Florian Weingarten, fw42]
* Improved whitespace stripping for blank blocks, related to #216 [Florian Weingarten, fw42] * Improved whitespace stripping for blank blocks, related to #216 [Florian Weingarten, fw42]
## 2.6.0 / 2013-11-25 / branch "2.6-stable"
IMPORTANT: Liquid 2.6 is going to be the last version of Liquid which maintains explicit Ruby 1.8 compatability.
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, joliss]
* Bugfix for #114: strip_html filter supports style tags [James Allardice, jamesallardice] * Bugfix for #114: strip_html filter supports style tags [James Allardice, jamesallardice]
@@ -21,6 +39,7 @@ The following releases will only be tested against Ruby 1.9 and Ruby 2.0 and are
* Bugfix for #204: 'raw' parsing bug [Florian Weingarten, fw42] * Bugfix for #204: 'raw' parsing bug [Florian Weingarten, fw42]
* Bugfix for #150: 'for' parsing bug [Peter Schröder, phoet] * Bugfix for #150: 'for' parsing bug [Peter Schröder, phoet]
* Bugfix for #126: Strip CRLF in strip_newline [Peter Schröder, phoet] * Bugfix for #126: Strip CRLF in strip_newline [Peter Schröder, phoet]
* Bugfix for #174, "can't convert Fixnum into String" for "replace" [wǒ_is神仙, 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, darkhelmet]
* Resource limits [Florian Weingarten, fw42] * Resource limits [Florian Weingarten, fw42]
* Add reverse filter [Jay Strybis, unreal] * Add reverse filter [Jay Strybis, unreal]
@@ -31,6 +50,21 @@ The following releases will only be tested against Ruby 1.9 and Ruby 2.0 and are
* Better documentation for 'include' tag (closes #163) [Peter Schröder, phoet] * Better documentation for 'include' tag (closes #163) [Peter Schröder, phoet]
* 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, arthurnn]
## 2.5.4 / 2013-11-11 / branch "2.5-stable"
* Fix "can't convert Fixnum into String" for "replace", see #173, [wǒ_is神仙, jsw0528]
## 2.5.3 / 2013-10-09
* #232, #234, #237: Fix map filter bugs [Florian Weingarten, fw42]
## 2.5.2 / 2013-09-03 / deleted
Yanked from rubygems, as it contained too many changes that broke compatibility. Those changes will be on following major releases.
## 2.5.1 / 2013-07-24
* #230: Fix security issue with map filter, Use invoke_drop in map filter [Florian Weingarten, fw42]
## 2.5.0 / 2013-03-06 ## 2.5.0 / 2013-03-06

View File

@@ -1,9 +1,9 @@
[![Build Status](https://secure.travis-ci.org/Shopify/liquid.png?branch=master)](http://travis-ci.org/Shopify/liquid)
# Liquid template engine # Liquid template engine
* [Contributing guidelines](CONTRIBUTING.md) * [Contributing guidelines](CONTRIBUTING.md)
* [Version history](History.md) * [Version history](History.md)
* [Liquid documentation from Shopify](http://docs.shopify.com/themes/liquid-basics) * [Liquid documentation from Shopify](http://docs.shopify.com/themes/liquid-basics)
* [Liquid Wiki from Shopify](http://wiki.shopify.com/Liquid)
* [Liquid Wiki at GitHub](https://github.com/Shopify/liquid/wiki) * [Liquid Wiki at GitHub](https://github.com/Shopify/liquid/wiki)
* [Website](http://liquidmarkup.org/) * [Website](http://liquidmarkup.org/)
@@ -71,5 +71,3 @@ 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.
[![Build Status](https://secure.travis-ci.org/Shopify/liquid.png)](http://travis-ci.org/Shopify/liquid)

View File

@@ -1,9 +1,7 @@
#!/usr/bin/env ruby
require 'rubygems'
require 'rake' require 'rake'
require 'rake/testtask' require 'rake/testtask'
require 'rubygems/package_task' $LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
require "liquid/version"
task :default => 'test' task :default => 'test'
@@ -29,14 +27,20 @@ task :test do
Rake::Task['base_test'].invoke Rake::Task['base_test'].invoke
end end
gemspec = eval(File.read('liquid.gemspec')) task :gem => :build
Gem::PackageTask.new(gemspec) do |pkg| task :build do
pkg.gem_spec = gemspec system "gem build liquid.gemspec"
end end
desc "Build the gem and release it to rubygems.org" task :install => :build do
task :release => :gem do system "gem install liquid-#{Liquid::VERSION}.gem"
sh "gem push pkg/liquid-#{gemspec.version}.gem" end
task :release => :build do
system "git tag -a v#{Liquid::VERSION} -m 'Tagging #{Liquid::VERSION}'"
system "git push --tags"
system "gem push liquid-#{Liquid::VERSION}.gem"
system "rm liquid-#{Liquid::VERSION}.gem"
end end
namespace :benchmark do namespace :benchmark do
@@ -60,9 +64,9 @@ namespace :profile do
ruby "./performance/profile.rb" ruby "./performance/profile.rb"
end end
desc "Run KCacheGrind" desc "Run the liquid profile/performance coverage with strict parsing"
task :grind => :run do task :strict do
system "qcachegrind /tmp/liquid.rubyprof_calltreeprinter.txt" ruby "./performance/profile.rb strict"
end end
end end
@@ -71,3 +75,11 @@ 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
if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'ruby'
require 'rake/extensiontask'
Rake::ExtensionTask.new "liquid" do |ext|
ext.lib_dir = "lib/liquid"
end
Rake::Task[:test].prerequisites << :compile
end

View File

@@ -13,7 +13,7 @@ class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet
def handle(type, req, res) def handle(type, req, res)
@request, @response = req, res @request, @response = req, res
@request.path_info =~ /(\w+)$/ @request.path_info =~ /(\w+)\z/
@action = $1 || 'index' @action = $1 || 'index'
@assigns = send(@action) if respond_to?(@action) @assigns = send(@action) if respond_to?(@action)

4
ext/liquid/extconf.rb Normal file
View File

@@ -0,0 +1,4 @@
require 'mkmf'
$CFLAGS << ' -Wall -Werror'
$warnflags.gsub!(/-Wdeclaration-after-statement/, "")
create_makefile("liquid/liquid")

9
ext/liquid/liquid.c Normal file
View File

@@ -0,0 +1,9 @@
#include "liquid.h"
VALUE mLiquid;
void Init_liquid(void)
{
mLiquid = rb_define_module("Liquid");
init_liquid_tokenizer();
}

11
ext/liquid/liquid.h Normal file
View File

@@ -0,0 +1,11 @@
#ifndef LIQUID_H
#define LIQUID_H
#include <ruby.h>
#include <stdbool.h>
#include "tokenizer.h"
extern VALUE mLiquid;
#endif

137
ext/liquid/tokenizer.c Normal file
View File

@@ -0,0 +1,137 @@
#include "liquid.h"
VALUE cLiquidTokenizer;
static void tokenizer_mark(void *ptr) {
tokenizer_t *tokenizer = ptr;
rb_gc_mark(tokenizer->source);
}
static void tokenizer_free(void *ptr)
{
tokenizer_t *tokenizer = ptr;
xfree(tokenizer);
}
static size_t tokenizer_memsize(const void *ptr)
{
return ptr ? sizeof(tokenizer_t) : 0;
}
const rb_data_type_t tokenizer_data_type = {
"liquid_tokenizer",
{tokenizer_mark, tokenizer_free, tokenizer_memsize,},
#ifdef RUBY_TYPED_FREE_IMMEDIATELY
NULL, NULL, RUBY_TYPED_FREE_IMMEDIATELY
#endif
};
static VALUE tokenizer_allocate(VALUE klass)
{
VALUE obj;
tokenizer_t *tokenizer;
obj = TypedData_Make_Struct(klass, tokenizer_t, &tokenizer_data_type, tokenizer);
tokenizer->source = Qnil;
return obj;
}
static VALUE tokenizer_initialize_method(VALUE self, VALUE source)
{
tokenizer_t *tokenizer;
Check_Type(source, T_STRING);
Tokenizer_Get_Struct(self, tokenizer);
source = rb_str_dup_frozen(source);
tokenizer->source = source;
tokenizer->cursor = RSTRING_PTR(source);
tokenizer->length = RSTRING_LEN(source);
return Qnil;
}
void tokenizer_next(tokenizer_t *tokenizer, token_t *token)
{
if (tokenizer->length <= 0) {
memset(token, 0, sizeof(*token));
return;
}
const char *cursor = tokenizer->cursor;
const char *last = cursor + tokenizer->length - 1;
token->str = cursor;
token->type = TOKEN_STRING;
while (cursor < last) {
if (*cursor++ != '{')
continue;
char c = *cursor++;
if (c != '%' && c != '{')
continue;
if (cursor - tokenizer->cursor > 2) {
token->type = TOKEN_STRING;
cursor -= 2;
goto found;
}
token->type = TOKEN_INVALID;
if (c == '%') {
while (cursor < last) {
if (*cursor++ != '%')
continue;
c = *cursor++;
while (c == '%' && cursor <= last)
c = *cursor++;
if (c != '}')
continue;
token->type = TOKEN_TAG;
goto found;
}
// unterminated tag
cursor = tokenizer->cursor + 2;
goto found;
} else {
while (cursor < last) {
if (*cursor++ != '}')
continue;
if (*cursor++ != '}') {
// variable incomplete end, used to end raw tags
cursor--;
goto found;
}
token->type = TOKEN_VARIABLE;
goto found;
}
// unterminated variable
cursor = tokenizer->cursor + 2;
goto found;
}
}
cursor = last + 1;
found:
token->length = cursor - tokenizer->cursor;
tokenizer->cursor += token->length;
tokenizer->length -= token->length;
}
static VALUE tokenizer_next_method(VALUE self)
{
tokenizer_t *tokenizer;
Tokenizer_Get_Struct(self, tokenizer);
token_t token;
tokenizer_next(tokenizer, &token);
if (token.type == TOKEN_NONE)
return Qnil;
return rb_str_new(token.str, token.length);
}
void init_liquid_tokenizer()
{
cLiquidTokenizer = rb_define_class_under(mLiquid, "Tokenizer", rb_cObject);
rb_define_alloc_func(cLiquidTokenizer, tokenizer_allocate);
rb_define_method(cLiquidTokenizer, "initialize", tokenizer_initialize_method, 1);
rb_define_method(cLiquidTokenizer, "next", tokenizer_next_method, 0);
rb_define_alias(cLiquidTokenizer, "shift", "next");
}

31
ext/liquid/tokenizer.h Normal file
View File

@@ -0,0 +1,31 @@
#ifndef LIQUID_TOKENIZER_H
#define LIQUID_TOKENIZER_H
enum token_type {
TOKEN_NONE,
TOKEN_INVALID,
TOKEN_STRING,
TOKEN_TAG,
TOKEN_VARIABLE
};
typedef struct token {
enum token_type type;
const char *str;
long length;
} token_t;
typedef struct tokenizer {
VALUE source;
const char *cursor;
long length;
} tokenizer_t;
extern VALUE cLiquidTokenizer;
extern const rb_data_type_t tokenizer_data_type;
#define Tokenizer_Get_Struct(obj, sval) TypedData_Get_Struct(obj, tokenizer_t, &tokenizer_data_type, sval)
void init_liquid_tokenizer();
void tokenizer_next(tokenizer_t *tokenizer, token_t *token);
#endif

View File

@@ -21,27 +21,18 @@
module Liquid module Liquid
FilterSeparator = /\|/ FilterSeparator = /\|/
ArgumentSeparator = ',' ArgumentSeparator = ','.freeze
FilterArgumentSeparator = ':' FilterArgumentSeparator = ':'.freeze
VariableAttributeSeparator = '.' VariableAttributeSeparator = '.'.freeze
TagStart = /\{\%/ TagStart = /\{\%/
TagEnd = /\%\}/ TagEnd = /\%\}/
VariableSignature = /\(?[\w\-\.\[\]]\)?/ VariableSignature = /\(?[\w\-\.\[\]]\)?/
VariableSegment = /[\w\-]/ VariableSegment = /[\w\-]/
VariableStart = /\{\{/ VariableStart = /\{\{/
VariableEnd = /\}\}/ VariableEnd = /\}\}/
VariableIncompleteEnd = /\}\}?/
QuotedString = /"[^"]*"|'[^']*'/ QuotedString = /"[^"]*"|'[^']*'/
QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o
StrictQuotedFragment = /"[^"]+"|'[^']+'|[^\s|:,]+/
FirstFilterArgument = /#{FilterArgumentSeparator}(?:#{StrictQuotedFragment})/o
OtherFilterArgument = /#{ArgumentSeparator}(?:#{StrictQuotedFragment})/o
SpacelessFilter = /^(?:'[^']+'|"[^"]+"|[^'"])*#{FilterSeparator}(?:#{StrictQuotedFragment})(?:#{FirstFilterArgument}(?:#{OtherFilterArgument})*)?/o
Expression = /(?:#{QuotedFragment}(?:#{SpacelessFilter})*)/o
TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/o TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/o
AnyStartingTag = /\{\{|\{\%/
PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/o
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/o
VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o
end end
@@ -61,7 +52,6 @@ require 'liquid/document'
require 'liquid/variable' require 'liquid/variable'
require 'liquid/file_system' require 'liquid/file_system'
require 'liquid/template' require 'liquid/template'
require 'liquid/htmltags'
require 'liquid/standardfilters' require 'liquid/standardfilters'
require 'liquid/condition' require 'liquid/condition'
require 'liquid/module_ex' require 'liquid/module_ex'
@@ -70,3 +60,9 @@ require 'liquid/utils'
# 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[File.dirname(__FILE__) + '/liquid/tags/*.rb'].each { |f| require f }
if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'ruby'
require 'liquid/liquid'
else
require 'liquid/tokenizer'
end

View File

@@ -1,9 +1,9 @@
module Liquid module Liquid
class Block < Tag class Block < Tag
IsTag = /^#{TagStart}/o IsTag = /\A#{TagStart}/o
IsVariable = /^#{VariableStart}/o IsVariable = /\A#{VariableStart}/o
FullToken = /^#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}$/o FullToken = /\A#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
ContentOfVariable = /^#{VariableStart}(.*)#{VariableEnd}$/o ContentOfVariable = /\A#{VariableStart}(.*)#{VariableEnd}\z/om
def blank? def blank?
@blank || false @blank || false
@@ -31,7 +31,7 @@ module Liquid
# fetch the tag from registered blocks # fetch the tag from registered blocks
if tag = Template.tags[$1] if tag = Template.tags[$1]
new_tag = tag.new_with_options($1, $2, tokens, @options || {}) new_tag = tag.parse($1, $2, tokens, @options)
@blank &&= new_tag.blank? @blank &&= new_tag.blank?
@nodelist << new_tag @nodelist << new_tag
@children << new_tag @children << new_tag
@@ -41,14 +41,14 @@ module Liquid
unknown_tag($1, $2, tokens) unknown_tag($1, $2, tokens)
end end
else else
raise SyntaxError.new(options[:locale].t("errors.syntax.tag_termination", :token => token, :tag_end => TagEnd.inspect)) raise SyntaxError.new(options[:locale].t("errors.syntax.tag_termination".freeze, :token => token, :tag_end => TagEnd.inspect))
end end
when IsVariable when IsVariable
new_var = create_variable(token) new_var = create_variable(token)
@nodelist << new_var @nodelist << new_var
@children << new_var @children << new_var
@blank = false @blank = false
when '' when ''.freeze
# pass # pass
else else
@nodelist << token @nodelist << token
@@ -79,15 +79,15 @@ module Liquid
def unknown_tag(tag, params, tokens) def unknown_tag(tag, params, tokens)
case tag case tag
when 'else' when 'else'.freeze
raise SyntaxError.new(options[:locale].t("errors.syntax.unexpected_else", raise SyntaxError.new(options[:locale].t("errors.syntax.unexpected_else".freeze,
:block_name => block_name)) :block_name => block_name))
when 'end' when 'end'.freeze
raise SyntaxError.new(options[:locale].t("errors.syntax.invalid_delimiter", raise SyntaxError.new(options[: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", :tag => tag)) raise SyntaxError.new(options[:locale].t("errors.syntax.unknown_tag".freeze, :tag => tag))
end end
end end
@@ -103,7 +103,7 @@ module Liquid
token.scan(ContentOfVariable) do |content| token.scan(ContentOfVariable) do |content|
return Variable.new(content.first, @options) return Variable.new(content.first, @options)
end end
raise SyntaxError.new(options[:locale].t("errors.syntax.variable_termination", :token => token, :tag_end => VariableEnd.inspect)) raise SyntaxError.new(options[:locale].t("errors.syntax.variable_termination".freeze, :token => token, :tag_end => VariableEnd.inspect))
end end
def render(context) def render(context)
@@ -113,7 +113,7 @@ module Liquid
protected protected
def assert_missing_delimitation! def assert_missing_delimitation!
raise SyntaxError.new(options[:locale].t("errors.syntax.tag_never_closed", :block_name => block_name)) raise SyntaxError.new(options[:locale].t("errors.syntax.tag_never_closed".freeze, :block_name => block_name))
end end
def render_all(list, context) def render_all(list, context)
@@ -135,10 +135,10 @@ module Liquid
end end
token_output = (token.respond_to?(:render) ? token.render(context) : token) token_output = (token.respond_to?(:render) ? token.render(context) : token)
context.resource_limits[:render_length_current] += (token_output.respond_to?(:length) ? token_output.length : 1) context.increment_used_resources(:render_length_current, token_output)
if context.resource_limits_reached? if context.resource_limits_reached?
context.resource_limits[:reached] = true context.resource_limits[:reached] = true
raise MemoryError.new("Memory limits exceeded") raise MemoryError.new("Memory limits exceeded".freeze)
end end
unless token.is_a?(Block) && token.blank? unless token.is_a?(Block) && token.blank?
output << token_output output << token_output

View File

@@ -8,14 +8,14 @@ module Liquid
# #
class Condition #:nodoc: class Condition #:nodoc:
@@operators = { @@operators = {
'==' => lambda { |cond, left, right| cond.send(:equal_variables, left, right) }, '=='.freeze => lambda { |cond, left, right| cond.send(:equal_variables, left, right) },
'!=' => lambda { |cond, left, right| !cond.send(:equal_variables, left, right) }, '!='.freeze => lambda { |cond, left, right| !cond.send(:equal_variables, left, right) },
'<>' => lambda { |cond, left, right| !cond.send(:equal_variables, left, right) }, '<>'.freeze => lambda { |cond, left, right| !cond.send(:equal_variables, left, right) },
'<' => :<, '<'.freeze => :<,
'>' => :>, '>'.freeze => :>,
'>=' => :>=, '>='.freeze => :>=,
'<=' => :<=, '<='.freeze => :<=,
'contains' => lambda { |cond, left, right| left && right ? left.include?(right) : false } 'contains'.freeze => lambda { |cond, left, right| left && right ? left.include?(right) : false }
} }
def self.operators def self.operators
@@ -61,7 +61,7 @@ module Liquid
end end
def inspect def inspect
"#<Condition #{[@left, @operator, @right].compact.join(' ')}>" "#<Condition #{[@left, @operator, @right].compact.join(' '.freeze)}>"
end end
private private

View File

@@ -25,6 +25,15 @@ module Liquid
squash_instance_assigns_with_environments squash_instance_assigns_with_environments
@interrupts = [] @interrupts = []
@filters = []
end
def increment_used_resources(key, obj)
@resource_limits[key] += if obj.kind_of?(String) || obj.kind_of?(Array) || obj.kind_of?(Hash)
obj.length
else
1
end
end end
def resource_limits_reached? def resource_limits_reached?
@@ -34,7 +43,7 @@ module Liquid
end end
def strainer def strainer
@strainer ||= Strainer.create(self) @strainer ||= Strainer.create(self, @filters)
end end
# Adds filters to this context. # Adds filters to this context.
@@ -43,11 +52,20 @@ module Liquid
# for that # for that
def add_filters(filters) def add_filters(filters)
filters = [filters].flatten.compact filters = [filters].flatten.compact
filters.each do |f| filters.each do |f|
raise ArgumentError, "Expected module but got: #{f.class}" unless f.is_a?(Module) raise ArgumentError, "Expected module but got: #{f.class}" unless f.is_a?(Module)
Strainer.add_known_filter(f) Strainer.add_known_filter(f)
strainer.extend(f) end
# If strainer is already setup then there's no choice but to use a runtime
# extend call. If strainer is not yet created, we can utilize strainers
# cached class based API, which avoids busting the method cache.
if @strainer
filters.each do |f|
strainer.extend(f)
end
else
@filters.concat filters
end end
end end
@@ -85,7 +103,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" if @scopes.length > 100 raise StackLevelError, "Nesting too deep".freeze if @scopes.length > 100
end end
# Merge a hash of variables in the current local scope # Merge a hash of variables in the current local scope
@@ -133,11 +151,11 @@ module Liquid
private private
LITERALS = { LITERALS = {
nil => nil, 'nil' => nil, 'null' => nil, '' => nil, nil => nil, 'nil'.freeze => nil, 'null'.freeze => nil, ''.freeze => nil,
'true' => true, 'true'.freeze => true,
'false' => false, 'false'.freeze => false,
'blank' => :blank?, 'blank'.freeze => :blank?,
'empty' => :empty? 'empty'.freeze => :empty?
} }
# Look up variable, either resolve directly after considering the name. We can directly handle # Look up variable, either resolve directly after considering the name. We can directly handle
@@ -153,15 +171,15 @@ module Liquid
LITERALS[key] LITERALS[key]
else else
case key case key
when /^'(.*)'$/ # Single quoted strings when /\A'(.*)'\z/m # Single quoted strings
$1 $1
when /^"(.*)"$/ # Double quoted strings when /\A"(.*)"\z/m # Double quoted strings
$1 $1
when /^(-?\d+)$/ # Integer and floats when /\A(-?\d+)\z/ # Integer and floats
$1.to_i $1.to_i
when /^\((\S+)\.\.(\S+)\)$/ # Ranges when /\A\((\S+)\.\.(\S+)\)\z/ # Ranges
(resolve($1).to_i..resolve($2).to_i) (resolve($1).to_i..resolve($2).to_i)
when /^(-?\d[\d\.]+)$/ # Floats when /\A(-?\d[\d\.]+)\z/ # Floats
$1.to_f $1.to_f
else else
variable(key) variable(key)
@@ -200,7 +218,7 @@ module Liquid
# assert_equal 'tobi', @context['hash["name"]'] # assert_equal 'tobi', @context['hash["name"]']
def variable(markup) def variable(markup)
parts = markup.scan(VariableParser) parts = markup.scan(VariableParser)
square_bracketed = /^\[(.*)\]$/ square_bracketed = /\A\[(.*)\]\z/m
first_part = parts.shift first_part = parts.shift
@@ -226,7 +244,7 @@ module Liquid
# Some special cases. If the part wasn't in square brackets and # Some special cases. If the part wasn't in square brackets and
# no key with the same name was found we interpret following calls # no key with the same name was found we interpret following calls
# as commands and call them on the current object # as commands and call them on the current object
elsif !part_resolved and object.respond_to?(part) and ['size', 'first', 'last'].include?(part) elsif !part_resolved and object.respond_to?(part) and ['size'.freeze, 'first'.freeze, 'last'.freeze].include?(part)
object = object.send(part.intern).to_liquid object = object.send(part.intern).to_liquid

View File

@@ -1,9 +1,8 @@
module Liquid module Liquid
class Document < Block class Document < Block
# we don't need markup to open this block def self.parse(tokens, options={})
def initialize(tokens, options = {}) # we don't need markup to open this block
@options = options super(nil, nil, tokens, options)
parse(tokens)
end end
# There isn't a real delimiter # There isn't a real delimiter

View File

@@ -52,6 +52,10 @@ module Liquid
self self
end end
def to_s
self.class.name
end
alias :[] :invoke_drop alias :[] :invoke_drop
private private
@@ -59,13 +63,12 @@ module Liquid
# Check for method existence without invoking respond_to?, which creates symbols # Check for method existence without invoking respond_to?, which creates symbols
def self.invokable?(method_name) def self.invokable?(method_name)
unless @invokable_methods unless @invokable_methods
# Ruby 1.8 compatibility: call to_s on method names (which are strings in 1.8, but already symbols in 1.9) blacklist = Liquid::Drop.public_instance_methods + [:each]
blacklist = (Liquid::Drop.public_instance_methods + [:each]).map(&:to_s)
if include?(Enumerable) if include?(Enumerable)
blacklist += Enumerable.public_instance_methods.map(&:to_s) blacklist += Enumerable.public_instance_methods
blacklist -= [:sort, :count, :first, :min, :max, :include?].map(&:to_s) blacklist -= [:sort, :count, :first, :min, :max, :include?]
end end
whitelist = [:to_liquid] + (public_instance_methods.map(&:to_s) - blacklist.map(&:to_s)) whitelist = [:to_liquid] + (public_instance_methods - blacklist)
@invokable_methods = Set.new(whitelist.map(&:to_s)) @invokable_methods = Set.new(whitelist.map(&:to_s))
end end
@invokable_methods.include?(method_name.to_s) @invokable_methods.include?(method_name.to_s)

View File

@@ -31,11 +31,22 @@ module Liquid
# file_system.full_path("mypartial") # => "/some/path/_mypartial.liquid" # file_system.full_path("mypartial") # => "/some/path/_mypartial.liquid"
# file_system.full_path("dir/mypartial") # => "/some/path/dir/_mypartial.liquid" # file_system.full_path("dir/mypartial") # => "/some/path/dir/_mypartial.liquid"
# #
# Optionally in the second argument you can specify a custom pattern for template filenames.
# The Kernel::sprintf format specification is used.
# Default pattern is "_%s.liquid".
#
# Example:
#
# file_system = Liquid::LocalFileSystem.new("/some/path", "%s.html")
#
# file_system.full_path("index") # => "/some/path/index.html"
#
class LocalFileSystem class LocalFileSystem
attr_accessor :root attr_accessor :root
def initialize(root) def initialize(root, pattern = "_%s.liquid".freeze)
@root = root @root = root
@pattern = pattern
end end
def read_template_file(template_path, context) def read_template_file(template_path, context)
@@ -46,15 +57,15 @@ module Liquid
end end
def full_path(template_path) def full_path(template_path)
raise FileSystemError, "Illegal template name '#{template_path}'" unless template_path =~ /^[^.\/][a-zA-Z0-9_\/]+$/ raise FileSystemError, "Illegal template name '#{template_path}'" unless template_path =~ /\A[^.\/][a-zA-Z0-9_\/]+\z/
full_path = if template_path.include?('/') full_path = if template_path.include?('/'.freeze)
File.join(root, File.dirname(template_path), "_#{File.basename(template_path)}.liquid") File.join(root, File.dirname(template_path), @pattern % File.basename(template_path))
else else
File.join(root, "_#{template_path}.liquid") File.join(root, @pattern % template_path)
end end
raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path) =~ /^#{File.expand_path(root)}/ raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path) =~ /\A#{File.expand_path(root)}/
full_path full_path
end end

View File

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

View File

@@ -6,7 +6,7 @@ module Liquid
class TranslationError < StandardError class TranslationError < StandardError
end end
attr_reader :path attr_reader :path
def initialize(path = DEFAULT_LOCALE) def initialize(path = DEFAULT_LOCALE)
@@ -24,14 +24,14 @@ module Liquid
private private
def interpolate(name, vars) def interpolate(name, vars)
name.gsub(/%{(\w+)}/) { name.gsub(/%\{(\w+)\}/) {
# 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
def deep_fetch_translation(name) def deep_fetch_translation(name)
name.split('.').reduce(locale) do |level, cur| name.split('.'.freeze).reduce(locale) do |level, cur|
level[cur] or raise TranslationError, "Translation for #{name} does not exist in locale #{path}" level[cur] or raise TranslationError, "Translation for #{name} does not exist in locale #{path}"
end end
end end

View File

@@ -5,7 +5,7 @@ module Liquid
attr_reader :message attr_reader :message
def initialize(message=nil) def initialize(message=nil)
@message = message || "interrupt" @message = message || "interrupt".freeze
end end
end end

View File

@@ -2,19 +2,20 @@ require "strscan"
module Liquid module Liquid
class Lexer class Lexer
SPECIALS = { SPECIALS = {
'|' => :pipe, '|'.freeze => :pipe,
'.' => :dot, '.'.freeze => :dot,
':' => :colon, ':'.freeze => :colon,
',' => :comma, ','.freeze => :comma,
'[' => :open_square, '['.freeze => :open_square,
']' => :close_square, ']'.freeze => :close_square,
'(' => :open_round, '('.freeze => :open_round,
')' => :close_round ')'.freeze => :close_round
} }
IDENTIFIER = /[\w\-?!]+/ IDENTIFIER = /[\w\-?!]+/
SINGLE_STRING_LITERAL = /'[^\']*'/ SINGLE_STRING_LITERAL = /'[^\']*'/
DOUBLE_STRING_LITERAL = /"[^\"]*"/ DOUBLE_STRING_LITERAL = /"[^\"]*"/
NUMBER_LITERAL = /-?\d+(\.\d+)?/ NUMBER_LITERAL = /-?\d+(\.\d+)?/
DOTDOT = /\.\./
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains/ COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains/
def initialize(input) def initialize(input)
@@ -32,6 +33,7 @@ module Liquid
when t = @ss.scan(DOUBLE_STRING_LITERAL) then [:string, t] when t = @ss.scan(DOUBLE_STRING_LITERAL) then [:string, t]
when t = @ss.scan(NUMBER_LITERAL) then [:number, t] when t = @ss.scan(NUMBER_LITERAL) then [:number, t]
when t = @ss.scan(IDENTIFIER) then [:id, t] when t = @ss.scan(IDENTIFIER) then [:id, t]
when t = @ss.scan(DOTDOT) then [:dotdot, t]
else else
c = @ss.getch c = @ss.getch
if s = SPECIALS[c] if s = SPECIALS[c]

View File

@@ -53,8 +53,7 @@ module Liquid
elsif token.first == :open_round elsif token.first == :open_round
consume consume
first = expression first = expression
consume(:dot) consume(:dotdot)
consume(:dot)
last = expression last = expression
consume(:close_round) consume(:close_round)
"(#{first}..#{last})" "(#{first}..#{last})"
@@ -67,10 +66,11 @@ module Liquid
str = "" str = ""
# might be a keyword argument (identifier: expression) # might be a keyword argument (identifier: expression)
if look(:id) && look(:colon, 1) if look(:id) && look(:colon, 1)
str << consume << consume << ' ' str << consume << consume << ' '.freeze
end end
str << expression str << expression
str
end end
def variable_signature def variable_signature

View File

@@ -4,10 +4,17 @@ require 'bigdecimal'
module Liquid module Liquid
module StandardFilters module StandardFilters
HTML_ESCAPE = {
'&'.freeze => '&amp;'.freeze,
'>'.freeze => '&gt;'.freeze,
'<'.freeze => '&lt;'.freeze,
'"'.freeze => '&quot;'.freeze,
"'".freeze => '&#39;'.freeze
}
HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/
# Return the size of an array or of an string # Return the size of an array or of an string
def size(input) def size(input)
input.respond_to?(:size) ? input.size : 0 input.respond_to?(:size) ? input.size : 0
end end
@@ -31,28 +38,25 @@ module Liquid
end end
def escape_once(input) def escape_once(input)
ActionView::Helpers::TagHelper.escape_once(input) input.to_s.gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE)
rescue NameError
input
end end
alias_method :h, :escape alias_method :h, :escape
# Truncate a string down to x characters # Truncate a string down to x characters
def truncate(input, length = 50, truncate_string = "...") def truncate(input, length = 50, truncate_string = "...".freeze)
if input.nil? then return end if input.nil? then return end
l = length.to_i - truncate_string.length l = length.to_i - truncate_string.length
l = 0 if l < 0 l = 0 if l < 0
truncated = RUBY_VERSION[0,3] == "1.8" ? input.scan(/./mu)[0...l].to_s : input[0...l] input.length > length.to_i ? input[0...l] + truncate_string : input
input.length > length.to_i ? truncated + truncate_string : input
end end
def truncatewords(input, words = 15, truncate_string = "...") def truncatewords(input, words = 15, truncate_string = "...".freeze)
if input.nil? then return end if input.nil? then return end
wordlist = input.to_s.split wordlist = input.to_s.split
l = words.to_i - 1 l = words.to_i - 1
l = 0 if l < 0 l = 0 if l < 0
wordlist.length > l ? wordlist[0..l].join(" ") + truncate_string : input wordlist.length > l ? wordlist[0..l].join(" ".freeze) + truncate_string : 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.
@@ -64,17 +68,30 @@ module Liquid
input.split(pattern) input.split(pattern)
end end
def strip(input)
input.to_s.strip
end
def lstrip(input)
input.to_s.lstrip
end
def rstrip(input)
input.to_s.rstrip
end
def strip_html(input) def strip_html(input)
input.to_s.gsub(/<script.*?<\/script>/m, '').gsub(/<!--.*?-->/m, '').gsub(/<style.*?<\/style>/m, '').gsub(/<.*?>/m, '') empty = ''.freeze
input.to_s.gsub(/<script.*?<\/script>/m, empty).gsub(/<!--.*?-->/m, empty).gsub(/<style.*?<\/style>/m, empty).gsub(/<.*?>/m, empty)
end end
# Remove all newlines from the string # Remove all newlines from the string
def strip_newlines(input) def strip_newlines(input)
input.to_s.gsub(/\r?\n/, '') input.to_s.gsub(/\r?\n/, ''.freeze)
end end
# Join elements of the array with certain character between them # Join elements of the array with certain character between them
def join(input, glue = ' ') def join(input, glue = ' '.freeze)
[input].flatten.join(glue) [input].flatten.join(glue)
end end
@@ -84,7 +101,7 @@ module Liquid
ary = flatten_if_necessary(input) ary = flatten_if_necessary(input)
if property.nil? if property.nil?
ary.sort ary.sort
elsif ary.first.respond_to?('[]') and !ary.first[property].nil? elsif ary.first.respond_to?('[]'.freeze) and !ary.first[property].nil?
ary.sort {|a,b| a[property] <=> b[property] } ary.sort {|a,b| a[property] <=> b[property] }
elsif ary.first.respond_to?(property) elsif ary.first.respond_to?(property)
ary.sort {|a,b| a.send(property) <=> b.send(property) } ary.sort {|a,b| a.send(property) <=> b.send(property) }
@@ -102,7 +119,7 @@ module Liquid
flatten_if_necessary(input).map do |e| flatten_if_necessary(input).map do |e|
e = e.call if e.is_a?(Proc) e = e.call if e.is_a?(Proc)
if property == "to_liquid" if property == "to_liquid".freeze
e e
elsif e.respond_to?(:[]) elsif e.respond_to?(:[])
e[property] e[property]
@@ -111,23 +128,23 @@ module Liquid
end end
# Replace occurrences of a string with another # Replace occurrences of a string with another
def replace(input, string, replacement = '') def replace(input, string, replacement = ''.freeze)
input.to_s.gsub(string, replacement.to_s) input.to_s.gsub(string, 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 = '') def replace_first(input, string, replacement = ''.freeze)
input.to_s.sub(string, replacement.to_s) input.to_s.sub(string, replacement.to_s)
end end
# remove a substring # remove a substring
def remove(input, string) def remove(input, string)
input.to_s.gsub(string, '') input.to_s.gsub(string, ''.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, '') input.to_s.sub(string, ''.freeze)
end end
# add one string to another # add one string to another
@@ -142,7 +159,7 @@ module Liquid
# Add <br /> tags in front of all newlines in input string # Add <br /> tags in front of all newlines in input string
def newline_to_br(input) def newline_to_br(input)
input.to_s.gsub(/\n/, "<br />\n") input.to_s.gsub(/\n/, "<br />\n".freeze)
end end
# Reformat a date # Reformat a date
@@ -179,13 +196,13 @@ module Liquid
return input.to_s return input.to_s
end end
if ((input.is_a?(String) && !/^\d+$/.match(input.to_s).nil?) || input.is_a?(Integer)) && input.to_i > 0 if ((input.is_a?(String) && !/\A\d+\z/.match(input.to_s).nil?) || input.is_a?(Integer)) && input.to_i > 0
input = Time.at(input.to_i) input = Time.at(input.to_i)
end end
date = if input.is_a?(String) date = if input.is_a?(String)
case input.downcase case input.downcase
when 'now', 'today' when 'now'.freeze, 'today'.freeze
Time.now Time.now
else else
Time.parse(input) Time.parse(input)
@@ -245,12 +262,17 @@ module Liquid
apply_operation(input, operand, :%) apply_operation(input, operand, :%)
end end
def default(input, default_value = "".freeze)
is_blank = input.respond_to?(:empty?) ? input.empty? : !input
is_blank ? default_value : input
end
private private
def flatten_if_necessary(input) def flatten_if_necessary(input)
ary = if input.is_a?(Array) ary = if input.is_a?(Array)
input.flatten input.flatten
elsif input.kind_of?(Enumerable) elsif input.is_a?(Enumerable) && !input.is_a?(Hash)
input input
else else
[input].flatten [input].flatten
@@ -265,7 +287,7 @@ module Liquid
when Numeric when Numeric
obj obj
when String when String
(obj.strip =~ /^\d+\.\d+$/) ? BigDecimal.new(obj) : obj.to_i (obj.strip =~ /\A\d+\.\d+\z/) ? BigDecimal.new(obj) : obj.to_i
else else
0 0
end end

View File

@@ -11,6 +11,11 @@ module Liquid
@@filters = [] @@filters = []
@@known_filters = Set.new @@known_filters = Set.new
@@known_methods = Set.new @@known_methods = Set.new
@@strainer_class_cache = Hash.new do |hash, filters|
hash[filters] = Class.new(Strainer) do
filters.each { |f| include f }
end
end
def initialize(context) def initialize(context)
@context = context @context = context
@@ -32,10 +37,13 @@ module Liquid
end end
end end
def self.create(context) def self.strainer_class_cache
strainer = Strainer.new(context) @@strainer_class_cache
@@filters.each { |m| strainer.extend(m) } end
strainer
def self.create(context, filters = [])
filters = @@filters + filters
strainer_class_cache[filters].new(context)
end end
def invoke(method, *args) def invoke(method, *args)
@@ -44,6 +52,8 @@ module Liquid
else else
args.first args.first
end end
rescue ::ArgumentError => e
raise Liquid::ArgumentError.new(e.message)
end end
def invokable?(method) def invokable?(method)

View File

@@ -1,22 +1,22 @@
module Liquid module Liquid
class Tag class Tag
attr_accessor :nodelist, :options attr_accessor :options
attr_reader :warnings attr_reader :nodelist, :warnings
def self.new_with_options(tag_name, markup, tokens, options) class << self
# Forgive me Matz for I have sinned. I know this code is weird def parse(tag_name, markup, tokens, options)
# but it was necessary to maintain API compatibility. tag = new(tag_name, markup, options)
new_tag = self.allocate tag.parse(tokens)
new_tag.options = options tag
new_tag.send(:initialize, tag_name, markup, tokens) end
new_tag
private :new
end end
def initialize(tag_name, markup, tokens) def initialize(tag_name, markup, options)
@tag_name = tag_name @tag_name = tag_name
@markup = markup @markup = markup
@options ||= {} # needs || because might be set before initialize @options = options
parse(tokens)
end end
def parse(tokens) def parse(tokens)
@@ -27,11 +27,11 @@ module Liquid
end end
def render(context) def render(context)
'' ''.freeze
end end
def blank? def blank?
@blank || true @blank || false
end end
def parse_with_selected_parser(markup) def parse_with_selected_parser(markup)

View File

@@ -9,27 +9,29 @@ module Liquid
# {{ foo }} # {{ foo }}
# #
class Assign < Tag class Assign < Tag
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/o Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
def initialize(tag_name, markup, tokens) def initialize(tag_name, markup, options)
super
if markup =~ Syntax if markup =~ Syntax
@to = $1 @to = $1
@from = Variable.new($2) @from = Variable.new($2)
else else
raise SyntaxError.new options[:locale].t("errors.syntax.assign") raise SyntaxError.new options[:locale].t("errors.syntax.assign".freeze)
end end
super
end end
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_current] += (val.respond_to?(:length) ? val.length : 1) context.increment_used_resources(:assign_score_current, val)
'' ''.freeze
end end
def blank?
true
end
end end
Template.register_tag('assign', Assign) Template.register_tag('assign'.freeze, Assign)
end end

View File

@@ -17,5 +17,5 @@ module Liquid
end end
Template.register_tag('break', Break) Template.register_tag('break'.freeze, Break)
end end

View File

@@ -14,21 +14,20 @@ module Liquid
class Capture < Block class Capture < Block
Syntax = /(\w+)/ Syntax = /(\w+)/
def initialize(tag_name, markup, tokens) def initialize(tag_name, markup, options)
super
if markup =~ Syntax if markup =~ Syntax
@to = $1 @to = $1
else else
raise SyntaxError.new(options[:locale].t("errors.syntax.capture")) raise SyntaxError.new(options[:locale].t("errors.syntax.capture"))
end end
super
end end
def render(context) def render(context)
output = super output = super
context.scopes.last[@to] = output context.scopes.last[@to] = output
context.resource_limits[:assign_score_current] += (output.respond_to?(:length) ? output.length : 1) context.increment_used_resources(:assign_score_current, output)
'' ''.freeze
end end
def blank? def blank?
@@ -36,5 +35,5 @@ module Liquid
end end
end end
Template.register_tag('capture', Capture) Template.register_tag('capture'.freeze, Capture)
end end

View File

@@ -1,26 +1,29 @@
module Liquid module Liquid
class Case < Block class Case < Block
Syntax = /(#{QuotedFragment})/o Syntax = /(#{QuotedFragment})/o
WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/o WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om
def initialize(tag_name, markup, tokens) def initialize(tag_name, markup, options)
super
@blocks = [] @blocks = []
if markup =~ Syntax if markup =~ Syntax
@left = $1 @left = $1
else else
raise SyntaxError.new(options[:locale].t("errors.syntax.case")) raise SyntaxError.new(options[:locale].t("errors.syntax.case".freeze))
end end
end
super def nodelist
@blocks.map(&:attachment).flatten
end end
def unknown_tag(tag, markup, tokens) def unknown_tag(tag, markup, tokens)
@nodelist = [] @nodelist = []
case tag case tag
when 'when' when 'when'.freeze
record_when_condition(markup) record_when_condition(markup)
when 'else' when 'else'.freeze
record_else_condition(markup) record_else_condition(markup)
else else
super super
@@ -50,12 +53,12 @@ module Liquid
while markup while markup
# Create a new nodelist and assign it to the new block # Create a new nodelist and assign it to the new block
if not markup =~ WhenSyntax if not markup =~ WhenSyntax
raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_when")) raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_when".freeze))
end end
markup = $2 markup = $2
block = Condition.new(@left, '==', $1) block = Condition.new(@left, '=='.freeze, $1)
block.attach(@nodelist) block.attach(@nodelist)
@blocks.push(block) @blocks.push(block)
end end
@@ -63,7 +66,7 @@ module Liquid
def record_else_condition(markup) def record_else_condition(markup)
if not markup.strip.empty? if not markup.strip.empty?
raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_else")) raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_else".freeze))
end end
block = ElseCondition.new block = ElseCondition.new
@@ -72,5 +75,5 @@ module Liquid
end end
end end
Template.register_tag('case', Case) Template.register_tag('case'.freeze, Case)
end end

View File

@@ -1,7 +1,10 @@
module Liquid module Liquid
class Comment < Block class Comment < Block
def render(context) def render(context)
'' ''.freeze
end
def unknown_tag(tag, markup, tokens)
end end
def blank? def blank?
@@ -9,5 +12,5 @@ module Liquid
end end
end end
Template.register_tag('comment', Comment) Template.register_tag('comment'.freeze, Comment)
end end

View File

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

View File

@@ -12,10 +12,11 @@ module Liquid
# <div class="green"> Item five</div> # <div class="green"> Item five</div>
# #
class Cycle < Tag class Cycle < Tag
SimpleSyntax = /^#{QuotedFragment}+/o SimpleSyntax = /\A#{QuotedFragment}+/o
NamedSyntax = /^(#{QuotedFragment})\s*\:\s*(.*)/o NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om
def initialize(tag_name, markup, tokens) def initialize(tag_name, markup, options)
super
case markup case markup
when NamedSyntax when NamedSyntax
@variables = variables_from_string($2) @variables = variables_from_string($2)
@@ -24,9 +25,8 @@ module Liquid
@variables = variables_from_string(markup) @variables = variables_from_string(markup)
@name = "'#{@variables.to_s}'" @name = "'#{@variables.to_s}'"
else else
raise SyntaxError.new(options[:locale].t("errors.syntax.cycle")) raise SyntaxError.new(options[:locale].t("errors.syntax.cycle".freeze))
end end
super
end end
def render(context) def render(context)

View File

@@ -19,10 +19,9 @@ module Liquid
# Hello: -3 # Hello: -3
# #
class Decrement < Tag class Decrement < Tag
def initialize(tag_name, markup, tokens) def initialize(tag_name, markup, options)
@variable = markup.strip
super super
@variable = markup.strip
end end
def render(context) def render(context)
@@ -35,5 +34,5 @@ module Liquid
private private
end end
Template.register_tag('decrement', Decrement) Template.register_tag('decrement'.freeze, Decrement)
end end

View File

@@ -46,14 +46,22 @@ 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
def initialize(tag_name, markup, tokens) def initialize(tag_name, markup, options)
super
parse_with_selected_parser(markup) parse_with_selected_parser(markup)
@nodelist = @for_block = [] @nodelist = @for_block = []
super end
def nodelist
if @else_block
@for_block + @else_block
else
@for_block
end
end end
def unknown_tag(tag, markup, tokens) def unknown_tag(tag, markup, tokens)
return super unless tag == 'else' return super unless tag == 'else'.freeze
@nodelist = @else_block = [] @nodelist = @else_block = []
end end
@@ -66,17 +74,16 @@ module Liquid
# Maintains Ruby 1.8.7 String#each behaviour on 1.9 # Maintains Ruby 1.8.7 String#each behaviour on 1.9
return render_else(context) unless iterable?(collection) return render_else(context) unless iterable?(collection)
from = if @attributes['offset'] == 'continue' from = if @attributes['offset'.freeze] == 'continue'.freeze
context.registers[:for][@name].to_i context.registers[:for][@name].to_i
else else
context[@attributes['offset']].to_i context[@attributes['offset'.freeze]].to_i
end end
limit = context[@attributes['limit']] limit = context[@attributes['limit'.freeze]]
to = limit ? limit.to_i + from : nil to = limit ? limit.to_i + from : nil
segment = Utils.slice_collection(collection, from, to)
segment = Utils.slice_collection_using_each(collection, from, to)
return render_else(context) if segment.empty? return render_else(context) if segment.empty?
@@ -92,15 +99,16 @@ module Liquid
context.stack do context.stack do
segment.each_with_index do |item, index| segment.each_with_index do |item, index|
context[@variable_name] = item context[@variable_name] = item
context['forloop'] = { context['forloop'.freeze] = {
'name' => @name, 'name'.freeze => @name,
'length' => length, 'length'.freeze => length,
'index' => index + 1, 'index'.freeze => index + 1,
'index0' => index, 'index0'.freeze => index,
'rindex' => length - index, 'rindex'.freeze => length - index,
'rindex0' => length - index - 1, 'rindex0'.freeze => length - index - 1,
'first' => (index == 0), 'first'.freeze => (index == 0),
'last' => (index == length - 1) } 'last'.freeze => (index == length - 1)
}
result << render_all(@for_block, context) result << render_all(@for_block, context)
@@ -128,22 +136,22 @@ module Liquid
@attributes[key] = value @attributes[key] = value
end end
else else
raise SyntaxError.new(options[:locale].t("errors.syntax.for")) raise SyntaxError.new(options[:locale].t("errors.syntax.for".freeze))
end end
end end
def strict_parse(markup) def strict_parse(markup)
p = Parser.new(markup) p = Parser.new(markup)
@variable_name = p.consume(:id) @variable_name = p.consume(:id)
raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_in")) unless p.id?('in') raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_in".freeze)) unless p.id?('in'.freeze)
@collection_name = p.expression @collection_name = p.expression
@name = "#{@variable_name}-#{@collection_name}" @name = "#{@variable_name}-#{@collection_name}"
@reversed = p.id?('reversed') @reversed = p.id?('reversed'.freeze)
@attributes = {} @attributes = {}
while p.look(:id) && p.look(:colon, 1) while p.look(:id) && p.look(:colon, 1)
unless attribute = p.id?('limit') || p.id?('offset') unless attribute = p.id?('limit'.freeze) || p.id?('offset'.freeze)
raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_attribute")) raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_attribute".freeze))
end end
p.consume p.consume
val = p.expression val = p.expression
@@ -154,14 +162,14 @@ module Liquid
private private
def render_else(context) def render_else(context)
return @else_block ? [render_all(@else_block, context)] : '' return @else_block ? [render_all(@else_block, context)] : ''.freeze
end end
def iterable?(collection) def iterable?(collection)
collection.respond_to?(:each) || Utils.non_blank_string?(collection) collection.respond_to?(:each) || Utils.non_blank_string?(collection)
end end
end end
Template.register_tag('for', For) Template.register_tag('for'.freeze, For)
end end

View File

@@ -12,15 +12,20 @@ module Liquid
class If < Block class If < Block
Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o
ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o
BOOLEAN_OPERATORS = %w(and or)
def initialize(tag_name, markup, tokens) def initialize(tag_name, markup, options)
@blocks = []
push_block('if', markup)
super super
@blocks = []
push_block('if'.freeze, markup)
end
def nodelist
@blocks.map(&:attachment).flatten
end end
def unknown_tag(tag, markup, tokens) def unknown_tag(tag, markup, tokens)
if ['elsif', 'else'].include?(tag) if ['elsif'.freeze, 'else'.freeze].include?(tag)
push_block(tag, markup) push_block(tag, markup)
else else
super super
@@ -34,14 +39,14 @@ module Liquid
return render_all(block.attachment, context) return render_all(block.attachment, context)
end end
end end
'' ''.freeze
end end
end end
private private
def push_block(tag, markup) def push_block(tag, markup)
block = if tag == 'else' block = if tag == 'else'.freeze
ElseCondition.new ElseCondition.new
else else
parse_with_selected_parser(markup) parse_with_selected_parser(markup)
@@ -53,17 +58,18 @@ module Liquid
def lax_parse(markup) def lax_parse(markup)
expressions = markup.scan(ExpressionsAndOperators).reverse expressions = markup.scan(ExpressionsAndOperators).reverse
raise(SyntaxError.new(options[:locale].t("errors.syntax.if"))) unless expressions.shift =~ Syntax raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.shift =~ Syntax
condition = Condition.new($1, $2, $3) condition = Condition.new($1, $2, $3)
while not expressions.empty? while not expressions.empty?
operator = (expressions.shift).to_s.strip operator = (expressions.shift).to_s.strip
raise(SyntaxError.new(options[:locale].t("errors.syntax.if"))) unless expressions.shift.to_s =~ Syntax raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.shift.to_s =~ Syntax
new_condition = Condition.new($1, $2, $3) new_condition = Condition.new($1, $2, $3)
new_condition.send(operator.to_sym, condition) raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless BOOLEAN_OPERATORS.include?(operator)
new_condition.send(operator, condition)
condition = new_condition condition = new_condition
end end
@@ -75,7 +81,7 @@ module Liquid
condition = parse_comparison(p) condition = parse_comparison(p)
while op = (p.id?('and') || p.id?('or')) while op = (p.id?('and'.freeze) || p.id?('or'.freeze))
new_cond = parse_comparison(p) new_cond = parse_comparison(p)
new_cond.send(op, condition) new_cond.send(op, condition)
condition = new_cond condition = new_cond
@@ -96,5 +102,5 @@ module Liquid
end end
end end
Template.register_tag('if', If) Template.register_tag('if'.freeze, If)
end end

View File

@@ -10,11 +10,11 @@ module Liquid
context.registers[:ifchanged] = output context.registers[:ifchanged] = output
output output
else else
'' ''.freeze
end end
end end
end end
end end
Template.register_tag('ifchanged', Ifchanged) Template.register_tag('ifchanged'.freeze, Ifchanged)
end end

View File

@@ -17,7 +17,9 @@ module Liquid
class Include < Tag class Include < Tag
Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/o Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/o
def initialize(tag_name, markup, tokens) def initialize(tag_name, markup, options)
super
if markup =~ Syntax if markup =~ Syntax
@template_name = $1 @template_name = $1
@@ -29,10 +31,8 @@ module Liquid
end end
else else
raise SyntaxError.new(options[:locale].t("errors.syntax.include")) raise SyntaxError.new(options[:locale].t("errors.syntax.include".freeze))
end end
super
end end
def parse(tokens) def parse(tokens)
@@ -51,13 +51,14 @@ module Liquid
context[key] = context[value] context[key] = context[value]
end end
context_variable_name = @template_name[1..-2].split('/'.freeze).last
if variable.is_a?(Array) if variable.is_a?(Array)
variable.collect do |var| variable.collect do |var|
context[@template_name[1..-2]] = var context[context_variable_name] = var
partial.render(context) partial.render(context)
end end
else else
context[@template_name[1..-2]] = variable context[context_variable_name] = variable
partial.render(context) partial.render(context)
end end
end end
@@ -93,5 +94,5 @@ module Liquid
end end
end end
Template.register_tag('include', Include) Template.register_tag('include'.freeze, Include)
end end

View File

@@ -15,9 +15,9 @@ module Liquid
# Hello: 2 # Hello: 2
# #
class Increment < Tag class Increment < Tag
def initialize(tag_name, markup, tokens) def initialize(tag_name, markup, options)
@variable = markup.strip
super super
@variable = markup.strip
end end
def render(context) def render(context)
@@ -31,5 +31,5 @@ module Liquid
end end
end end
Template.register_tag('increment', Increment) Template.register_tag('increment'.freeze, Increment)
end end

View File

@@ -1,13 +1,13 @@
module Liquid module Liquid
class Raw < Block class Raw < Block
FullTokenPossiblyInvalid = /^(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}$/o FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
def parse(tokens) def parse(tokens)
@nodelist ||= [] @nodelist ||= []
@nodelist.clear @nodelist.clear
while token = tokens.shift while token = tokens.shift
if token =~ FullTokenPossiblyInvalid if token =~ FullTokenPossiblyInvalid
@nodelist << $1 if $1 != "" @nodelist << $1 if $1 != "".freeze
if block_delimiter == $2 if block_delimiter == $2
end_tag end_tag
return return
@@ -18,5 +18,5 @@ module Liquid
end end
end end
Template.register_tag('raw', Raw) Template.register_tag('raw'.freeze, Raw)
end end

View File

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

View File

@@ -1,7 +1,6 @@
require File.dirname(__FILE__) + '/if' require File.dirname(__FILE__) + '/if'
module Liquid module Liquid
# Unless is a conditional just like 'if' but works on the inverse logic. # Unless is a conditional just like 'if' but works on the inverse logic.
# #
# {% unless x < 0 %} x is greater than zero {% end %} # {% unless x < 0 %} x is greater than zero {% end %}
@@ -23,11 +22,10 @@ module Liquid
end end
end end
'' ''.freeze
end end
end end
end end
Template.register_tag('unless'.freeze, Unless)
Template.register_tag('unless', Unless)
end end

View File

@@ -72,7 +72,7 @@ module Liquid
# Parse source code. # Parse source code.
# Returns self for easy chaining # Returns self for easy chaining
def parse(source, options = {}) def parse(source, options = {})
@root = Document.new(tokenize(source), DEFAULT_OPTIONS.merge(options)) @root = Document.parse(tokenize(source), DEFAULT_OPTIONS.merge(options))
@warnings = nil @warnings = nil
self self
end end
@@ -110,7 +110,7 @@ module Liquid
# filters and tags and might be useful to integrate liquid more with its host application # filters and tags and might be useful to integrate liquid more with its host application
# #
def render(*args) def render(*args)
return '' if @root.nil? return ''.freeze if @root.nil?
context = case args.first context = case args.first
when Liquid::Context when Liquid::Context
@@ -162,16 +162,9 @@ 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) source = source.source if source.respond_to?(:source)
return [] if source.to_s.empty? Tokenizer.new(source.to_s)
tokens = 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
end end

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

@@ -0,0 +1,20 @@
module Liquid
class Tokenizer
VariableIncompleteEnd = /\}\}?/
AnyStartingTag = /\{\{|\{\%/
PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om
def initialize(source)
@tokens = source.split(TemplateParser)
# removes the rogue empty element at the beginning of the array
@tokens.shift if @tokens[0] && @tokens[0].empty?
end
def next
@tokens.shift
end
alias_method :shift, :next
end
end

View File

@@ -1,5 +1,18 @@
module Liquid module Liquid
module Utils module Utils
def self.slice_collection(collection, from, to)
if (from != 0 || to != nil) && collection.respond_to?(:load_slice)
collection.load_slice(from, to)
else
slice_collection_using_each(collection, from, to)
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
@@ -22,9 +35,5 @@ module Liquid
segments segments
end end
def self.non_blank_string?(collection)
collection.is_a?(String) && collection != ''
end
end end
end end

View File

@@ -12,14 +12,13 @@ module Liquid
# #
class Variable class Variable
FilterParser = /(?:#{FilterSeparator}|(?:\s*(?:#{QuotedFragment}|#{ArgumentSeparator})\s*)+)/o FilterParser = /(?:#{FilterSeparator}|(?:\s*(?:#{QuotedFragment}|#{ArgumentSeparator})\s*)+)/o
EasyParse = /^ *(\w+(?:\.\w+)*) *$/ EasyParse = /\A *(\w+(?:\.\w+)*) *\z/
attr_accessor :filters, :name, :warnings attr_accessor :filters, :name, :warnings
def initialize(markup, options = {}) def initialize(markup, options = {})
@markup = markup @markup = markup
@name = nil @name = nil
@options = options || {} @options = options || {}
case @options[:error_mode] || Template.error_mode case @options[:error_mode] || Template.error_mode
when :strict then strict_parse(markup) when :strict then strict_parse(markup)
@@ -37,9 +36,9 @@ module Liquid
def lax_parse(markup) def lax_parse(markup)
@filters = [] @filters = []
if match = markup.match(/\s*(#{QuotedFragment})(.*)/o) if match = markup.match(/\s*(#{QuotedFragment})(.*)/om)
@name = match[1] @name = match[1]
if match[2].match(/#{FilterSeparator}\s*(.*)/o) if match[2].match(/#{FilterSeparator}\s*(.*)/om)
filters = Regexp.last_match(1).scan(FilterParser) filters = Regexp.last_match(1).scan(FilterParser)
filters.each do |f| filters.each do |f|
if matches = f.match(/\s*(\w+)/) if matches = f.match(/\s*(\w+)/)
@@ -63,7 +62,7 @@ module Liquid
@filters = [] @filters = []
p = Parser.new(markup) p = Parser.new(markup)
# Could be just filters with no input # Could be just filters with no input
@name = p.look(:pipe) ? '' : p.expression @name = p.look(:pipe) ? ''.freeze : p.expression
while p.consume?(:pipe) while p.consume?(:pipe)
filtername = p.consume(:id) filtername = p.consume(:id)
filterargs = p.consume?(:colon) ? parse_filterargs(p) : [] filterargs = p.consume?(:colon) ? parse_filterargs(p) : []
@@ -86,7 +85,7 @@ module Liquid
end end
def render(context) def render(context)
return '' if @name.nil? return ''.freeze if @name.nil?
@filters.inject(context[@name]) do |output, filter| @filters.inject(context[@name]) do |output, filter|
filterargs = [] filterargs = []
keyword_args = {} keyword_args = {}

View File

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

View File

@@ -18,9 +18,17 @@ Gem::Specification.new do |s|
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,ext}/**/*") + %w(MIT-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 'activesupport'
if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'ruby'
s.extensions = ['ext/liquid/extconf.rb']
s.add_development_dependency 'rake-compiler'
s.add_development_dependency 'stackprof' if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.1.0")
end
end end

View File

@@ -1,19 +1,13 @@
require 'rubygems' require 'stackprof' rescue fail("install stackprof extension/gem")
require 'ruby-prof' rescue fail("install ruby-prof extension/gem")
require File.dirname(__FILE__) + '/theme_runner' require File.dirname(__FILE__) + '/theme_runner'
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new profiler = ThemeRunner.new
profiler.run
puts 'Running profiler...' results = StackProf.run(mode: :cpu) do
100.times do
results = profiler.run_profile profiler.run
end
puts 'Success'
puts
[RubyProf::FlatPrinter, RubyProf::GraphHtmlPrinter, RubyProf::CallTreePrinter, RubyProf::DotPrinter].each do |klass|
filename = (ENV['TMP'] || '/tmp') + (klass.name.include?('Html') ? "/liquid.#{klass.name.downcase}.html" : "/callgrind.liquid.#{klass.name.downcase}.txt")
filename.gsub!(/:+/, '_')
File.open(filename, "w+") { |fp| klass.new(results).print(fp, :print_file => true, :min_percent => 3) }
$stderr.puts "wrote #{klass.name} output to #{filename}"
end end
StackProf::Report.new(results).print_text(false, 20)
File.write(ENV['FILENAME'], Marshal.dump(results)) if ENV['FILENAME']

View File

@@ -1,15 +1,15 @@
class CommentForm < Liquid::Block class CommentForm < Liquid::Block
Syntax = /(#{Liquid::VariableSignature}+)/ Syntax = /(#{Liquid::VariableSignature}+)/
def initialize(tag_name, markup, tokens) def initialize(tag_name, markup, options)
super
if markup =~ Syntax if markup =~ Syntax
@variable_name = $1 @variable_name = $1
@attributes = {} @attributes = {}
else else
raise SyntaxError.new("Syntax Error in 'comment_form' - Valid syntax: comment_form [article]") raise SyntaxError.new("Syntax Error in 'comment_form' - Valid syntax: comment_form [article]")
end end
super
end end
def render(context) def render(context)

View File

@@ -1,7 +1,9 @@
class Paginate < Liquid::Block class Paginate < Liquid::Block
Syntax = /(#{Liquid::QuotedFragment})\s*(by\s*(\d+))?/ Syntax = /(#{Liquid::QuotedFragment})\s*(by\s*(\d+))?/
def initialize(tag_name, markup, tokens) def initialize(tag_name, markup, options)
super
@nodelist = [] @nodelist = []
if markup =~ Syntax if markup =~ Syntax
@@ -19,8 +21,6 @@ class Paginate < Liquid::Block
else else
raise SyntaxError.new("Syntax Error in tag 'paginate' - Valid syntax: paginate [collection] by number") raise SyntaxError.new("Syntax Error in tag 'paginate' - Valid syntax: paginate [collection] by number")
end end
super
end end
def render(context) def render(context)

View File

@@ -45,16 +45,16 @@ module ShopFilter
end end
def url_for_vendor(vendor_title) def url_for_vendor(vendor_title)
"/collections/#{vendor_title.to_handle}" "/collections/#{to_handle(vendor_title)}"
end end
def url_for_type(type_title) def url_for_type(type_title)
"/collections/#{type_title.to_handle}" "/collections/#{to_handle(type_title)}"
end end
def product_img_url(url, style = 'small') def product_img_url(url, style = 'small')
unless url =~ /^products\/([\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
@@ -95,4 +95,16 @@ module ShopFilter
input == 1 ? singular : plural input == 1 ? singular : plural
end end
private
def to_handle(str)
result = str.dup
result.downcase!
result.delete!("'\"()[]")
result.gsub!(/\W+/, '-')
result.gsub!(/-+\z/, '') if result[-1] == '-'
result.gsub!(/\A-+/, '') if result[0] == '-'
result
end
end end

View File

@@ -8,6 +8,7 @@
require 'rubygems' require 'rubygems'
require 'active_support' require 'active_support'
require 'active_support/json'
require 'yaml' require 'yaml'
require 'digest/md5' require 'digest/md5'
require File.dirname(__FILE__) + '/shopify/liquid' require File.dirname(__FILE__) + '/shopify/liquid'
@@ -64,52 +65,17 @@ class ThemeRunner
end end
def run_profile
RubyProf.measure_mode = RubyProf::WALL_TIME
# Dup assigns because will make some changes to them
assigns = Database.tables.dup
@tests.each do |liquid, layout, template_name|
# Compute page_tempalte outside of profiler run, uninteresting to profiler
html = nil
page_template = File.basename(template_name, File.extname(template_name))
unless @started
RubyProf.start
RubyProf.pause
@started = true
end
html = nil
RubyProf.resume
html = compile_and_render(liquid, layout, assigns, page_template, template_name)
RubyProf.pause
# return the result and the MD5 of the content, this can be used to detect regressions between liquid version
$stdout.puts "* rendered template %s, content: %s" % [template_name, Digest::MD5.hexdigest(html)]
# Uncomment to dump html files to /tmp so that you can inspect for errors
# File.open("/tmp/#{File.basename(template_name)}.html", "w+") { |fp| fp <<html}
end
RubyProf.stop
end
def compile_and_render(template, layout, assigns, page_template, template_file) def compile_and_render(template, layout, assigns, 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))
content_for_layout = tmpl.parse(template).render(assigns) content_for_layout = tmpl.parse(template).render!(assigns)
if layout if layout
assigns['content_for_layout'] = content_for_layout assigns['content_for_layout'] = content_for_layout
tmpl.parse(layout).render(assigns) tmpl.parse(layout).render!(assigns)
else else
content_for_layout content_for_layout
end end

View File

@@ -1,5 +1,13 @@
require 'test_helper' require 'test_helper'
class FoobarTag < Liquid::Tag
def render(*args)
" "
end
Liquid::Template.register_tag('foobar', FoobarTag)
end
class BlankTestFileSystem class BlankTestFileSystem
def read_template_file(template_path, context) def read_template_file(template_path, context)
template_path template_path
@@ -22,6 +30,10 @@ class BlankTest < Test::Unit::TestCase
wrap_in_for(body) + wrap_in_if(body) wrap_in_for(body) + wrap_in_if(body)
end end
def test_new_tags_are_not_blank_by_default
assert_template_result(" "*N, wrap_in_for("{% foobar %}"))
end
def test_loops_are_blank def test_loops_are_blank
assert_template_result("", wrap_in_for(" ")) assert_template_result("", wrap_in_for(" "))
end end
@@ -81,9 +93,9 @@ class BlankTest < Test::Unit::TestCase
def test_include_is_blank def test_include_is_blank
Liquid::Template.file_system = BlankTestFileSystem.new Liquid::Template.file_system = BlankTestFileSystem.new
assert_equal "foobar"*(N+1), Template.parse(wrap("{% include 'foobar' %}")).render() assert_template_result "foobar"*(N+1), wrap("{% include 'foobar' %}")
assert_equal " foobar "*(N+1), Template.parse(wrap("{% include ' foobar ' %}")).render() assert_template_result " foobar "*(N+1), wrap("{% include ' foobar ' %}")
assert_equal " ", Template.parse(" {% include ' ' %} ").render() assert_template_result " "*(N+1), wrap(" {% include ' ' %} ")
end end
def test_case_is_blank def test_case_is_blank

View File

@@ -19,7 +19,7 @@ class CaptureTest < Test::Unit::TestCase
{{var}} {{var}}
END_TEMPLATE END_TEMPLATE
template = Template.parse(template_source) template = Template.parse(template_source)
rendered = template.render rendered = template.render!
assert_equal "test-string", rendered.gsub(/\s/, '') assert_equal "test-string", rendered.gsub(/\s/, '')
end end
@@ -34,7 +34,7 @@ class CaptureTest < Test::Unit::TestCase
{{ first }}-{{ second }} {{ first }}-{{ second }}
END_TEMPLATE END_TEMPLATE
template = Template.parse(template_source) template = Template.parse(template_source)
rendered = template.render rendered = template.render!
assert_equal "3-3", rendered.gsub(/\s/, '') assert_equal "3-3", rendered.gsub(/\s/, '')
end end
end # CaptureTest end # CaptureTest

View File

@@ -176,8 +176,8 @@ class ContextTest < Test::Unit::TestCase
end end
Template.register_filter(global) Template.register_filter(global)
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
def test_only_intended_filters_make_it_there def test_only_intended_filters_make_it_there

View File

@@ -106,135 +106,140 @@ class DropsTest < Test::Unit::TestCase
def test_product_drop def test_product_drop
assert_nothing_raised do assert_nothing_raised do
tpl = Liquid::Template.parse( ' ' ) tpl = Liquid::Template.parse( ' ' )
tpl.render('product' => ProductDrop.new) tpl.render!('product' => ProductDrop.new)
end end
end end
def test_drop_does_only_respond_to_whitelisted_methods def test_drop_does_only_respond_to_whitelisted_methods
assert_equal "", Liquid::Template.parse("{{ product.inspect }}").render('product' => ProductDrop.new) assert_equal "", Liquid::Template.parse("{{ product.inspect }}").render!('product' => ProductDrop.new)
assert_equal "", Liquid::Template.parse("{{ product.pretty_inspect }}").render('product' => ProductDrop.new) assert_equal "", Liquid::Template.parse("{{ product.pretty_inspect }}").render!('product' => ProductDrop.new)
assert_equal "", Liquid::Template.parse("{{ product.whatever }}").render('product' => ProductDrop.new) assert_equal "", Liquid::Template.parse("{{ product.whatever }}").render!('product' => ProductDrop.new)
assert_equal "", Liquid::Template.parse('{{ product | map: "inspect" }}').render('product' => ProductDrop.new) assert_equal "", Liquid::Template.parse('{{ product | map: "inspect" }}').render!('product' => ProductDrop.new)
assert_equal "", Liquid::Template.parse('{{ product | map: "pretty_inspect" }}').render('product' => ProductDrop.new) assert_equal "", Liquid::Template.parse('{{ product | map: "pretty_inspect" }}').render!('product' => ProductDrop.new)
assert_equal "", Liquid::Template.parse('{{ product | map: "whatever" }}').render('product' => ProductDrop.new) assert_equal "", Liquid::Template.parse('{{ product | map: "whatever" }}').render!('product' => ProductDrop.new)
end end
def test_drops_respond_to_to_liquid def test_drops_respond_to_to_liquid
assert_equal "text1", Liquid::Template.parse("{{ product.to_liquid.texts.text }}").render('product' => ProductDrop.new) assert_equal "text1", Liquid::Template.parse("{{ product.to_liquid.texts.text }}").render!('product' => ProductDrop.new)
assert_equal "text1", Liquid::Template.parse('{{ product | map: "to_liquid" | map: "texts" | map: "text" }}').render('product' => ProductDrop.new) assert_equal "text1", Liquid::Template.parse('{{ product | map: "to_liquid" | map: "texts" | map: "text" }}').render!('product' => ProductDrop.new)
end end
def test_text_drop def test_text_drop
output = Liquid::Template.parse( ' {{ product.texts.text }} ' ).render('product' => ProductDrop.new) output = Liquid::Template.parse( ' {{ product.texts.text }} ' ).render!('product' => ProductDrop.new)
assert_equal ' text1 ', output assert_equal ' text1 ', output
end end
def test_unknown_method def test_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 ' method: unknown ', output
end end
def test_integer_argument_drop def test_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 ' method: 8 ', output
end end
def test_text_array_drop def test_text_array_drop
output = Liquid::Template.parse( '{% for text in product.texts.array %} {{text}} {% endfor %}' ).render('product' => ProductDrop.new) output = Liquid::Template.parse( '{% for text in product.texts.array %} {{text}} {% endfor %}' ).render!('product' => ProductDrop.new)
assert_equal ' text1 text2 ', output assert_equal ' text1 text2 ', output
end end
def test_context_drop def test_context_drop
output = Liquid::Template.parse( ' {{ context.bar }} ' ).render('context' => ContextDrop.new, 'bar' => "carrot") output = Liquid::Template.parse( ' {{ context.bar }} ' ).render!('context' => ContextDrop.new, 'bar' => "carrot")
assert_equal ' carrot ', output assert_equal ' carrot ', output
end end
def test_nested_context_drop def test_nested_context_drop
output = Liquid::Template.parse( ' {{ product.context.foo }} ' ).render('product' => ProductDrop.new, 'foo' => "monkey") output = Liquid::Template.parse( ' {{ product.context.foo }} ' ).render!('product' => ProductDrop.new, 'foo' => "monkey")
assert_equal ' monkey ', output assert_equal ' monkey ', output
end end
def test_protected def test_protected
output = Liquid::Template.parse( ' {{ product.callmenot }} ' ).render('product' => ProductDrop.new) output = Liquid::Template.parse( ' {{ product.callmenot }} ' ).render!('product' => ProductDrop.new)
assert_equal ' ', output assert_equal ' ', output
end end
def test_object_methods_not_allowed def test_object_methods_not_allowed
[:dup, :clone, :singleton_class, :eval, :class_eval, :inspect].each do |method| [:dup, :clone, :singleton_class, :eval, :class_eval, :inspect].each do |method|
output = Liquid::Template.parse(" {{ product.#{method} }} ").render('product' => ProductDrop.new) output = Liquid::Template.parse(" {{ product.#{method} }} ").render!('product' => ProductDrop.new)
assert_equal ' ', output assert_equal ' ', output
end end
end end
def test_scope def test_scope
assert_equal '1', Liquid::Template.parse( '{{ context.scopes }}' ).render('context' => ContextDrop.new) assert_equal '1', Liquid::Template.parse( '{{ context.scopes }}' ).render!('context' => ContextDrop.new)
assert_equal '2', Liquid::Template.parse( '{%for i in dummy%}{{ context.scopes }}{%endfor%}' ).render('context' => ContextDrop.new, 'dummy' => [1]) assert_equal '2', Liquid::Template.parse( '{%for i in dummy%}{{ context.scopes }}{%endfor%}' ).render!('context' => ContextDrop.new, 'dummy' => [1])
assert_equal '3', Liquid::Template.parse( '{%for i in dummy%}{%for i in dummy%}{{ context.scopes }}{%endfor%}{%endfor%}' ).render('context' => ContextDrop.new, 'dummy' => [1]) assert_equal '3', Liquid::Template.parse( '{%for i in dummy%}{%for i in dummy%}{{ context.scopes }}{%endfor%}{%endfor%}' ).render!('context' => ContextDrop.new, 'dummy' => [1])
end end
def test_scope_though_proc def test_scope_though_proc
assert_equal '1', Liquid::Template.parse( '{{ s }}' ).render('context' => ContextDrop.new, 's' => Proc.new{|c| c['context.scopes'] }) assert_equal '1', Liquid::Template.parse( '{{ s }}' ).render!('context' => ContextDrop.new, 's' => Proc.new{|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.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.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.new{|c| c['context.scopes'] }, 'dummy' => [1])
end end
def test_scope_with_assigns def test_scope_with_assigns
assert_equal 'variable', Liquid::Template.parse( '{% assign a = "variable"%}{{a}}' ).render('context' => ContextDrop.new) assert_equal 'variable', Liquid::Template.parse( '{% assign a = "variable"%}{{a}}' ).render!('context' => ContextDrop.new)
assert_equal 'variable', Liquid::Template.parse( '{% assign a = "variable"%}{%for i in dummy%}{{a}}{%endfor%}' ).render('context' => ContextDrop.new, 'dummy' => [1]) assert_equal 'variable', Liquid::Template.parse( '{% assign a = "variable"%}{%for i in dummy%}{{a}}{%endfor%}' ).render!('context' => ContextDrop.new, 'dummy' => [1])
assert_equal 'test', Liquid::Template.parse( '{% assign header_gif = "test"%}{{header_gif}}' ).render('context' => ContextDrop.new) assert_equal 'test', Liquid::Template.parse( '{% assign header_gif = "test"%}{{header_gif}}' ).render!('context' => ContextDrop.new)
assert_equal 'test', Liquid::Template.parse( "{% assign header_gif = 'test'%}{{header_gif}}" ).render('context' => ContextDrop.new) assert_equal 'test', Liquid::Template.parse( "{% assign header_gif = 'test'%}{{header_gif}}" ).render!('context' => ContextDrop.new)
end end
def test_scope_from_tags def test_scope_from_tags
assert_equal '1', Liquid::Template.parse( '{% for i in context.scopes_as_array %}{{i}}{% endfor %}' ).render('context' => ContextDrop.new, 'dummy' => [1]) assert_equal '1', Liquid::Template.parse( '{% for i in context.scopes_as_array %}{{i}}{% endfor %}' ).render!('context' => ContextDrop.new, 'dummy' => [1])
assert_equal '12', Liquid::Template.parse( '{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}' ).render('context' => ContextDrop.new, 'dummy' => [1]) assert_equal '12', Liquid::Template.parse( '{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}' ).render!('context' => ContextDrop.new, 'dummy' => [1])
assert_equal '123', Liquid::Template.parse( '{%for a in dummy%}{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}{% endfor %}' ).render('context' => ContextDrop.new, 'dummy' => [1]) assert_equal '123', Liquid::Template.parse( '{%for a in dummy%}{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}{% endfor %}' ).render!('context' => ContextDrop.new, 'dummy' => [1])
end end
def test_access_context_from_drop def test_access_context_from_drop
assert_equal '123', Liquid::Template.parse( '{%for a in dummy%}{{ context.loop_pos }}{% endfor %}' ).render('context' => ContextDrop.new, 'dummy' => [1,2,3]) assert_equal '123', Liquid::Template.parse( '{%for a in dummy%}{{ context.loop_pos }}{% endfor %}' ).render!('context' => ContextDrop.new, 'dummy' => [1,2,3])
end end
def test_enumerable_drop def test_enumerable_drop
assert_equal '123', Liquid::Template.parse( '{% for c in collection %}{{c}}{% endfor %}').render('collection' => EnumerableDrop.new) assert_equal '123', Liquid::Template.parse( '{% for c in collection %}{{c}}{% endfor %}').render!('collection' => EnumerableDrop.new)
end end
def test_enumerable_drop_size def test_enumerable_drop_size
assert_equal '3', Liquid::Template.parse( '{{collection.size}}').render('collection' => EnumerableDrop.new) assert_equal '3', Liquid::Template.parse( '{{collection.size}}').render!('collection' => EnumerableDrop.new)
end end
def test_enumerable_drop_will_invoke_before_method_for_clashing_method_names def test_enumerable_drop_will_invoke_before_method_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)
assert_equal method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render('collection' => RealEnumerableDrop.new) assert_equal method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new)
assert_equal method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render('collection' => RealEnumerableDrop.new) assert_equal method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new)
end end
end end
def test_some_enumerable_methods_still_get_invoked def test_some_enumerable_methods_still_get_invoked
[ :count, :max ].each do |method| [ :count, :max ].each do |method|
assert_equal "3", Liquid::Template.parse("{{collection.#{method}}}").render('collection' => RealEnumerableDrop.new) assert_equal "3", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new)
assert_equal "3", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render('collection' => RealEnumerableDrop.new) assert_equal "3", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new)
assert_equal "3", Liquid::Template.parse("{{collection.#{method}}}").render('collection' => EnumerableDrop.new) assert_equal "3", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new)
assert_equal "3", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render('collection' => EnumerableDrop.new) assert_equal "3", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new)
end end
assert_equal "yes", Liquid::Template.parse("{% if collection contains 3 %}yes{% endif %}").render('collection' => RealEnumerableDrop.new) assert_equal "yes", Liquid::Template.parse("{% if collection contains 3 %}yes{% endif %}").render!('collection' => RealEnumerableDrop.new)
[ :min, :first ].each do |method| [ :min, :first ].each do |method|
assert_equal "1", Liquid::Template.parse("{{collection.#{method}}}").render('collection' => RealEnumerableDrop.new) assert_equal "1", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new)
assert_equal "1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render('collection' => RealEnumerableDrop.new) assert_equal "1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new)
assert_equal "1", Liquid::Template.parse("{{collection.#{method}}}").render('collection' => EnumerableDrop.new) assert_equal "1", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new)
assert_equal "1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render('collection' => EnumerableDrop.new) assert_equal "1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new)
end end
end end
def test_empty_string_value_access def test_empty_string_value_access
assert_equal '', Liquid::Template.parse('{{ product[value] }}').render('product' => ProductDrop.new, 'value' => '') assert_equal '', Liquid::Template.parse('{{ product[value] }}').render!('product' => ProductDrop.new, 'value' => '')
end end
def test_nil_value_access def test_nil_value_access
assert_equal '', Liquid::Template.parse('{{ product[value] }}').render('product' => ProductDrop.new, 'value' => nil) assert_equal '', Liquid::Template.parse('{{ product[value] }}').render!('product' => ProductDrop.new, 'value' => nil)
end
def test_default_to_s_on_drops
assert_equal 'ProductDrop', Liquid::Template.parse("{{ product }}").render!('product' => ProductDrop.new)
assert_equal 'EnumerableDrop', Liquid::Template.parse('{{ collection }}').render!('collection' => EnumerableDrop.new)
end end
end # DropsTest end # DropsTest

View File

@@ -26,4 +26,10 @@ class FileSystemTest < Test::Unit::TestCase
file_system.full_path("/etc/passwd") file_system.full_path("/etc/passwd")
end end
end end
def test_custom_template_filename_patterns
file_system = Liquid::LocalFileSystem.new("/some/path", "%s.html")
assert_equal "/some/path/mypartial.html" , file_system.full_path("mypartial")
assert_equal "/some/path/dir/mypartial.html", file_system.full_path("dir/mypartial")
end
end # FileSystemTest end # FileSystemTest

View File

@@ -113,13 +113,13 @@ class FiltersInTemplate < Test::Unit::TestCase
def test_local_global def test_local_global
Template.register_filter(MoneyFilter) Template.register_filter(MoneyFilter)
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
def test_local_filter_with_deprecated_syntax def test_local_filter_with_deprecated_syntax
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render(nil, CanadianMoneyFilter) assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, CanadianMoneyFilter)
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render(nil, [CanadianMoneyFilter]) assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, [CanadianMoneyFilter])
end end
end # FiltersTest end # FiltersTest

View File

@@ -77,11 +77,11 @@ class ModuleExTest < Test::Unit::TestCase
end end
def test_should_use_regular_objects_as_drops def test_should_use_regular_objects_as_drops
assert_equal 'allowedA', Liquid::Template.parse("{{ a.allowedA }}").render('a'=>@a) assert_template_result 'allowedA', "{{ a.allowedA }}", 'a'=>@a
assert_equal 'allowedB', Liquid::Template.parse("{{ a.chainedB.allowedB }}").render('a'=>@a) assert_template_result 'allowedB', "{{ a.chainedB.allowedB }}", 'a'=>@a
assert_equal 'allowedC', Liquid::Template.parse("{{ a.chainedB.chainedC.allowedC }}").render('a'=>@a) assert_template_result 'allowedC', "{{ a.chainedB.chainedC.allowedC }}", 'a'=>@a
assert_equal 'another_allowedC', Liquid::Template.parse("{{ a.chainedB.chainedC.another_allowedC }}").render('a'=>@a) assert_template_result 'another_allowedC', "{{ a.chainedB.chainedC.another_allowedC }}", 'a'=>@a
assert_equal '', Liquid::Template.parse("{{ a.restricted }}").render('a'=>@a) assert_template_result '', "{{ a.restricted }}", 'a'=>@a
assert_equal '', Liquid::Template.parse("{{ a.unknown }}").render('a'=>@a) assert_template_result '', "{{ a.unknown }}", 'a'=>@a
end end
end # ModuleExTest end # ModuleExTest

View File

@@ -41,76 +41,76 @@ class OutputTest < Test::Unit::TestCase
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
text = %| {{car.bmw}} {{car.gm}} {{car.bmw}} | text = %| {{car.bmw}} {{car.gm}} {{car.bmw}} |
expected = %| good bad good | 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_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

@@ -56,6 +56,14 @@ class ParserTest < Test::Unit::TestCase
assert_equal '"wut"', p.expression assert_equal '"wut"', p.expression
end end
def test_ranges
p = Parser.new("(5..7) (1.5..9.6) (young..old) (hi[5].wat..old)")
assert_equal '(5..7)', p.expression
assert_equal '(1.5..9.6)', p.expression
assert_equal '(young..old)', p.expression
assert_equal '(hi[5].wat..old)', p.expression
end
def test_arguments def test_arguments
p = Parser.new("filter: hi.there[5], keyarg: 7") p = Parser.new("filter: hi.there[5], keyarg: 7")
assert_equal 'filter', p.consume(:id) assert_equal 'filter', p.consume(:id)

View File

@@ -7,7 +7,7 @@ class ParsingQuirksTest < Test::Unit::TestCase
text = %| div { font-weight: bold; } | text = %| div { font-weight: bold; } |
template = Template.parse(text) template = Template.parse(text)
assert_equal text, template.render assert_equal text, template.render!
assert_equal [String], template.root.nodelist.collect {|i| i.class} assert_equal [String], template.root.nodelist.collect {|i| i.class}
end end

View File

@@ -21,11 +21,11 @@ class RegexpTest < Test::Unit::TestCase
assert_equal ['<style', 'class="hello">', '</style>'], %|<style class="hello">' </style>|.scan(QuotedFragment) assert_equal ['<style', 'class="hello">', '</style>'], %|<style class="hello">' </style>|.scan(QuotedFragment)
end end
def test_quoted_words def test_double_quoted_words
assert_equal ['arg1', 'arg2', '"arg 3"'], 'arg1 arg2 "arg 3"'.scan(QuotedFragment) assert_equal ['arg1', 'arg2', '"arg 3"'], 'arg1 arg2 "arg 3"'.scan(QuotedFragment)
end end
def test_quoted_words def test_single_quoted_words
assert_equal ['arg1', 'arg2', "'arg 3'"], 'arg1 arg2 \'arg 3\''.scan(QuotedFragment) assert_equal ['arg1', 'arg2', "'arg 3'"], 'arg1 arg2 \'arg 3\''.scan(QuotedFragment)
end end

View File

@@ -13,14 +13,14 @@ class SecurityTest < Test::Unit::TestCase
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
@@ -28,7 +28,7 @@ class SecurityTest < Test::Unit::TestCase
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
@@ -36,7 +36,7 @@ class SecurityTest < Test::Unit::TestCase
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
@@ -47,7 +47,7 @@ class SecurityTest < Test::Unit::TestCase
template = Template.parse(test) template = Template.parse(test)
assert_equal [], (Symbol.all_symbols - current_symbols) assert_equal [], (Symbol.all_symbols - current_symbols)
template.render template.render!
assert_equal [], (Symbol.all_symbols - current_symbols) assert_equal [], (Symbol.all_symbols - current_symbols)
end end

View File

@@ -62,11 +62,6 @@ class StandardFiltersTest < Test::Unit::TestCase
assert_equal '', @filters.upcase(nil) assert_equal '', @filters.upcase(nil)
end end
def test_upcase
assert_equal 'TESTING', @filters.upcase("Testing")
assert_equal '', @filters.upcase(nil)
end
def test_truncate def test_truncate
assert_equal '1234...', @filters.truncate('1234567890', 7) assert_equal '1234...', @filters.truncate('1234567890', 7)
assert_equal '1234567890', @filters.truncate('1234567890', 20) assert_equal '1234567890', @filters.truncate('1234567890', 20)
@@ -75,7 +70,7 @@ class StandardFiltersTest < Test::Unit::TestCase
assert_equal "测试...", @filters.truncate("测试测试测试测试", 5) assert_equal "测试...", @filters.truncate("测试测试测试测试", 5)
end end
def test_strip 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', '~')
@@ -89,7 +84,7 @@ class StandardFiltersTest < Test::Unit::TestCase
end end
def test_escape_once def test_escape_once
assert_equal '&lt;strong&gt;', @filters.escape_once(@filters.escape('<strong>')) assert_equal '&lt;strong&gt;Hulk&lt;/strong&gt;', @filters.escape_once('&lt;strong&gt;Hulk</strong>')
end end
def test_truncatewords def test_truncatewords
@@ -131,33 +126,38 @@ class StandardFiltersTest < Test::Unit::TestCase
end end
def test_map_doesnt_call_arbitrary_stuff def test_map_doesnt_call_arbitrary_stuff
assert_equal "", Liquid::Template.parse('{{ "foo" | map: "__id__" }}').render assert_template_result "", '{{ "foo" | map: "__id__" }}'
assert_equal "", Liquid::Template.parse('{{ "foo" | map: "inspect" }}').render assert_template_result "", '{{ "foo" | map: "inspect" }}'
end end
def test_map_calls_to_liquid def test_map_calls_to_liquid
t = TestThing.new t = TestThing.new
assert_equal "woot: 1", Liquid::Template.parse('{{ foo | map: "whatever" }}').render("foo" => [t]) assert_template_result "woot: 1", '{{ foo | map: "whatever" }}', "foo" => [t]
end
def test_map_on_hashes
assert_template_result "4217", '{{ thing | map: "foo" | map: "bar" }}',
"thing" => { "foo" => [ { "bar" => 42 }, { "bar" => 17 } ] }
end end
def test_sort_calls_to_liquid def test_sort_calls_to_liquid
t = TestThing.new t = TestThing.new
assert_equal "woot: 1", Liquid::Template.parse('{{ foo | sort: "whatever" }}').render("foo" => [t]) assert_template_result "woot: 1", '{{ foo | sort: "whatever" }}', "foo" => [t]
end end
def test_map_over_proc def test_map_over_proc
drop = TestDrop.new drop = TestDrop.new
p = Proc.new{ drop } p = Proc.new{ drop }
templ = '{{ procs | map: "test" }}' templ = '{{ procs | map: "test" }}'
assert_equal "testfoo", Liquid::Template.parse(templ).render("procs" => [p]) assert_template_result "testfoo", templ, "procs" => [p]
end end
def test_map_works_on_enumerables def test_map_works_on_enumerables
assert_equal "123", Liquid::Template.parse('{{ foo | map: "foo" }}').render!("foo" => TestEnumerable.new) assert_template_result "123", '{{ foo | map: "foo" }}', "foo" => TestEnumerable.new
end end
def test_sort_works_on_enumerables def test_sort_works_on_enumerables
assert_equal "213", Liquid::Template.parse('{{ foo | sort: "bar" | map: "foo" }}').render!("foo" => TestEnumerable.new) assert_template_result "213", '{{ foo | sort: "bar" | map: "foo" }}', "foo" => TestEnumerable.new
end end
def test_date def test_date
@@ -210,6 +210,21 @@ class StandardFiltersTest < Test::Unit::TestCase
assert_template_result 'foobar', "{{ 'foo|bar' | remove: '|' }}" assert_template_result 'foobar', "{{ 'foo|bar' | remove: '|' }}"
end end
def test_strip
assert_template_result 'ab c', "{{ source | strip }}", 'source' => " ab c "
assert_template_result 'ab c', "{{ source | strip }}", 'source' => " \tab c \n \t"
end
def test_lstrip
assert_template_result 'ab c ', "{{ source | lstrip }}", 'source' => " ab c "
assert_template_result "ab c \n \t", "{{ source | lstrip }}", 'source' => " \tab c \n \t"
end
def test_rstrip
assert_template_result " ab c", "{{ source | rstrip }}", 'source' => " ab c "
assert_template_result " \tab c", "{{ source | rstrip }}", 'source' => " \tab c \n \t"
end
def test_strip_newlines def test_strip_newlines
assert_template_result 'abc', "{{ source | strip_newlines }}", 'source' => "a\nb\nc" assert_template_result 'abc', "{{ source | strip_newlines }}", 'source' => "a\nb\nc"
assert_template_result 'abc', "{{ source | strip_newlines }}", 'source' => "a\r\nb\nc" assert_template_result 'abc', "{{ source | strip_newlines }}", 'source' => "a\r\nb\nc"
@@ -233,9 +248,6 @@ class StandardFiltersTest < Test::Unit::TestCase
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 }}"
# Ruby v1.9.2-rc1, or higher, backwards compatible Float test
assert_match(/(6\.3)|(6\.(0{13})1)/, Template.parse("{{ '2.1' | times:3 }}").render)
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 }}"
@@ -245,11 +257,8 @@ class StandardFiltersTest < Test::Unit::TestCase
assert_template_result "4", "{{ 12 | divided_by:3 }}" assert_template_result "4", "{{ 12 | divided_by:3 }}"
assert_template_result "4", "{{ 14 | divided_by:3 }}" assert_template_result "4", "{{ 14 | divided_by:3 }}"
# Ruby v1.9.2-rc1, or higher, backwards compatible Float test
assert_match(/4\.(6{13,14})7/, Template.parse("{{ 14 | divided_by:'3.0' }}").render)
assert_template_result "5", "{{ 15 | divided_by:3 }}" assert_template_result "5", "{{ 15 | divided_by:3 }}"
assert_template_result "Liquid error: divided by 0", "{{ 5 | divided_by:0 }}" 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 }}"
end end
@@ -270,6 +279,15 @@ class StandardFiltersTest < Test::Unit::TestCase
assert_template_result('abc',"{{ a | prepend: b}}",assigns) assert_template_result('abc',"{{ a | prepend: b}}",assigns)
end end
def test_default
assert_equal "foo", @filters.default("foo", "bar")
assert_equal "bar", @filters.default(nil, "bar")
assert_equal "bar", @filters.default("", "bar")
assert_equal "bar", @filters.default(false, "bar")
assert_equal "bar", @filters.default([], "bar")
assert_equal "bar", @filters.default({}, "bar")
end
def test_cannot_access_private_methods def test_cannot_access_private_methods
assert_template_result('a',"{{ 'a' | to_number }}") assert_template_result('a',"{{ 'a' | to_number }}")
end end

View File

@@ -22,6 +22,13 @@ class StrainerTest < Test::Unit::TestCase
assert_equal "public", strainer.invoke("public_filter") assert_equal "public", strainer.invoke("public_filter")
end end
def test_stainer_raises_argument_error
strainer = Strainer.create(nil)
assert_raises(Liquid::ArgumentError) do
strainer.invoke("public_filter", 1)
end
end
def test_strainer_only_invokes_public_filter_methods def test_strainer_only_invokes_public_filter_methods
strainer = Strainer.create(nil) strainer = Strainer.create(nil)
assert_equal false, strainer.invokable?('__test__') assert_equal false, strainer.invokable?('__test__')
@@ -49,4 +56,15 @@ class StrainerTest < Test::Unit::TestCase
assert_equal "has_method?", strainer.invoke("invoke", "has_method?", "invoke") assert_equal "has_method?", strainer.invoke("invoke", "has_method?", "invoke")
end end
def test_strainer_uses_a_class_cache_to_avoid_method_cache_invalidation
a, b = Module.new, Module.new
strainer = Strainer.create(nil, [a,b])
assert_kind_of Strainer, strainer
assert_kind_of a, strainer
assert_kind_of b, strainer
Strainer.class_variable_get(:@@filters).each do |m|
assert_kind_of m, strainer
end
end
end # StrainerTest end # StrainerTest

View File

@@ -0,0 +1,10 @@
require 'test_helper'
class CaseTagTest < Test::Unit::TestCase
include Liquid
def test_case_nodelist
template = Liquid::Template.parse('{% case var %}{% when true %}WHEN{% else %}ELSE{% endcase %}')
assert_equal ['WHEN', 'ELSE'], template.root.nodelist[0].nodelist
end
end # CaseTest

View File

@@ -294,4 +294,72 @@ HERE
assigns = {'items' => [1,2,3,4,5]} assigns = {'items' => [1,2,3,4,5]}
assert_template_result(expected, template, assigns) assert_template_result(expected, template, assigns)
end end
def test_for_nodelist
template = Liquid::Template.parse('{% for item in items %}FOR{% endfor %}')
assert_equal ['FOR'], template.root.nodelist[0].nodelist
end
def test_for_else_nodelist
template = Liquid::Template.parse('{% for item in items %}FOR{% else %}ELSE{% endfor %}')
assert_equal ['FOR', 'ELSE'], template.root.nodelist[0].nodelist
end
class LoaderDrop < Liquid::Drop
attr_accessor :each_called, :load_slice_called
def initialize(data)
@data = data
end
def each
@each_called = true
@data.each { |el| yield el }
end
def load_slice(from, to)
@load_slice_called = true
@data[(from..to-1)]
end
end
def test_iterate_with_each_when_no_limit_applied
loader = LoaderDrop.new([1,2,3,4,5])
assigns = {'items' => loader}
expected = '12345'
template = '{% for item in items %}{{item}}{% endfor %}'
assert_template_result(expected, template, assigns)
assert loader.each_called
assert !loader.load_slice_called
end
def test_iterate_with_load_slice_when_limit_applied
loader = LoaderDrop.new([1,2,3,4,5])
assigns = {'items' => loader}
expected = '1'
template = '{% for item in items limit:1 %}{{item}}{% endfor %}'
assert_template_result(expected, template, assigns)
assert !loader.each_called
assert loader.load_slice_called
end
def test_iterate_with_load_slice_when_limit_and_offset_applied
loader = LoaderDrop.new([1,2,3,4,5])
assigns = {'items' => loader}
expected = '34'
template = '{% for item in items offset:2 limit:2 %}{{item}}{% endfor %}'
assert_template_result(expected, template, assigns)
assert !loader.each_called
assert loader.load_slice_called
end
def test_iterate_with_load_slice_returns_same_results_as_without
loader = LoaderDrop.new([1,2,3,4,5])
loader_assigns = {'items' => loader}
array_assigns = {'items' => [1,2,3,4,5]}
expected = '34'
template = '{% for item in items offset:2 limit:2 %}{{item}}{% endfor %}'
assert_template_result(expected, template, loader_assigns)
assert_template_result(expected, template, array_assigns)
end
end end

View File

@@ -157,4 +157,15 @@ class IfElseTagTest < Test::Unit::TestCase
assert_template_result('yes', assert_template_result('yes',
%({% if 'gnomeslab-and-or-liquid' contains 'gnomeslab-and-or-liquid' %}yes{% endif %})) %({% if 'gnomeslab-and-or-liquid' contains 'gnomeslab-and-or-liquid' %}yes{% endif %}))
end end
def test_if_nodelist
template = Liquid::Template.parse('{% if true %}IF{% else %}ELSE{% endif %}')
assert_equal ['IF', 'ELSE'], template.root.nodelist[0].nodelist
end
def test_operators_are_whitelisted
assert_raise(SyntaxError) do
assert_template_result('', %({% if 1 or throw or or 1 %}yes{% endif %}))
end
end
end # IfElseTest end # IfElseTest

View File

@@ -48,6 +48,27 @@ class CountingFileSystem
end end
end end
class CustomInclude < Liquid::Tag
Syntax = /(#{Liquid::QuotedFragment}+)(\s+(?:with|for)\s+(#{Liquid::QuotedFragment}+))?/o
def initialize(tag_name, markup, tokens)
markup =~ Syntax
@template_name = $1
super
end
def parse(tokens)
end
def blank?
false
end
def render(context)
@template_name[1..-2]
end
end
class IncludeTagTest < Test::Unit::TestCase class IncludeTagTest < Test::Unit::TestCase
include Liquid include Liquid
@@ -57,57 +78,52 @@ class IncludeTagTest < Test::Unit::TestCase
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_equal "Product: Draft 151cm ", assert_template_result "Product: Draft 151cm ",
Template.parse("{% include 'product' with products[0] %}").render( "products" => [ {'title' => 'Draft 151cm'}, {'title' => 'Element 155cm'} ] ) "{% include 'product' with products[0] %}", "products" => [ {'title' => 'Draft 151cm'}, {'title' => 'Element 155cm'} ]
end end
def test_include_tag_with_default_name def test_include_tag_with_default_name
assert_equal "Product: Draft 151cm ", assert_template_result "Product: Draft 151cm ",
Template.parse("{% include 'product' %}").render( "product" => {'title' => 'Draft 151cm'} ) "{% include 'product' %}", "product" => {'title' => 'Draft 151cm'}
end end
def test_include_tag_for def test_include_tag_for
assert_template_result "Product: Draft 151cm Product: Element 155cm ",
assert_equal "Product: Draft 151cm Product: Element 155cm ", "{% include 'product' for products %}", "products" => [ {'title' => 'Draft 151cm'}, {'title' => 'Element 155cm'} ]
Template.parse("{% include 'product' for products %}").render( "products" => [ {'title' => 'Draft 151cm'}, {'title' => 'Element 155cm'} ] )
end end
def test_include_tag_with_local_variables def test_include_tag_with_local_variables
assert_equal "Locale: test123 ", assert_template_result "Locale: test123 ", "{% include 'locale_variables' echo1: 'test123' %}"
Template.parse("{% include 'locale_variables' echo1: 'test123' %}").render
end end
def test_include_tag_with_multiple_local_variables def test_include_tag_with_multiple_local_variables
assert_equal "Locale: test123 test321", assert_template_result "Locale: test123 test321",
Template.parse("{% include 'locale_variables' echo1: 'test123', echo2: 'test321' %}").render "{% include 'locale_variables' echo1: 'test123', echo2: 'test321' %}"
end end
def test_include_tag_with_multiple_local_variables_from_context def test_include_tag_with_multiple_local_variables_from_context
assert_equal "Locale: test123 test321", assert_template_result "Locale: test123 test321",
Template.parse("{% include 'locale_variables' echo1: echo1, echo2: more_echos.echo2 %}").render('echo1' => 'test123', 'more_echos' => { "echo2" => 'test321'}) "{% include 'locale_variables' echo1: echo1, echo2: more_echos.echo2 %}",
'echo1' => 'test123', 'more_echos' => { "echo2" => 'test321'}
end end
def test_nested_include_tag def test_nested_include_tag
assert_equal "body body_detail", assert_template_result "body body_detail", "{% include 'body' %}"
Template.parse("{% include 'body' %}").render
assert_equal "header body body_detail footer", assert_template_result "header body body_detail footer", "{% include 'nested_template' %}"
Template.parse("{% include 'nested_template' %}").render
end end
def test_nested_include_with_variable def test_nested_include_with_variable
assert_template_result "Product: Draft 151cm details ",
"{% include 'nested_product_template' with product %}", "product" => {"title" => 'Draft 151cm'}
assert_equal "Product: Draft 151cm details ", assert_template_result "Product: Draft 151cm details Product: Element 155cm details ",
Template.parse("{% include 'nested_product_template' with product %}").render("product" => {"title" => 'Draft 151cm'}) "{% include 'nested_product_template' for products %}", "products" => [{"title" => 'Draft 151cm'}, {"title" => 'Element 155cm'}]
assert_equal "Product: Draft 151cm details Product: Element 155cm details ",
Template.parse("{% include 'nested_product_template' for products %}").render("products" => [{"title" => 'Draft 151cm'}, {"title" => 'Element 155cm'}])
end end
def test_recursively_included_template_does_not_produce_endless_loop def test_recursively_included_template_does_not_produce_endless_loop
@@ -139,28 +155,54 @@ class IncludeTagTest < Test::Unit::TestCase
end end
def test_dynamically_choosen_template def test_dynamically_choosen_template
assert_template_result "Test123", "{% include template %}", "template" => 'Test123'
assert_template_result "Test321", "{% include template %}", "template" => 'Test321'
assert_equal "Test123", Template.parse("{% include template %}").render("template" => 'Test123') assert_template_result "Product: Draft 151cm ", "{% include template for product %}",
assert_equal "Test321", Template.parse("{% include template %}").render("template" => 'Test321') "template" => 'product', 'product' => { 'title' => 'Draft 151cm'}
assert_equal "Product: Draft 151cm ", Template.parse("{% include template for product %}").render("template" => 'product', 'product' => { 'title' => 'Draft 151cm'})
end end
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
def test_include_tag_within_if_statement
assert_template_result "foo_if_true", "{% if true %}{% include 'foo_if_true' %}{% endif %}"
end
def test_custom_include_tag
original_tag = Liquid::Template.tags['include']
Liquid::Template.tags['include'] = CustomInclude
begin
assert_equal "custom_foo",
Template.parse("{% include 'custom_foo' %}").render!
ensure
Liquid::Template.tags['include'] = original_tag
end
end
def test_custom_include_tag_within_if_statement
original_tag = Liquid::Template.tags['include']
Liquid::Template.tags['include'] = CustomInclude
begin
assert_equal "custom_foo_if_true",
Template.parse("{% if true %}{% include 'custom_foo_if_true' %}{% endif %}").render!
ensure
Liquid::Template.tags['include'] = original_tag
end
end
end # IncludeTagTest end # IncludeTagTest

View File

@@ -20,5 +20,6 @@ class RawTagTest < Test::Unit::TestCase
assert_template_result ' Foobar {% invalid {% {% endraw ', '{% raw %} Foobar {% invalid {% {% endraw {% endraw %}' assert_template_result ' Foobar {% invalid {% {% endraw ', '{% raw %} Foobar {% invalid {% {% endraw {% endraw %}'
assert_template_result ' Foobar {% {% {% ', '{% raw %} Foobar {% {% {% {% endraw %}' assert_template_result ' Foobar {% {% {% ', '{% raw %} Foobar {% {% {% {% endraw %}'
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 }}'
end end
end end

View File

@@ -4,7 +4,7 @@ class StandardTagTest < Test::Unit::TestCase
include Liquid include Liquid
def test_tag def test_tag
tag = Tag.new('tag', [], []) tag = Tag.parse('tag', [], [], {})
assert_equal 'liquid::tag', tag.name assert_equal 'liquid::tag', tag.name
assert_equal '', tag.render(Context.new) assert_equal '', tag.render(Context.new)
end end
@@ -33,6 +33,13 @@ class StandardTagTest < Test::Unit::TestCase
assert_template_result('','{% comment %}{% endcomment %}') assert_template_result('','{% comment %}{% endcomment %}')
assert_template_result('','{%comment%}comment{%endcomment%}') assert_template_result('','{%comment%}comment{%endcomment%}')
assert_template_result('','{% comment %}comment{% endcomment %}') assert_template_result('','{% comment %}comment{% endcomment %}')
assert_template_result('','{% comment %} 1 {% comment %} 2 {% endcomment %} 3 {% endcomment %}')
assert_template_result('','{%comment%}{%blabla%}{%endcomment%}')
assert_template_result('','{% comment %}{% blabla %}{% endcomment %}')
assert_template_result('','{%comment%}{% endif %}{%endcomment%}')
assert_template_result('','{% comment %}{% endwhatever %}{% endcomment %}')
assert_template_result('','{% comment %}{% raw %} {{%%%%}} }} { {% endcomment %} {% comment {% endraw %} {% endcomment %}')
assert_template_result('foobar','foo{%comment%}comment{%endcomment%}bar') assert_template_result('foobar','foo{%comment%}comment{%endcomment%}bar')
assert_template_result('foobar','foo{% comment %}comment{% endcomment %}bar') assert_template_result('foobar','foo{% comment %}comment{% endcomment %}bar')
@@ -47,16 +54,9 @@ class StandardTagTest < Test::Unit::TestCase
{%endcomment%}bar') {%endcomment%}bar')
end end
def test_assign
assigns = {'var' => 'content' }
assert_template_result('var2: var2:content', 'var2:{{var2}} {%assign var2 = var%} var2:{{var2}}', assigns)
end
def test_hyphenated_assign def test_hyphenated_assign
assigns = {'a-b' => '1' } assigns = {'a-b' => '1' }
assert_template_result('a-b:1 a-b:2', 'a-b:{{a-b}} {%assign a-b = 2 %}a-b:{{a-b}}', assigns) assert_template_result('a-b:1 a-b:2', 'a-b:{{a-b}} {%assign a-b = 2 %}a-b:{{a-b}}', assigns)
end end
def test_assign_with_colon_and_spaces def test_assign_with_colon_and_spaces
@@ -180,11 +180,11 @@ class StandardTagTest < Test::Unit::TestCase
# 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 = %q({% 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'})
assert_equal "womenswear", template.render("collection" => {'handle' => 'x'}) assert_equal "womenswear", template.render!("collection" => {'handle' => 'x'})
assert_equal "womenswear", template.render("collection" => {'handle' => 'y'}) assert_equal "womenswear", template.render!("collection" => {'handle' => 'y'})
assert_equal "womenswear", template.render("collection" => {'handle' => 'z'}) assert_equal "womenswear", template.render!("collection" => {'handle' => 'z'})
end end
def test_case_when_or def test_case_when_or
@@ -218,16 +218,20 @@ class StandardTagTest < Test::Unit::TestCase
end end
def test_assign def test_assign
assert_equal 'variable', Liquid::Template.parse( '{% assign a = "variable"%}{{a}}' ).render assert_template_result 'variable', '{% assign a = "variable"%}{{a}}'
end
def test_assign_unassigned
assigns = { 'var' => 'content' }
assert_template_result('var2: var2:content', 'var2:{{var2}} {%assign var2 = var%} var2:{{var2}}', assigns)
end end
def test_assign_an_empty_string def test_assign_an_empty_string
assert_equal '', Liquid::Template.parse( '{% assign a = ""%}{{a}}' ).render assert_template_result '', '{% assign a = ""%}{{a}}'
end end
def test_assign_is_global def test_assign_is_global
assert_equal 'variable', assert_template_result 'variable', '{%for i in (1..2) %}{% assign a = "variable"%}{% endfor %}{{a}}'
Liquid::Template.parse( '{%for i in (1..2) %}{% assign a = "variable"%}{% endfor %}{{a}}' ).render
end end
def test_case_detects_bad_syntax def test_case_detects_bad_syntax
@@ -292,4 +296,8 @@ class StandardTagTest < Test::Unit::TestCase
assigns = {'array' => [ 1, 1, 1, 1] } assigns = {'array' => [ 1, 1, 1, 1] }
assert_template_result('1','{%for item in array%}{%ifchanged%}{{item}}{% endifchanged %}{%endfor%}',assigns) assert_template_result('1','{%for item in array%}{%ifchanged%}{{item}}{% endifchanged %}{%endfor%}',assigns)
end end
def test_multiline_tag
assert_template_result '0 1 2 3', "0{%\nfor i in (1..3)\n%} {{\ni\n}}{%\nendfor\n%}"
end
end # StandardTagTest end # StandardTagTest

View File

@@ -4,93 +4,78 @@ class StatementsTest < Test::Unit::TestCase
include Liquid include Liquid
def test_true_eql_true def test_true_eql_true
text = %| {% if true == true %} true {% else %} false {% endif %} | text = ' {% if true == true %} true {% else %} false {% endif %} '
expected = %| true | assert_template_result ' true ', text
assert_equal expected, Template.parse(text).render
end end
def test_true_not_eql_true def test_true_not_eql_true
text = %| {% if true != true %} true {% else %} false {% endif %} | text = ' {% if true != true %} true {% else %} false {% endif %} '
expected = %| false | assert_template_result ' false ', text
assert_equal expected, Template.parse(text).render
end end
def test_true_lq_true def test_true_lq_true
text = %| {% if 0 > 0 %} true {% else %} false {% endif %} | text = ' {% if 0 > 0 %} true {% else %} false {% endif %} '
expected = %| false | assert_template_result ' false ', text
assert_equal expected, Template.parse(text).render
end end
def test_one_lq_zero def test_one_lq_zero
text = %| {% if 1 > 0 %} true {% else %} false {% endif %} | text = ' {% if 1 > 0 %} true {% else %} false {% endif %} '
expected = %| true | assert_template_result ' true ', text
assert_equal expected, Template.parse(text).render
end end
def test_zero_lq_one def test_zero_lq_one
text = %| {% if 0 < 1 %} true {% else %} false {% endif %} | text = ' {% if 0 < 1 %} true {% else %} false {% endif %} '
expected = %| true | assert_template_result ' true ', text
assert_equal expected, Template.parse(text).render
end end
def test_zero_lq_or_equal_one def test_zero_lq_or_equal_one
text = %| {% if 0 <= 0 %} true {% else %} false {% endif %} | text = ' {% if 0 <= 0 %} true {% else %} false {% endif %} '
expected = %| true | assert_template_result ' true ', text
assert_equal expected, Template.parse(text).render
end end
def test_zero_lq_or_equal_one_involving_nil def test_zero_lq_or_equal_one_involving_nil
text = %| {% if null <= 0 %} true {% else %} false {% endif %} | text = ' {% if null <= 0 %} true {% else %} false {% endif %} '
expected = %| false | assert_template_result ' false ', text
assert_equal expected, Template.parse(text).render
text = %| {% if 0 <= null %} true {% else %} false {% endif %} | text = ' {% if 0 <= null %} true {% else %} false {% endif %} '
expected = %| false | assert_template_result ' false ', text
assert_equal expected, Template.parse(text).render
end end
def test_zero_lqq_or_equal_one def test_zero_lqq_or_equal_one
text = %| {% if 0 >= 0 %} true {% else %} false {% endif %} | text = ' {% if 0 >= 0 %} true {% else %} false {% endif %} '
expected = %| true | assert_template_result ' true ', text
assert_equal expected, Template.parse(text).render
end end
def test_strings def test_strings
text = %| {% if 'test' == 'test' %} true {% else %} false {% endif %} | text = " {% if 'test' == 'test' %} true {% else %} false {% endif %} "
expected = %| true | assert_template_result ' true ', text
assert_equal expected, Template.parse(text).render
end end
def test_strings_not_equal def test_strings_not_equal
text = %| {% if 'test' != 'test' %} true {% else %} false {% endif %} | text = " {% if 'test' != 'test' %} true {% else %} false {% endif %} "
expected = %| false | assert_template_result ' false ', text
assert_equal expected, Template.parse(text).render
end end
def test_var_strings_equal def test_var_strings_equal
text = %| {% if var == "hello there!" %} true {% else %} false {% endif %} | text = ' {% if var == "hello there!" %} true {% else %} false {% endif %} '
expected = %| true | assert_template_result ' true ', text, 'var' => 'hello there!'
assert_equal expected, Template.parse(text).render('var' => 'hello there!')
end end
def test_var_strings_are_not_equal def test_var_strings_are_not_equal
text = %| {% if "hello there!" == var %} true {% else %} false {% endif %} | text = ' {% if "hello there!" == var %} true {% else %} false {% endif %} '
expected = %| true | assert_template_result ' true ', text, 'var' => 'hello there!'
assert_equal expected, Template.parse(text).render('var' => 'hello there!')
end end
def test_var_and_long_string_are_equal def test_var_and_long_string_are_equal
text = %| {% if var == 'hello there!' %} true {% else %} false {% endif %} | text = " {% if var == 'hello there!' %} true {% else %} false {% endif %} "
expected = %| true | assert_template_result ' true ', text, 'var' => 'hello there!'
assert_equal expected, Template.parse(text).render('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 %} "
expected = %| true | assert_template_result ' true ', text, 'var' => 'hello there!'
assert_equal expected, Template.parse(text).render('var' => 'hello there!')
end end
#def test_is_nil #def test_is_nil
@@ -101,34 +86,28 @@ class StatementsTest < Test::Unit::TestCase
#end #end
def test_is_collection_empty def test_is_collection_empty
text = %| {% if array == empty %} true {% else %} false {% endif %} | text = ' {% if array == empty %} true {% else %} false {% endif %} '
expected = %| true | assert_template_result ' true ', text, 'array' => []
assert_equal expected, Template.parse(text).render('array' => [])
end end
def test_is_not_collection_empty def test_is_not_collection_empty
text = %| {% if array == empty %} true {% else %} false {% endif %} | text = ' {% if array == empty %} true {% else %} false {% endif %} '
expected = %| false | assert_template_result ' false ', text, 'array' => [1,2,3]
assert_equal expected, Template.parse(text).render('array' => [1,2,3])
end end
def test_nil def test_nil
text = %| {% if var == nil %} true {% else %} false {% endif %} | text = ' {% if var == nil %} true {% else %} false {% endif %} '
expected = %| true | assert_template_result ' true ', text, 'var' => nil
assert_equal expected, Template.parse(text).render('var' => nil)
text = %| {% if var == null %} true {% else %} false {% endif %} | text = ' {% if var == null %} true {% else %} false {% endif %} '
expected = %| true | assert_template_result ' true ', text, 'var' => nil
assert_equal expected, Template.parse(text).render('var' => nil)
end end
def test_not_nil def test_not_nil
text = %| {% if var != nil %} true {% else %} false {% endif %} | text = ' {% if var != nil %} true {% else %} false {% endif %} '
expected = %| true | assert_template_result ' true ', text, 'var' => 1
assert_equal expected, Template.parse(text).render('var' => 1 )
text = %| {% if var != null %} true {% else %} false {% endif %} | text = ' {% if var != null %} true {% else %} false {% endif %} '
expected = %| true | assert_template_result ' true ', text, 'var' => 1
assert_equal expected, Template.parse(text).render('var' => 1 )
end end
end # StatementsTest end # StatementsTest

View File

@@ -1,6 +1,6 @@
require 'test_helper' require 'test_helper'
class HtmlTagTest < Test::Unit::TestCase class TableRowTest < Test::Unit::TestCase
include Liquid include Liquid
class ArrayDrop < Liquid::Drop class ArrayDrop < Liquid::Drop
@@ -15,7 +15,7 @@ class HtmlTagTest < Test::Unit::TestCase
end end
end end
def test_html_table 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 %}',
@@ -26,14 +26,14 @@ class HtmlTagTest < Test::Unit::TestCase
'numbers' => []) 'numbers' => [])
end end
def test_html_table_with_different_cols def test_table_row_with_different_cols
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_html_col_counter def test_table_col_counter
assert_template_result("<tr class=\"row1\">\n<td class=\"col1\">1</td><td class=\"col2\">2</td></tr>\n<tr class=\"row2\"><td class=\"col1\">1</td><td class=\"col2\">2</td></tr>\n<tr class=\"row3\"><td class=\"col1\">1</td><td class=\"col2\">2</td></tr>\n", assert_template_result("<tr class=\"row1\">\n<td class=\"col1\">1</td><td class=\"col2\">2</td></tr>\n<tr class=\"row2\"><td class=\"col1\">1</td><td class=\"col2\">2</td></tr>\n<tr class=\"row3\"><td class=\"col1\">1</td><td class=\"col2\">2</td></tr>\n",
'{% tablerow n in numbers cols:2%}{{tablerowloop.col}}{% endtablerow %}', '{% tablerow n in numbers cols:2%}{{tablerowloop.col}}{% endtablerow %}',
'numbers' => [1,2,3,4,5,6]) 'numbers' => [1,2,3,4,5,6])
@@ -60,4 +60,4 @@ class HtmlTagTest < Test::Unit::TestCase
'{% 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
end # HtmlTagTest end

View File

@@ -14,85 +14,79 @@ class TemplateContextDrop < Liquid::Drop
end end
end end
class SomethingWithLength
def length
nil
end
liquid_methods :length
end
class TemplateTest < Test::Unit::TestCase class TemplateTest < Test::Unit::TestCase
include Liquid include Liquid
def test_tokenize_strings
assert_equal [' '], Template.new.send(:tokenize, ' ')
assert_equal ['hello world'], Template.new.send(:tokenize, 'hello world')
end
def test_tokenize_variables
assert_equal ['{{funk}}'], Template.new.send(:tokenize, '{{funk}}')
assert_equal [' ', '{{funk}}', ' '], Template.new.send(:tokenize, ' {{funk}} ')
assert_equal [' ', '{{funk}}', ' ', '{{so}}', ' ', '{{brother}}', ' '], Template.new.send(:tokenize, ' {{funk}} {{so}} {{brother}} ')
assert_equal [' ', '{{ funk }}', ' '], Template.new.send(:tokenize, ' {{ funk }} ')
end
def test_tokenize_blocks
assert_equal ['{%comment%}'], Template.new.send(:tokenize, '{%comment%}')
assert_equal [' ', '{%comment%}', ' '], Template.new.send(:tokenize, ' {%comment%} ')
assert_equal [' ', '{%comment%}', ' ', '{%endcomment%}', ' '], Template.new.send(:tokenize, ' {%comment%} {%endcomment%} ')
assert_equal [' ', '{% comment %}', ' ', '{% endcomment %}', ' '], Template.new.send(:tokenize, " {% comment %} {% endcomment %} ")
end
def test_instance_assigns_persist_on_same_template_object_between_parses def test_instance_assigns_persist_on_same_template_object_between_parses
t = Template.new t = Template.new
assert_equal 'from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render assert_equal 'from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render!
assert_equal 'from instance assigns', t.parse("{{ foo }}").render assert_equal 'from instance assigns', t.parse("{{ foo }}").render!
end end
def test_instance_assigns_persist_on_same_template_parsing_between_renders def test_instance_assigns_persist_on_same_template_parsing_between_renders
t = Template.new.parse("{{ foo }}{% assign foo = 'foo' %}{{ foo }}") t = Template.new.parse("{{ foo }}{% assign foo = 'foo' %}{{ foo }}")
assert_equal 'foo', t.render assert_equal 'foo', t.render!
assert_equal 'foofoo', t.render assert_equal 'foofoo', t.render!
end end
def test_custom_assigns_do_not_persist_on_same_template def test_custom_assigns_do_not_persist_on_same_template
t = Template.new t = Template.new
assert_equal 'from custom assigns', t.parse("{{ foo }}").render('foo' => 'from custom assigns') assert_equal 'from custom assigns', t.parse("{{ foo }}").render!('foo' => 'from custom assigns')
assert_equal '', t.parse("{{ foo }}").render assert_equal '', t.parse("{{ foo }}").render!
end end
def test_custom_assigns_squash_instance_assigns def test_custom_assigns_squash_instance_assigns
t = Template.new t = Template.new
assert_equal 'from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render assert_equal 'from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render!
assert_equal 'from custom assigns', t.parse("{{ foo }}").render('foo' => 'from custom assigns') assert_equal 'from custom assigns', t.parse("{{ foo }}").render!('foo' => 'from custom assigns')
end end
def test_persistent_assigns_squash_instance_assigns def test_persistent_assigns_squash_instance_assigns
t = Template.new t = Template.new
assert_equal 'from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render assert_equal 'from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render!
t.assigns['foo'] = 'from persistent assigns' t.assigns['foo'] = 'from persistent assigns'
assert_equal 'from persistent assigns', t.parse("{{ foo }}").render assert_equal 'from persistent assigns', t.parse("{{ foo }}").render!
end end
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'] = lambda { @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!
@global = nil @global = nil
end end
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' => lambda { @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)
@global = nil @global = nil
end end
def test_resource_limits_works_with_custom_length_method
t = Template.parse("{% assign foo = bar %}")
t.resource_limits = { :render_length_limit => 42 }
assert_equal "", t.render!("bar" => SomethingWithLength.new)
end
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!()
assert_not_nil t.resource_limits[:render_length_current] assert_not_nil t.resource_limits[:render_length_current]
end end
@@ -106,7 +100,7 @@ class TemplateTest < Test::Unit::TestCase
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!()
assert_not_nil t.resource_limits[:render_score_current] assert_not_nil t.resource_limits[:render_score_current]
end end
@@ -116,7 +110,7 @@ class TemplateTest < Test::Unit::TestCase
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!()
assert_not_nil t.resource_limits[:assign_score_current] assert_not_nil t.resource_limits[:assign_score_current]
end end
@@ -129,7 +123,7 @@ class TemplateTest < Test::Unit::TestCase
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_current] > 0 assert t.resource_limits[:assign_score_current] > 0
assert t.resource_limits[:render_score_current] > 0 assert t.resource_limits[:render_score_current] > 0
assert t.resource_limits[:render_length_current] > 0 assert t.resource_limits[:render_length_current] > 0
@@ -139,9 +133,9 @@ class TemplateTest < Test::Unit::TestCase
t = Template.new t = Template.new
t.registers['lulz'] = 'haha' t.registers['lulz'] = 'haha'
drop = TemplateContextDrop.new drop = TemplateContextDrop.new
assert_equal 'fizzbuzz', t.parse('{{foo}}').render(drop) assert_equal 'fizzbuzz', t.parse('{{foo}}').render!(drop)
assert_equal 'bar', t.parse('{{bar}}').render(drop) assert_equal 'bar', t.parse('{{bar}}').render!(drop)
assert_equal 'haha', t.parse("{{baz}}").render(drop) assert_equal 'haha', t.parse("{{baz}}").render!(drop)
end end
def test_sets_default_localization_in_document def test_sets_default_localization_in_document

View File

@@ -0,0 +1,34 @@
require 'test_helper'
class TokenizerTest < Test::Unit::TestCase
def test_tokenize_strings
assert_equal [' '], tokenize(' ')
assert_equal ['hello world'], tokenize('hello world')
end
def test_tokenize_variables
assert_equal ['{{funk}}'], tokenize('{{funk}}')
assert_equal [' ', '{{funk}}', ' '], tokenize(' {{funk}} ')
assert_equal [' ', '{{funk}}', ' ', '{{so}}', ' ', '{{brother}}', ' '], tokenize(' {{funk}} {{so}} {{brother}} ')
assert_equal [' ', '{{ funk }}', ' '], tokenize(' {{ funk }} ')
end
def test_tokenize_blocks
assert_equal ['{%comment%}'], tokenize('{%comment%}')
assert_equal [' ', '{%comment%}', ' '], tokenize(' {%comment%} ')
assert_equal [' ', '{%comment%}', ' ', '{%endcomment%}', ' '], tokenize(' {%comment%} {%endcomment%} ')
assert_equal [' ', '{% comment %}', ' ', '{% endcomment %}', ' '], tokenize(" {% comment %} {% endcomment %} ")
end
private
def tokenize(source)
tokenizer = Liquid::Tokenizer.new(source)
tokens = []
while token = tokenizer.next
tokens << token
end
tokens
end
end

View File

@@ -141,49 +141,49 @@ class VariableResolutionTest < Test::Unit::TestCase
def test_simple_variable def test_simple_variable
template = Template.parse(%|{{test}}|) template = Template.parse(%|{{test}}|)
assert_equal 'worked', template.render('test' => 'worked') assert_equal 'worked', template.render!('test' => 'worked')
assert_equal 'worked wonderfully', template.render('test' => 'worked wonderfully') assert_equal 'worked wonderfully', template.render!('test' => 'worked wonderfully')
end end
def test_simple_with_whitespaces def test_simple_with_whitespaces
template = Template.parse(%| {{ test }} |) template = Template.parse(%| {{ test }} |)
assert_equal ' worked ', template.render('test' => 'worked') assert_equal ' worked ', template.render!('test' => 'worked')
assert_equal ' worked wonderfully ', template.render('test' => 'worked wonderfully') assert_equal ' worked wonderfully ', template.render!('test' => 'worked wonderfully')
end end
def test_ignore_unknown def test_ignore_unknown
template = Template.parse(%|{{ test }}|) template = Template.parse(%|{{ test }}|)
assert_equal '', template.render assert_equal '', template.render!
end end
def test_hash_scoping def test_hash_scoping
template = Template.parse(%|{{ test.test }}|) template = Template.parse(%|{{ test.test }}|)
assert_equal 'worked', template.render('test' => {'test' => 'worked'}) assert_equal 'worked', template.render!('test' => {'test' => 'worked'})
end end
def test_preset_assigns def test_preset_assigns
template = Template.parse(%|{{ test }}|) template = Template.parse(%|{{ test }}|)
template.assigns['test'] = 'worked' template.assigns['test'] = 'worked'
assert_equal 'worked', template.render assert_equal 'worked', template.render!
end end
def test_reuse_parsed_template def test_reuse_parsed_template
template = Template.parse(%|{{ greeting }} {{ name }}|) template = Template.parse(%|{{ greeting }} {{ name }}|)
template.assigns['greeting'] = 'Goodbye' template.assigns['greeting'] = 'Goodbye'
assert_equal 'Hello Tobi', template.render('greeting' => 'Hello', 'name' => 'Tobi') assert_equal 'Hello Tobi', template.render!('greeting' => 'Hello', 'name' => 'Tobi')
assert_equal 'Hello ', template.render('greeting' => 'Hello', 'unknown' => 'Tobi') assert_equal 'Hello ', template.render!('greeting' => 'Hello', 'unknown' => 'Tobi')
assert_equal 'Hello Brian', template.render('greeting' => 'Hello', 'name' => 'Brian') assert_equal 'Hello Brian', template.render!('greeting' => 'Hello', 'name' => 'Brian')
assert_equal 'Goodbye Brian', template.render('name' => 'Brian') assert_equal 'Goodbye Brian', template.render!('name' => 'Brian')
assert_equal({'greeting'=>'Goodbye'}, template.assigns) assert_equal({'greeting'=>'Goodbye'}, template.assigns)
end end
def test_assigns_not_polluted_from_template def test_assigns_not_polluted_from_template
template = Template.parse(%|{{ test }}{% assign test = 'bar' %}{{ test }}|) template = Template.parse(%|{{ test }}{% assign test = 'bar' %}{{ test }}|)
template.assigns['test'] = 'baz' template.assigns['test'] = 'baz'
assert_equal 'bazbar', template.render assert_equal 'bazbar', template.render!
assert_equal 'bazbar', template.render assert_equal 'bazbar', template.render!
assert_equal 'foobar', template.render('test' => 'foo') assert_equal 'foobar', template.render!('test' => 'foo')
assert_equal 'bazbar', template.render assert_equal 'bazbar', template.render!
end end
def test_hash_with_default_proc def test_hash_with_default_proc
@@ -197,4 +197,8 @@ class VariableResolutionTest < Test::Unit::TestCase
} }
assert_equal "Unknown variable 'test'", e.message assert_equal "Unknown variable 'test'", e.message
end end
def test_multiline_variable
assert_equal 'worked', Template.parse("{{\ntest\n}}").render!('test' => 'worked')
end
end # VariableTest end # VariableTest

View File

@@ -2,11 +2,6 @@
require 'test/unit' require 'test/unit'
require 'test/unit/assertions' require 'test/unit/assertions'
begin
require 'ruby-debug'
rescue LoadError
puts "Couldn't load ruby-debug. gem install ruby-debug if you need it."
end
$:.unshift(File.join(File.expand_path(File.dirname(__FILE__)), '..', 'lib')) $:.unshift(File.join(File.expand_path(File.dirname(__FILE__)), '..', 'lib'))
require 'liquid.rb' require 'liquid.rb'
@@ -31,13 +26,13 @@ module Test
include Liquid include Liquid
def assert_template_result(expected, template, assigns = {}, message = nil) def assert_template_result(expected, template, assigns = {}, message = nil)
assert_equal expected, Template.parse(template).render(assigns) assert_equal expected, Template.parse(template).render!(assigns)
end end
def assert_template_result_matches(expected, template, assigns = {}, message = nil) def assert_template_result_matches(expected, template, assigns = {}, message = nil)
return assert_template_result(expected, template, assigns, message) unless expected.is_a? Regexp return assert_template_result(expected, template, assigns, message) unless expected.is_a? Regexp
assert_match expected, Template.parse(template).render(assigns) assert_match expected, Template.parse(template).render!(assigns)
end end
def assert_match_syntax_error(match, template, registers = {}) def assert_match_syntax_error(match, template, registers = {})