Compare commits

..

130 Commits

Author SHA1 Message Date
Tobias Lutke
43b44c007a remove binaries 2012-10-29 21:16:25 -04:00
Tobias Lutke
858cb62c4f various speedups 2012-10-29 21:15:50 -04:00
Tobias Lütke
80be33884e Convert parser to C99 2012-10-29 16:43:42 -04:00
Tobias Lutke
cd040dabd8 Implement naive recusrive descent
Ragel doesn't allow us to recurse so we simply
reinvoke the parser for each step.
2012-10-28 21:55:20 -04:00
Tobias Lütke
18b83a58bd Replace regexpes with Ragel grammer
context parsing was handrolled and pretty ad-hoc
this branch exists to explore parsing the context
through a defined fsm as produced by Ragel
2012-10-28 21:50:18 -04:00
Tobias Lutke
6b64bfb53e fix benchmarks 2012-10-28 21:37:07 -04:00
Tom Burns
6c2fde5eea Instantiate blank string once instead of at every comparison 2012-10-25 11:54:19 -04:00
Tobias Lütke
ce76dbf8d9 fixed the performance suite 2012-10-20 10:53:53 -04:00
Steven Soroka
661ff2ccdf Merge pull request #140 from binarycleric/feature/break_for_loop
Added break and continue statements
2012-08-21 13:22:24 -07:00
Jon Daniel
9c183bea83 added interrupt class for continue/break statements
When a continue or break statement is executed it pushes an interrupt to a
stack in context. If any non-handled interrupts are present blocks will cease
to execute. The for loop can handle the most recent interrupt in the stack.
2012-08-21 13:14:27 -04:00
Jon Daniel
484fd18612 added break and continue tags 2012-08-21 00:00:02 -04:00
Jonathan Rudenberg
bf86459456 Merge pull request #139 from pjb3/fix_block_test_name
Class name does not match file name
2012-08-19 15:07:59 -07:00
Paul Barry
d2827c561b Class name does not match file name 2012-08-19 07:44:35 -04:00
Tobias Lütke
16c34595a4 fix mergeconflict 2012-08-07 13:21:31 -04:00
Tobias Lütke
6e091909ee Merge branch 'master' of github.com:Shopify/liquid 2012-08-07 13:20:37 -04:00
Tobias Lütke
d7cb39ccb3 release 2.4.0 2012-08-07 13:20:23 -04:00
Jonathan Rudenberg
f8d46804fd Fix rake test for broken version of rake on travis 2012-08-07 09:49:55 -04:00
Jonathan Rudenberg
5c6de2d919 Fix typo 2012-08-07 09:37:19 -04:00
Jonathan Rudenberg
a8e9327f0b Update HISTORY.md for v2.4.0 release 2012-08-07 09:32:38 -04:00
Dylan Smith
f5a20ff8e8 Fix a regression in tablerow limit parameter.
I had accidentally read slice_collection_using_each as using to as an
inclusive limit rather than exclusive, and no tests covered the offset or
limit parameters.
2012-06-21 14:56:05 -04:00
Dylan Smith
d0184555d9 Allow tablerow to work with any Enumerable. Closes #132 2012-06-20 11:07:11 -04:00
Jason Normore
6ebdded8f2 Merge branch 'issue_1650_strip_html_ignore_comments' 2012-06-11 10:33:00 -04:00
Jason Normore
515b31158e strip_html to ignore comments with html tags. fixes #1650 2012-06-11 10:32:12 -04:00
7rans
40cc799f3d Add example to split filter. 2012-06-11 10:32:12 -04:00
Daniel Schierbeck
5ac91e0837 Fix typo and add punctuation 2012-06-11 10:32:12 -04:00
Jonathan Rudenberg
f6cb54fa59 Merge pull request #93 from trans/master
Split Filter Example
2012-06-07 12:21:40 -07:00
Jonathan Rudenberg
1606b4b705 Merge pull request #118 from dasch/patch-1
Fix typo and add punctuation
2012-06-07 12:20:54 -07:00
Jonathan Rudenberg
7cfd0f15d1 Merge pull request #128 from andmej/patch-1
Tpyo.
2012-06-07 12:18:09 -07:00
Jonathan Rudenberg
25ba54fc52 Enable 19mode for travis rbx/jruby 2012-06-07 15:16:58 -04:00
Jonathan Rudenberg
1aff63ff57 Merge pull request #107 from amateurhuman/syntax-error-fixes-for-rubinius
Fix syntax error in htmltags.rb and for.rb for compatibility with rbx-2.0.0-dev (1.9.3)
2012-06-07 12:14:35 -07:00
Jonathan Rudenberg
08fdcbbf65 Merge pull request #120 from infospace/interpolate_regex_once
add interpolate once flag to regexes that never change
2012-06-07 12:06:57 -07:00
Jonathan Rudenberg
2dba9ed0ea Merge pull request #113 from arika/improve-process-time
apply "o" option to regexps to improve process time
2012-06-07 12:04:28 -07:00
Andrés Mejía
6d02d59fbd Tpyo. 2012-06-06 22:32:42 -05:00
Michael Green
281e3ea9c8 add interpolate once flag to regexes that never change 2012-05-08 16:27:50 -07:00
Daniel Schierbeck
b51b30fac1 Fix typo and add punctuation 2012-05-03 14:16:06 +03:00
akira yamada
84ed3d9964 apply "o" option to regexps to improve process time 2012-04-02 19:31:55 +09:00
Dennis Theisen
c10f936d2a Merge pull request #109 from DanAtkinson/patch-1
Minor modification to readme
2012-03-14 08:27:43 -07:00
Dan Atkinson
1ee342d83b * Seperated 'Howto' into 'How to'.
* Added periods to the second list as the first item has them. I guess I'm anally retentive like that. :)
2012-03-14 14:58:12 +00:00
Dennis Theisen
0e3b522fe2 Fix conditions using negative number comparisons 2012-03-12 16:38:34 -04:00
Chris Kelly
db07e2b67e Fix syntax error in for and htmltags files for compatibility with Rubinius 2.0.0-dev 2012-03-09 00:24:31 -08:00
Dennis Theisen
b8d7b9aeda Fix for-tag update to also work properly in Ruby 1.8.
* Follow up commit to 3d7c1c8
2012-02-29 16:13:46 -05:00
Dennis Theisen
3d7c1c80a0 Ruby 1.8 compatibility fix: Ensure for-loop on an empty string does not enter for-body. 2012-02-29 14:41:21 -05:00
Dennis Theisen
1b2d0198ea Added backwards compatibility test for tablerow tag update
* Follow up to 043d816
2012-02-22 14:30:19 -05:00
Dennis Theisen
043d816460 Fix tablerow block to work with collection names in quoted syntax.
* Allows e.g. {% tablerow product in collections['frontpage'] %} instead of only collections.frontpage
2012-02-22 12:45:38 -05:00
7rans
974ea40cca Add example to split filter. 2012-02-10 13:45:35 -05:00
Kristian PD
d8b416187a Merge pull request #88 from kristianpd/master
Add 1.9.3 support for 1.8.7 like strings behaviour in forloop
2012-01-31 10:27:44 -08:00
Kristian PD
58ad90677b Clarified compatibility comments, removed unused var from tag test. 2012-01-20 17:22:23 -05:00
Kristian PD
2b04590d4b Revert "Revert "Added backwards compatibility for 1.8.7 String.each returning itself (and 1.9.3 not supporting .each).""
This reverts commit bce0033c65.
2012-01-20 16:54:26 -05:00
Kristian PD
bce0033c65 Revert "Added backwards compatibility for 1.8.7 String.each returning itself (and 1.9.3 not supporting .each)."
This reverts commit 01dea94671.
2012-01-20 16:47:34 -05:00
Kristian PD
01dea94671 Added backwards compatibility for 1.8.7 String.each returning itself (and 1.9.3 not supporting .each).
Moved for tag tests to their own test model from StandardTagTest.
2012-01-20 16:45:41 -05:00
Tobias Lütke
1a1b4702d7 Merge pull request #80 from ROFISH/master
Add Filters to Assign
2011-12-20 13:58:05 -08:00
Steven Soroka
85d1dc0d07 Merge pull request #84 from biow0lf/master
Fix typo
2011-12-16 09:12:07 -08:00
Igor Zubkov
0eafe7f2fd Fix typo 2011-12-16 12:28:29 +02:00
Jonathan Rudenberg
815e4e2b8d Remove travis rbx-1.9 mode for now 2011-11-17 16:55:50 -05:00
Jonathan Rudenberg
ef98715b12 Update Travis rubies 2011-11-17 14:23:25 -05:00
Jonathan Rudenberg
c4d713b6bb Add modulo filter 2011-11-17 14:08:57 -05:00
Ryan Alyea
975b17b529 Allow filters in assign.
A simple attempt, but basically it shifts the parsing of the right hand side of the assign from a simple context lookup to using the Variable parser (that includes filters). Fixes #79.
2011-11-01 01:46:26 -07:00
Jonathan Rudenberg
745d875e79 Add date to History.md 2011-10-16 10:22:20 -04:00
Tobias Lütke
c91cb8af6c released new gem 2011-10-16 09:16:23 -04:00
Jonathan Rudenberg
bb73198b4f Fix servlet strip example 2011-10-15 22:59:58 -04:00
Jonathan Rudenberg
f0f2e56369 Merge pull request #76 from trans/master
Add split example to example servlet
2011-10-15 19:58:40 -07:00
Jonathan Rudenberg
b816521563 Merge pull request #72 from zacstewart/patch-1
Correct formatting for doc comments
2011-10-15 17:25:48 -07:00
Jonathan Rudenberg
4026f6c340 Update history file 2011-10-15 20:18:00 -04:00
Jonathan Rudenberg
05a20c627d Clean up test suite 2011-10-15 20:04:27 -04:00
Jonathan Rudenberg
8e3f0c122e Clean up Rakefile and gemspec 2011-10-15 19:56:03 -04:00
Jonathan Rudenberg
487b240404 Fix another drop call regression 2011-10-14 11:21:22 -04:00
Jonathan Rudenberg
f129ee33f2 Remove 1.8.6 (unsupported) from Travis 2011-10-13 12:03:08 -04:00
Jonathan Rudenberg
cdf7f5b6a7 Fix UTC time for Travis 2011-10-13 12:00:42 -04:00
Jonathan Rudenberg
204d876187 Don't use the rubygems version of liquid in performance test 2011-10-13 11:48:24 -04:00
Jonathan Rudenberg
8b5cf73ccc Fix regression with calling a drop with a empty string 2011-10-13 11:47:23 -04:00
7rans
edebcaa603 Add split example to example servlet. 2011-09-29 18:41:47 -04:00
Zac
6cf6d8b990 Correct formatting for doc comments
Portions of the code examples were not being rendered as code because they lacked a leading tab. Also, a missing period on the first sentence made the first paragraph confusing.
2011-09-10 10:36:22 -03:00
Tobias Lütke
1379061398 Merge pull request #69 from trans/master
Added split filter
2011-09-05 13:01:15 -07:00
7rans
4971b9e9bc Remove comment about stripping split results. 2011-09-01 12:03:27 -04:00
7rans
da34d27258 Add test for split filter. 2011-08-31 22:20:45 -04:00
7rans
cc7899aef5 Add split filter. 2011-08-31 21:28:32 -04:00
Jonathan Rudenberg
0fc78a2dbb Merge pull request #65 from adomokos/add_rvmrc_to_gitignore
Add the .rvmrc to .gitignore
2011-07-13 04:51:34 -07:00
Attila Domokos
c401ffb9c3 Add the .rvmrc to .gitignore 2011-07-13 04:18:19 -04:00
Jonathan Rudenberg
2434c3d0e0 Remove duplicate commas from StrictQuotedFragment. Closes #26. 2011-07-12 09:18:46 -04:00
Tobias Lütke
352f83a9d2 Merge pull request #28 from mcr/mcr_inc_tag
increment tag
2011-07-01 11:48:13 -07:00
Tobias Lütke
98c96ed86a Merge pull request #60 from a-team/master
Avoid creating singleton classes on nil, true and false objects
2011-07-01 11:43:27 -07:00
Tobias Lütke
410cce9740 Merge pull request #56 from oozou/for-else
For-else !!!
2011-07-01 11:42:17 -07:00
Jonathan Rudenberg
a8ed72a036 Add build status and small copy changes in the README 2011-06-30 16:36:49 -04:00
Jonathan Rudenberg
4aaf750fa8 Don't mess with the load path 2011-06-30 16:31:37 -04:00
Jonathan Rudenberg
e5a3d67a32 Add Rubinius to Travis runs 2011-06-30 16:25:55 -04:00
Jonathan Rudenberg
fe25644726 Rescue NameError for Rubinius 2011-06-30 16:24:17 -04:00
Jonathan Rudenberg
c47eac1683 Add kind_of? and singleton_methods to Strainer so that Rubinius works 2011-06-30 16:17:31 -04:00
Jonathan Rudenberg
c10a40f1fa Ignore rubinius bytecode files 2011-06-30 16:16:35 -04:00
Jonathan Rudenberg
e4003a74a1 Remove failing Rubies from Travis run for now 2011-06-29 15:08:44 -04:00
Jonathan Rudenberg
03065274d8 Use Travis CI 2011-06-29 11:06:20 -04:00
Jonathan Rudenberg
00afc9dd8a Merge pull request #62 from rmetzler/master
code blocks in README.md displayed properly on github (closes #61)
2011-06-19 20:48:03 -07:00
Richard Metzler
6bfdda9e17 html highlighting 2011-06-19 03:50:52 -07:00
Richard Metzler
00da0b6a42 ruby highlighting 2011-06-19 03:50:08 -07:00
Richard Metzler
39174ccee6 code blocks in github flavored markdown 2011-06-19 03:49:37 -07:00
Tim Felgentreff
c7033ff4c8 Avoid creating singleton classes on nil, true and false objects. Instead extend their respective classes. Just because those objects are singletons in current Ruby implementations doesn't mean we should rely on that. Fixes liquid on Maglev. 2011-06-13 18:13:47 +02:00
Jonathan Rudenberg
f85bea2902 Remove literal tag (raw is more performant)
This reverts commit c00a650492.
2011-06-13 09:34:15 -04:00
Jonathan Rudenberg
17922273e1 Merge old Shopify/liquid 2011-06-13 09:31:31 -04:00
Jonathan Rudenberg
4087a94d88 Add raw tag. Fixes #37.
Thanks to phaer: https://gist.github.com/1020852

YO DAWG, WE HEARD YOU LIKE LIQUID

SO WE PUT A RAW TAG IN YOUR LIQUID
SO YOU CAN HAVE LIQUID IN YOUR LIQUID
2011-06-12 11:15:16 -04:00
Steven Soroka
1a4ff9547a render_all should internally always return strings. This eases some 1.9 compatability issues. 2011-05-02 10:58:27 -04:00
Steven Soroka
888cbe8f09 fix next if /^__.+__$/ to /^(__.+__|object_id)$/, since object_id will complain if it is removed 2011-04-28 15:24:50 -05:00
Steven Soroka
2f11364417 make ruby-debug optional 2011-04-28 15:18:59 -05:00
Steven Soroka
74cdfc6718 Fix commit 7bbb4ff84f so it's backwards-compatible 2011-04-28 15:18:51 -05:00
Prathan Thananart
d19213177a Added documentation for for-else 2011-04-28 13:11:13 +07:00
Prathan Thananart
caf59940d3 Added code to make for-else work 2011-04-28 13:07:41 +07:00
Prathan Thananart
8319d78c2e Added a failing test case for for-else 2011-04-28 12:00:21 +07:00
Tobias Lütke
bc55c4d348 Merged pull request #34 from claco/date-epoch.
Epoch support
2011-04-25 09:03:30 -07:00
Tobias Lütke
017f0dc342 Merged pull request #53 from mhw/fix-stacklevelerror-test.
Fix behaviour of Context#stack when Context#push raises.
2011-04-25 09:00:00 -07:00
Tobias Lütke
ab556cbdd9 Merged pull request #54 from mhw/fix-drop-numeric-parameter.
Fix passing numeric parameter to drops
2011-04-25 08:59:10 -07:00
Tobias Lütke
82e4904403 Merged pull request #55 from ssoroka/master.
fixes failing test
2011-04-25 08:58:13 -07:00
Steven Soroka
59a63e0fe5 fix failing test "recursively_included_template_does_not_produce_endless_loop", push needs to come before exception raise since pop is in ensure block and always happens. 2011-04-24 01:09:56 -05:00
Mark H. Wilkinson
aafb3ed9f2 Fix behaviour of Context#stack when Context#push raises.
We only want to pop the scope at the end of the method if pushing it
succeeded, so the push needs to be outside the block with the ensure.
2011-04-12 14:24:26 +01:00
Mark H. Wilkinson
935d3530af Handle invoking drops for keys that are not strings.
For example, {{ pages[8] ... }} will result in the integer value 8 being
passed to invoke_drop.
2011-04-12 14:11:06 +01:00
Mark H. Wilkinson
662b2983fe Failing test for integer drop lookup. 2011-04-12 14:09:43 +01:00
Mark H. Wilkinson
37a0fe213b Fix text method name clash. 2011-04-12 13:47:27 +01:00
David Turnbull
7bbb4ff84f Context is no longer lost when moving into a Liquid FileSystem. 2011-01-28 21:19:44 -05:00
Tobias Lütke
9926d86c91 Fix rake file, add rake benchmark:run 2011-01-28 21:18:07 -05:00
Tobias Lütke
9c49b8bbb2 improved benchmark suite 2011-01-28 21:16:22 -05:00
thedarkone
f7ff9c81d3 Make use of the Module#public_method_defined?. 2011-01-28 21:16:22 -05:00
thedarkone
a65c4f51bc These are strings already. 2011-01-28 21:16:22 -05:00
thedarkone
a24049906b Resolve literals faster. 2011-01-28 21:16:22 -05:00
thedarkone
aa678302d6 Fix comments. 2011-01-28 21:16:22 -05:00
thedarkone
6d0bb3303c Clean-up Context#stack. 2011-01-28 21:16:22 -05:00
Austin Mills
4fec29f288 OPS-250 Modifying liquid error handling so that it no longer captures Exceptions that are not subclasses of StandardError (previously, it was rescuing things like Interrupt and NoMemoryError). 2011-01-10 14:55:05 -06:00
Christopher H. Laco
33ecb29d49 Update date to accept epoch 2010-11-07 20:41:41 -05:00
Michael Richardson
58e5820e7a thought that these were turned off, removed 2010-09-03 10:54:29 -04:00
Michael Richardson
bb035d96e1 ignore backups 2010-09-02 09:46:21 -04:00
Michael Richardson
3919ff6bc3 renamed inc -> increment
added decrement as well (pre-decremented)
2010-09-02 09:41:43 -04:00
Michael Richardson
381b4f5268 adjusted test case to have third argument 2010-08-31 19:42:13 -04:00
Michael Richardson
97f18112b2 the variables should be stored in the normal environment, not in the register 2010-08-31 17:26:41 -04:00
Michael Richardson
ca2fa587cf added tag to increment a variable each time it is referenced 2010-08-31 16:17:12 -04:00
87 changed files with 4473 additions and 1395 deletions

5
.gitignore vendored
View File

@@ -1,3 +1,8 @@
*~
*.gem
*.swp
pkg
*.rbc
.rvmrc
*.o
*.bundle

13
.travis.yml Normal file
View File

@@ -0,0 +1,13 @@
rvm:
- 1.8.7
- 1.9.3
- ree
- jruby-18mode
- jruby-19mode
- rbx-18mode
- rbx-19mode
script: "rake test"
notifications:
disable: true

View File

@@ -1,46 +0,0 @@
* Make context and assign work the same
* Ruby 1.9.1 bugfixes
* Fix LiquidView for Rails 2.2. Fix local assigns for all versions of Rails
* Fixed gem install rake task
* Improve Error encapsulation in liquid by maintaining a own set of exceptions instead of relying on ruby build ins
* Added If with or / and expressions
* Implemented .to_liquid for all objects which can be passed to liquid like Strings Arrays Hashes Numerics and Booleans. To export new objects to liquid just implement .to_liquid on them and return objects which themselves have .to_liquid methods.
* Added more tags to standard library
* Added include tag ( like partials in rails )
* [...] Gazillion of detail improvements
* Added strainers as filter hosts for better security [Tobias Luetke]
* Fixed that rails integration would call filter with the wrong "self" [Michael Geary]
* Fixed bad error reporting when a filter called a method which doesn't exist. Liquid told you that it couldn't find the filter which was obviously misleading [Tobias Luetke]
* Removed count helper from standard lib. use size [Tobias Luetke]
* Fixed bug with string filter parameters failing to tolerate commas in strings. [Paul Hammond]
* Improved filter parameters. Filter parameters are now context sensitive; Types are resolved according to the rules of the context. Multiple parameters are now separated by the Liquid::ArgumentSeparator: , by default [Paul Hammond]
{{ 'Typo' | link_to: 'http://typo.leetsoft.com', 'Typo - a modern weblog engine' }}
* Added Liquid::Drop. A base class which you can use for exporting proxy objects to liquid which can acquire more data when used in liquid. [Tobias Luetke]
class ProductDrop < Liquid::Drop
def top_sales
Shop.current.products.find(:all, :order => 'sales', :limit => 10 )
end
end
t = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {% endfor %} ' )
t.render('product' => ProductDrop.new )
* Added filter parameters support. Example: {{ date | format_date: "%Y" }} [Paul Hammond]

View File

@@ -1,53 +1,67 @@
2.2.1 / 2010-08-23
# Liquid Version History
## 2.4.0 / 2012-08-03
* Performance improvements
* Allow filters in `assign`
* Add `modulo` filter
* Ruby 1.8, 1.9, and Rubinius compatibility fixes
* Add support for `quoted['references']` in `tablerow`
* Add support for Enumerable to `tablerow`
* `strip_html` filter removes html comments
## 2.3.0 / 2011-10-16
* Several speed/memory improvements
* Numerous bug fixes
* Added support for MRI 1.9, Rubinius, and JRuby
* Added support for integer drop parameters
* Added epoch support to `date` filter
* New `raw` tag that suppresses parsing
* Added `else` option to `for` tag
* New `increment` tag
* New `split` filter
## 2.2.1 / 2010-08-23
* Added support for literal tags
2.2.0 / 2010-08-22
## 2.2.0 / 2010-08-22
* Compatible with Ruby 1.8.7, 1.9.1 and 1.9.2-p0
* Merged some changed made by the community
1.9.0 / 2008-03-04
## 1.9.0 / 2008-03-04
* Fixed gem install rake task
* Improve Error encapsulation in liquid by maintaining a own set of exceptions instead of relying on ruby build ins
Before 1.9.0
## Before 1.9.0
* Added If with or / and expressions
* Implemented .to_liquid for all objects which can be passed to liquid like Strings Arrays Hashes Numerics and Booleans. To export new objects to liquid just implement .to_liquid on them and return objects which themselves have .to_liquid methods.
* Implemented .to_liquid for all objects which can be passed to liquid like Strings Arrays Hashes Numerics and Booleans. To export new objects to liquid just implement .to_liquid on them and return objects which themselves have .to_liquid methods.
* Added more tags to standard library
* Added include tag ( like partials in rails )
* [...] Gazillion of detail improvements
* Added strainers as filter hosts for better security [Tobias Luetke]
* Fixed that rails integration would call filter with the wrong "self" [Michael Geary]
* Fixed bad error reporting when a filter called a method which doesn't exist. Liquid told you that it couldn't find the filter which was obviously misleading [Tobias Luetke]
* Removed count helper from standard lib. use size [Tobias Luetke]
* Fixed bug with string filter parameters failing to tolerate commas in strings. [Paul Hammond]
* Improved filter parameters. Filter parameters are now context sensitive; Types are resolved according to the rules of the context. Multiple parameters are now separated by the Liquid::ArgumentSeparator: , by default [Paul Hammond]
{{ 'Typo' | link_to: 'http://typo.leetsoft.com', 'Typo - a modern weblog engine' }}
* Added Liquid::Drop. A base class which you can use for exporting proxy objects to liquid which can acquire more data when used in liquid. [Tobias Luetke]
{{ 'Typo' | link_to: 'http://typo.leetsoft.com', 'Typo - a modern weblog engine' }}
* Added Liquid::Drop. A base class which you can use for exporting proxy objects to liquid which can acquire more data when used in liquid. [Tobias Luetke]
class ProductDrop < Liquid::Drop
def top_sales
Shop.current.products.find(:all, :order => 'sales', :limit => 10 )
end
end
end
t = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {% endfor %} ' )
t.render('product' => ProductDrop.new )
* Added filter parameters support. Example: {{ date | format_date: "%Y" }} [Paul Hammond]

View File

@@ -1,34 +0,0 @@
CHANGELOG
History.txt
MIT-LICENSE
Manifest.txt
README.md
Rakefile
init.rb
lib/extras/liquid_view.rb
lib/liquid.rb
lib/liquid/block.rb
lib/liquid/condition.rb
lib/liquid/context.rb
lib/liquid/document.rb
lib/liquid/drop.rb
lib/liquid/errors.rb
lib/liquid/extensions.rb
lib/liquid/file_system.rb
lib/liquid/htmltags.rb
lib/liquid/module_ex.rb
lib/liquid/standardfilters.rb
lib/liquid/strainer.rb
lib/liquid/tag.rb
lib/liquid/tags/assign.rb
lib/liquid/tags/capture.rb
lib/liquid/tags/case.rb
lib/liquid/tags/comment.rb
lib/liquid/tags/cycle.rb
lib/liquid/tags/for.rb
lib/liquid/tags/if.rb
lib/liquid/tags/ifchanged.rb
lib/liquid/tags/include.rb
lib/liquid/tags/unless.rb
lib/liquid/template.rb
lib/liquid/variable.rb

View File

@@ -2,41 +2,43 @@
## Introduction
Liquid is a template engine which I wrote for very specific requirements
Liquid is a template engine which was written with very specific requirements:
* It has to have beautiful and simple markup. Template engines which don't produce good looking markup are no fun to use.
* It needs to be non evaling and secure. Liquid templates are made so that users can edit them. You don't want to run code on your server which your users wrote.
* It has to be stateless. Compile and render steps have to be seperate so that the expensive parsing and compiling can be done once and later on you can just render it passing in a hash with local variables and objects.
* It has to be stateless. Compile and render steps have to be separate so that the expensive parsing and compiling can be done once and later on you can just render it passing in a hash with local variables and objects.
## Why should I use Liquid
## Why you should use Liquid
* You want to allow your users to edit the appearance of your application but don't want them to run **insecure code on your server**.
* You want to render templates directly from the database
* You like smarty (PHP) style template engines
* You need a template engine which does HTML just as well as emails
* You don't like the markup of your current templating engine
* You want to render templates directly from the database.
* You like smarty (PHP) style template engines.
* You need a template engine which does HTML just as well as emails.
* You don't like the markup of your current templating engine.
## What does it look like?
<code>
<ul id="products">
{% for product in products %}
<li>
<h2>{{product.name}}</h2>
Only {{product.price | price }}
```html
<ul id="products">
{% for product in products %}
<li>
<h2>{{ product.name }}</h2>
Only {{ product.price | price }}
{{product.description | prettyprint | paragraph }}
</li>
{% endfor %}
</ul>
</code>
{{ product.description | prettyprint | paragraph }}
</li>
{% endfor %}
</ul>
```
## Howto use Liquid
## How to use Liquid
Liquid supports a very simple API based around the Liquid::Template class.
For standard use you can just pass it the content of a file and call render with a parameters hash.
<pre>
```ruby
@template = Liquid::Template.parse("hi {{name}}") # Parses and compiles the template
@template.render( 'name' => 'tobi' ) # => "hi tobi"
</pre>
@template.render('name' => 'tobi') # => "hi tobi"
```
[![Build Status](https://secure.travis-ci.org/Shopify/liquid.png)](http://travis-ci.org/Shopify/liquid)

View File

@@ -1,43 +1,91 @@
#!/usr/bin/env ruby
$:.unshift File.join(File.dirname(__FILE__), 'test') unless $:.include? File.join(File.dirname(__FILE__), 'test')
require 'rubygems'
require 'rake'
require 'rake/clean'
require 'fileutils'
require 'rake/testtask'
require 'rake/gempackagetask'
require 'rubygems/package_task'
task :default => 'test'
task :default => [:compile, :test]
Rake::TestTask.new(:test) do |t|
task :ragel do
sh "find . -name '*.rl' | xargs ragel -C -G2"
end
task :compile => [:ragel, :liquid_ext]
extension = "liquid_ext"
ext = "ext/liquid"
ext_so = "#{ext}/#{extension}.#{RbConfig::CONFIG['DLEXT']}"
ext_files = FileList[
"#{ext}/*.c",
"#{ext}/*.h",
"#{ext}/*.rl",
"#{ext}/extconf.rb",
"#{ext}/Makefile",
"lib"
]
task "lib" do
directory "lib"
end
desc "Builds just the #{extension} extension"
task extension.to_sym => [:ragel, "#{ext}/Makefile", ext_so ]
file "#{ext}/Makefile" => ["#{ext}/extconf.rb"] do
Dir.chdir(ext) do ruby "extconf.rb" end
end
file ext_so => ext_files do
Dir.chdir(ext) do
sh "make"
end
cp ext_so, "lib"
end
Rake::TestTask.new(:test => [:ragel, 'liquid_ext']) do |t|
t.libs << '.' << 'lib' << 'test'
t.pattern = 'test/lib/**/*_test.rb'
t.test_files = FileList['test/liquid/**/*_test.rb']
t.verbose = false
end
gemspec = eval(File.read('liquid.gemspec'))
Rake::GemPackageTask.new(gemspec) do |pkg|
Gem::PackageTask.new(gemspec) do |pkg|
pkg.gem_spec = gemspec
end
desc "build the gem and release it to rubygems.org"
desc "Build the gem and release it to rubygems.org"
task :release => :gem do
sh "gem push pkg/liquid-#{gemspec.version}.gem"
end
namespace :benchmark do
desc "Run the liquid benchmark"
task :run do
ruby "./performance/benchmark.rb"
end
end
namespace :profile do
task :default => [:run]
desc "Run the liquid profile/perforamce coverage"
desc "Run the liquid profile/performance coverage"
task :run do
ruby "performance/shopify.rb"
ruby "./performance/profile.rb"
end
desc "Run KCacheGrind"
task :grind => :run do
system "kcachegrind /tmp/liquid.rubyprof_calltreeprinter.txt"
system "qcachegrind /tmp//callgrind.liquid.txt"
end
end
desc "Run example"
task :example do
ruby "-w -d -Ilib example/server/server.rb"
end

View File

@@ -25,7 +25,11 @@ class Servlet < LiquidServlet
def products
{ 'products' => products_list, 'section' => 'Snowboards', 'cool_products' => true}
end
def description
"List of Products ~ This is a list of products with price and description."
end
private
def products_list
@@ -34,4 +38,4 @@ class Servlet < LiquidServlet
{'name' => 'Arbor Diamond', 'price' => 59900, 'description' => 'the *arbor diamond* is a made up product because im obsessed with arbor and have no creativity'}]
end
end
end

View File

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

View File

@@ -16,8 +16,12 @@
</head>
<body>
<h1>There are currently {{products | count}} products in the {{section}} catalog</h1>
<h1>{{ description | split: '~' | first }}</h1>
<h2>{{ description | split: '~' | last }}</h2>
<h2>There are currently {{products | count}} products in the {{section}} catalog</h2>
{% if cool_products %}
Cool products :)

213
ext/liquid/Makefile Normal file
View File

@@ -0,0 +1,213 @@
SHELL = /bin/sh
# V=0 quiet, V=1 verbose. other values don't work.
V = 0
Q1 = $(V:1=)
Q = $(Q1:0=@)
n=$(NULLCMD)
ECHO1 = $(V:1=@$n)
ECHO = $(ECHO1:0=@echo)
#### Start of system configuration section. ####
srcdir = .
topdir = /Users/tobi/.rbenv/versions/1.9.3-p194-perf/include/ruby-1.9.1
hdrdir = /Users/tobi/.rbenv/versions/1.9.3-p194-perf/include/ruby-1.9.1
arch_hdrdir = /Users/tobi/.rbenv/versions/1.9.3-p194-perf/include/ruby-1.9.1/$(arch)
VPATH = $(srcdir):$(arch_hdrdir)/ruby:$(hdrdir)/ruby
prefix = $(DESTDIR)/Users/tobi/.rbenv/versions/1.9.3-p194-perf
exec_prefix = $(prefix)
rubylibprefix = $(libdir)/$(RUBY_BASE_NAME)
bindir = $(exec_prefix)/bin
sbindir = $(exec_prefix)/sbin
libexecdir = $(exec_prefix)/libexec
datarootdir = $(prefix)/share
datadir = $(datarootdir)
sysconfdir = $(prefix)/etc
sharedstatedir = $(prefix)/com
localstatedir = $(prefix)/var
includedir = $(prefix)/include
oldincludedir = $(DESTDIR)/usr/include
docdir = $(datarootdir)/doc/$(PACKAGE)
infodir = $(datarootdir)/info
htmldir = $(docdir)
dvidir = $(docdir)
pdfdir = $(docdir)
psdir = $(docdir)
libdir = $(exec_prefix)/lib
localedir = $(datarootdir)/locale
mandir = $(datarootdir)/man
ridir = $(datarootdir)/$(RI_BASE_NAME)
sitedir = $(rubylibprefix)/site_ruby
vendordir = $(rubylibprefix)/vendor_ruby
rubyhdrdir = $(includedir)/$(RUBY_BASE_NAME)-$(ruby_version)
sitehdrdir = $(rubyhdrdir)/site_ruby
vendorhdrdir = $(rubyhdrdir)/vendor_ruby
rubylibdir = $(rubylibprefix)/$(ruby_version)
archdir = $(rubylibdir)/$(arch)
sitelibdir = $(sitedir)/$(ruby_version)
sitearchdir = $(sitelibdir)/$(sitearch)
vendorlibdir = $(vendordir)/$(ruby_version)
vendorarchdir = $(vendorlibdir)/$(sitearch)
NULLCMD = :
CC = /usr/bin/gcc-4.2
CXX = g++
LIBRUBY = $(LIBRUBY_A)
LIBRUBY_A = lib$(RUBY_SO_NAME)-static.a
LIBRUBYARG_SHARED =
LIBRUBYARG_STATIC = -l$(RUBY_SO_NAME)-static
OUTFLAG = -o
COUTFLAG = -o
RUBY_EXTCONF_H =
cflags = $(optflags) $(debugflags) $(warnflags)
optflags = -O3
debugflags = -ggdb
warnflags = -Wextra -Wno-unused-parameter -Wno-parentheses -Wno-long-long -Wno-missing-field-initializers -Wpointer-arith -Wwrite-strings -Wdeclaration-after-statement -Wshorten-64-to-32 -Wimplicit-function-declaration
CFLAGS = -fno-common $(cflags) -pipe $(ARCH_FLAG)
INCFLAGS = -I. -I$(arch_hdrdir) -I$(hdrdir)/ruby/backward -I$(hdrdir) -I$(srcdir)
DEFS =
CPPFLAGS = -I'/Users/tobi/.rbenv/versions/1.9.3-p194-perf/include' -D_XOPEN_SOURCE -D_DARWIN_C_SOURCE $(DEFS) $(cppflags)
CXXFLAGS = $(CFLAGS) $(cxxflags)
ldflags = -L. -L'/Users/tobi/.rbenv/versions/1.9.3-p194-perf/lib' -L/usr/local/lib
dldflags = -Wl,-undefined,dynamic_lookup -Wl,-multiply_defined,suppress -Wl,-flat_namespace
ARCH_FLAG =
DLDFLAGS = $(ldflags) $(dldflags) $(ARCH_FLAG)
LDSHARED = $(CC) -dynamic -bundle
LDSHAREDXX = $(CXX) -dynamic -bundle
AR = ar
EXEEXT =
RUBY_BASE_NAME = ruby
RUBY_INSTALL_NAME = ruby
RUBY_SO_NAME = ruby
arch = x86_64-darwin12.0.0
sitearch = $(arch)
ruby_version = 1.9.1
ruby = /Users/tobi/.rbenv/versions/1.9.3-p194-perf/bin/ruby
RUBY = $(ruby)
RM = rm -f
RM_RF = $(RUBY) -run -e rm -- -rf
RMDIRS = rmdir -p
MAKEDIRS = mkdir -p
INSTALL = /usr/bin/install -c
INSTALL_PROG = $(INSTALL) -m 0755
INSTALL_DATA = $(INSTALL) -m 644
COPY = cp
#### End of system configuration section. ####
preload =
libpath = . $(libdir)
LIBPATH = -L. -L$(libdir)
DEFFILE =
CLEANFILES = mkmf.log
DISTCLEANFILES =
DISTCLEANDIRS =
extout =
extout_prefix =
target_prefix =
LOCAL_LIBS =
LIBS = -lc -lpthread -ldl -lobjc
SRCS = liquid_ext.c
OBJS = liquid_ext.o
TARGET = liquid_ext
DLLIB = $(TARGET).bundle
EXTSTATIC =
STATIC_LIB =
BINDIR = $(bindir)
RUBYCOMMONDIR = $(sitedir)$(target_prefix)
RUBYLIBDIR = $(sitelibdir)$(target_prefix)
RUBYARCHDIR = $(sitearchdir)$(target_prefix)
HDRDIR = $(rubyhdrdir)/ruby$(target_prefix)
ARCHHDRDIR = $(rubyhdrdir)/$(arch)/ruby$(target_prefix)
TARGET_SO = $(DLLIB)
CLEANLIBS = $(TARGET).bundle
CLEANOBJS = *.o *.bak
all: $(DLLIB)
static: $(STATIC_LIB)
.PHONY: all install static install-so install-rb
.PHONY: clean clean-so clean-rb
clean-rb-default::
clean-rb::
clean-so::
clean: clean-so clean-rb-default clean-rb
@-$(RM) $(CLEANLIBS) $(CLEANOBJS) $(CLEANFILES)
distclean-rb-default::
distclean-rb::
distclean-so::
distclean: clean distclean-so distclean-rb-default distclean-rb
@-$(RM) Makefile $(RUBY_EXTCONF_H) conftest.* mkmf.log
@-$(RM) core ruby$(EXEEXT) *~ $(DISTCLEANFILES)
@-$(RMDIRS) $(DISTCLEANDIRS) 2> /dev/null || true
realclean: distclean
install: install-so install-rb
install-so: $(RUBYARCHDIR)
install-so: $(RUBYARCHDIR)/$(DLLIB)
$(RUBYARCHDIR)/$(DLLIB): $(DLLIB)
@-$(MAKEDIRS) $(@D)
$(INSTALL_PROG) $(DLLIB) $(@D)
install-rb: pre-install-rb install-rb-default
install-rb-default: pre-install-rb-default
pre-install-rb: Makefile
pre-install-rb-default: Makefile
pre-install-rb-default:
$(ECHO) installing default liquid_ext libraries
$(RUBYARCHDIR):
$(Q) $(MAKEDIRS) $@
site-install: site-install-so site-install-rb
site-install-so: install-so
site-install-rb: install-rb
.SUFFIXES: .c .m .cc .mm .cxx .cpp .C .o
.cc.o:
$(ECHO) compiling $(<)
$(Q) $(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) $(COUTFLAG)$@ -c $<
.mm.o:
$(ECHO) compiling $(<)
$(Q) $(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) $(COUTFLAG)$@ -c $<
.cxx.o:
$(ECHO) compiling $(<)
$(Q) $(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) $(COUTFLAG)$@ -c $<
.cpp.o:
$(ECHO) compiling $(<)
$(Q) $(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) $(COUTFLAG)$@ -c $<
.C.o:
$(ECHO) compiling $(<)
$(Q) $(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) $(COUTFLAG)$@ -c $<
.c.o:
$(ECHO) compiling $(<)
$(Q) $(CC) $(INCFLAGS) $(CPPFLAGS) $(CFLAGS) $(COUTFLAG)$@ -c $<
.m.o:
$(ECHO) compiling $(<)
$(Q) $(CC) $(INCFLAGS) $(CPPFLAGS) $(CFLAGS) $(COUTFLAG)$@ -c $<
$(DLLIB): $(OBJS) Makefile
$(ECHO) linking shared-object $(DLLIB)
@-$(RM) $(@)
$(Q) $(LDSHARED) -o $@ $(OBJS) $(LIBPATH) $(DLDFLAGS) $(LOCAL_LIBS) $(LIBS)
$(OBJS): $(hdrdir)/ruby.h $(hdrdir)/ruby/defines.h $(arch_hdrdir)/ruby/config.h

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

@@ -0,0 +1,6 @@
require 'mkmf'
dir_config("liquid_ext")
have_library("c", "main")
create_makefile("liquid_ext")

BIN
ext/liquid/liquid_ext.bundle Executable file

Binary file not shown.

2636
ext/liquid/liquid_ext.c Normal file

File diff suppressed because it is too large Load Diff

BIN
ext/liquid/liquid_ext.o Normal file

Binary file not shown.

159
ext/liquid/liquid_ext.rl Normal file
View File

@@ -0,0 +1,159 @@
/*
Parser for context#[] method. Generated through ragel from parser.rl
Only modify parser.rl. Run rake ragel afterwards to generate this file.
*/
#include <ruby.h>
%%{
machine fsm;
action mark {
mark = p;
}
action lookup {
EMIT("lookup", Qnil)
}
action call {
EMIT("call", Qnil)
}
action range {
EMIT("range", Qnil)
}
constants = ( "true" | "false" | "nil" | "null" );
# strings
string = "\"" any* "\"" | "'" any* "'";
# nothingness
nil = "nil" | "null" ;
# numbers
integer = ('+'|'-')? digit+;
float = ('+'|'-')? digit+ '.' digit+;
# simple values
primitive = (
integer >mark %{
EMIT("id", rb_funcall(rb_cObject, rb_intern("Integer"), 1, rb_str_new(mark, p - mark)));
} |
float >mark %{
EMIT("id", rb_funcall(rb_cObject, rb_intern("Float"), 1, rb_str_new(mark, p - mark)))
} |
nil %{ EMIT("id", Qnil) } |
"true" %{ EMIT("id", Qtrue) } |
"false" %{ EMIT("id", Qfalse) } |
string >mark %{ EMIT("id", rb_str_new(mark + 1, p - mark - 2)) }
);
entity = (
((alpha [A-Za-z0-9_\-]*) - (constants)) >mark %{
EMIT("id", rb_str_new(mark, p - mark))
EMIT("lookup", Qnil)
}
);
# Because of recursion we cannot immediatly resolve the content of this in
# the current grammar. We simply re-invoke the parser here to descend into
# the substring
recur = (
(any+ - ']') >mark %{
VALUE body = rb_str_new(mark, p - mark);
liquid_context_parse_impl(body, tokens);
}
);
expr = (
entity |
primitive |
"(" (primitive | entity) ".." (primitive | entity) <: ")" %range |
"[" recur "]" %lookup
);
hash_accessors = (
"[" recur "]" %call |
".first" %{
EMIT("buildin", rb_str_new2("first"))
} |
".last" %{
EMIT("buildin", rb_str_new2("last"))
} |
".size" %{
EMIT("buildin", rb_str_new2("size"))
} |
"." ((alpha [A-Za-z0-9_\-]*) - ("first"|"last"|"size")) >mark %{
EMIT("id", rb_str_new(mark, p - mark))
EMIT("call", Qnil)
}
);
main := (
expr <: (hash_accessors)*
);
}%%
%% write data nofinal;
// def self.emit(sym, data, tokens)
// puts "emitting: #{sym} -> #{data.inspect}" if $VERBOSE
// tokens.push [sym, data]
// end
#define EMIT(sym, data) rb_ary_push(tokens, rb_ary_new3(2, ID2SYM(rb_intern(sym)), data));
void liquid_context_parse_impl(VALUE text, VALUE tokens)
{
char *p;
char *pe;
char *eof;
char *mark;
int cs, res = 0;
if (RSTRING_LEN(text) <= 0) {
return;
}
mark = p = RSTRING_PTR(text);
eof = pe = RSTRING_PTR(text) + RSTRING_LEN(text);
%% write init;
%% write exec;
}
VALUE liquid_context_parse(VALUE self, VALUE text) {
VALUE tokens;
//printf("text: %s\n", RSTRING_PTR(text));
//Check_Type(text, T_STRING);
tokens = rb_ary_new();
liquid_context_parse_impl(text, tokens);
return tokens;
}
static VALUE rb_Liquid;
static VALUE rb_Parser;
void Init_liquid_ext()
{
rb_Liquid = rb_define_module("Liquid");
rb_Parser = rb_define_class_under(rb_Liquid, "Parser", rb_cObject);
rb_define_singleton_method(rb_Parser, "parse", liquid_context_parse, 1);
}

22
ext/liquid/mkmf.log Normal file
View File

@@ -0,0 +1,22 @@
have_library: checking for main() in -lc... -------------------- yes
"/usr/bin/gcc-4.2 -o conftest -I/Users/tobi/.rbenv/versions/1.9.3-p194-perf/include/ruby-1.9.1/x86_64-darwin12.0.0 -I/Users/tobi/.rbenv/versions/1.9.3-p194-perf/include/ruby-1.9.1/ruby/backward -I/Users/tobi/.rbenv/versions/1.9.3-p194-perf/include/ruby-1.9.1 -I. -I'/Users/tobi/.rbenv/versions/1.9.3-p194-perf/include' -D_XOPEN_SOURCE -D_DARWIN_C_SOURCE -I'/Users/tobi/.rbenv/versions/1.9.3-p194-perf/include' -O3 -ggdb -Wextra -Wno-unused-parameter -Wno-parentheses -Wno-long-long -Wno-missing-field-initializers -Wpointer-arith -Wwrite-strings -Wdeclaration-after-statement -Wshorten-64-to-32 -Wimplicit-function-declaration -pipe conftest.c -L. -L/Users/tobi/.rbenv/versions/1.9.3-p194-perf/lib -L. -L'/Users/tobi/.rbenv/versions/1.9.3-p194-perf/lib' -L/usr/local/lib -lruby-static -lpthread -ldl -lobjc "
checked program was:
/* begin */
1: #include "ruby.h"
2:
3: int main() {return 0;}
/* end */
"/usr/bin/gcc-4.2 -o conftest -I/Users/tobi/.rbenv/versions/1.9.3-p194-perf/include/ruby-1.9.1/x86_64-darwin12.0.0 -I/Users/tobi/.rbenv/versions/1.9.3-p194-perf/include/ruby-1.9.1/ruby/backward -I/Users/tobi/.rbenv/versions/1.9.3-p194-perf/include/ruby-1.9.1 -I. -I'/Users/tobi/.rbenv/versions/1.9.3-p194-perf/include' -D_XOPEN_SOURCE -D_DARWIN_C_SOURCE -I'/Users/tobi/.rbenv/versions/1.9.3-p194-perf/include' -O3 -ggdb -Wextra -Wno-unused-parameter -Wno-parentheses -Wno-long-long -Wno-missing-field-initializers -Wpointer-arith -Wwrite-strings -Wdeclaration-after-statement -Wshorten-64-to-32 -Wimplicit-function-declaration -pipe conftest.c -L. -L/Users/tobi/.rbenv/versions/1.9.3-p194-perf/lib -L. -L'/Users/tobi/.rbenv/versions/1.9.3-p194-perf/lib' -L/usr/local/lib -lruby-static -lc -lpthread -ldl -lobjc "
checked program was:
/* begin */
1: #include "ruby.h"
2:
3: /*top*/
4: int main() {return 0;}
5: int t() { void ((*volatile p)()); p = (void ((*)()))main; return 0; }
/* end */
--------------------

View File

@@ -19,8 +19,6 @@
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
$LOAD_PATH.unshift(File.dirname(__FILE__))
module Liquid
FilterSeparator = /\|/
ArgumentSeparator = ','
@@ -34,23 +32,23 @@ module Liquid
VariableEnd = /\}\}/
VariableIncompleteEnd = /\}\}?/
QuotedString = /"[^"]*"|'[^']*'/
QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/
StrictQuotedFragment = /"[^"]+"|'[^']+'|[^\s,\|,\:,\,]+/
FirstFilterArgument = /#{FilterArgumentSeparator}(?:#{StrictQuotedFragment})/
OtherFilterArgument = /#{ArgumentSeparator}(?:#{StrictQuotedFragment})/
SpacelessFilter = /^(?:'[^']+'|"[^"]+"|[^'"])*#{FilterSeparator}(?:#{StrictQuotedFragment})(?:#{FirstFilterArgument}(?:#{OtherFilterArgument})*)?/
Expression = /(?:#{QuotedFragment}(?:#{SpacelessFilter})*)/
TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/
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
AnyStartingTag = /\{\{|\{\%/
PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/
VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/
LiteralShorthand = /^(?:\{\{\{\s?)(.*?)(?:\s*\}\}\})$/
PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/o
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/o
VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o
end
require 'liquid/drop'
require 'liquid/extensions'
require 'liquid/errors'
require 'liquid/interrupts'
require 'liquid/strainer'
require 'liquid/context'
require 'liquid/tag'
@@ -63,6 +61,8 @@ require 'liquid/htmltags'
require 'liquid/standardfilters'
require 'liquid/condition'
require 'liquid/module_ex'
require 'liquid/utils'
require 'liquid_ext'
# Load all the tags of the standard library
#

View File

@@ -1,10 +1,10 @@
module Liquid
class Block < Tag
IsTag = /^#{TagStart}/
IsVariable = /^#{VariableStart}/
FullToken = /^#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}$/
ContentOfVariable = /^#{VariableStart}(.*)#{VariableEnd}$/
IsTag = /^#{TagStart}/o
IsVariable = /^#{VariableStart}/o
FullToken = /^#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}$/o
ContentOfVariable = /^#{VariableStart}(.*)#{VariableEnd}$/o
def parse(tokens)
@nodelist ||= []
@@ -89,13 +89,27 @@ module Liquid
end
def render_all(list, context)
list.collect do |token|
output = []
list.each do |token|
# Break out if we have any unhanded interrupts.
break if context.has_interrupt?
begin
token.respond_to?(:render) ? token.render(context) : token
rescue Exception => e
context.handle_error(e)
# If we get an Interrupt that means the block must stop processing. An
# Interrupt is any command that stops block execution such as {% break %}
# or {% continue %}
if token.is_a? Continue or token.is_a? Break
context.push_interrupt(token.interrupt)
break
end
output << (token.respond_to?(:render) ? token.render(context) : token)
rescue ::StandardError => e
output << (context.handle_error(e))
end
end
output
end
end
end

View File

@@ -1,5 +1,6 @@
module Liquid
# Context keeps the variable stack and resolves variables, as well as keywords
#
# context['variable'] = 'testing'
@@ -22,6 +23,8 @@ module Liquid
@errors = []
@rethrow_errors = rethrow_errors
squash_instance_assigns_with_environments
@interrupts = []
end
def strainer
@@ -41,6 +44,21 @@ module Liquid
end
end
# are there any not handled interrupts?
def has_interrupt?
!@interrupts.empty?
end
# push an interrupt to the stack. this interrupt is considered not handled.
def push_interrupt(e)
@interrupts.push(e)
end
# pop an interrupt from the stack
def pop_interrupt
@interrupts.pop
end
def handle_error(e)
errors.push(e)
raise if @rethrow_errors
@@ -63,8 +81,8 @@ module Liquid
# Push new local scope on the stack. use <tt>Context#stack</tt> instead
def push(new_scope={})
raise StackLevelError, "Nesting too deep" if @scopes.length > 100
@scopes.unshift(new_scope)
raise StackLevelError, "Nesting too deep" if @scopes.length > 100
end
# Merge a hash of variables in the current local scope
@@ -86,69 +104,100 @@ module Liquid
# end
#
# context['var] #=> nil
def stack(new_scope={},&block)
result = nil
def stack(new_scope={})
push(new_scope)
begin
result = yield
ensure
pop
end
result
yield
ensure
pop
end
def clear_instance_assigns
@scopes[0] = {}
end
# Look up variable, either resolve directly after considering the name. We can directly handle
# Strings, digits, floats and booleans (true,false).
# If no match is made we lookup the variable in the current scope and
# later move up to the parent blocks to see if we can resolve the variable somewhere up the tree.
# Some special keywords return symbols. Those symbols are to be called on the rhs object in expressions
#
# Example:
# products == empty #=> products.empty?
def resolve(key)
case key
when nil, ""
return nil
when "blank"
return :blank?
when "empty"
return :empty?
end
result = Parser.parse(key)
stack = []
result.each do |(sym, value)|
case sym
when :id
stack.push value
when :lookup
left = stack.pop
value = find_variable(left)
stack.push(harden(value))
when :range
right = stack.pop.to_i
left = stack.pop.to_i
stack.push (left..right)
when :buildin
left = stack.pop
value = invoke_buildin(left, value)
stack.push(harden(value))
when :call
left = stack.pop
right = stack.pop
value = lookup_and_evaluate(right, left)
stack.push(harden(value))
else
raise "unknown #{sym}"
end
end
return stack.first
end
# Only allow String, Numeric, Hash, Array, Proc, Boolean or <tt>Liquid::Drop</tt>
def []=(key, value)
@scopes[0][key] = value
end
def [](key)
resolve(key)
end
def has_key?(key)
resolve(key) != nil
end
alias_method :[], :resolve
private
# Look up variable, either resolve directly after considering the name. We can directly handle
# Strings, digits, floats and booleans (true,false).
# If no match is made we lookup the variable in the current scope and
# later move up to the parent blocks to see if we can resolve the variable somewhere up the tree.
# Some special keywords return symbols. Those symbols are to be called on the rhs object in expressions
#
# Example:
# products == empty #=> products.empty?
def resolve(key)
case key
when nil, 'nil', 'null', ''
nil
when 'true'
true
when 'false'
false
when 'blank'
:blank?
when 'empty' # Single quoted strings
:empty?
when /^'(.*)'$/ # Double quoted strings
$1.to_s
when /^"(.*)"$/ # Integer and floats
$1.to_s
when /^(\d+)$/ # Ranges
$1.to_i
when /^\((\S+)\.\.(\S+)\)$/ # Floats
(resolve($1).to_i..resolve($2).to_i)
when /^(\d[\d\.]+)$/
$1.to_f
def invoke_buildin(obj, key)
# as weird as this is, liquid unit tests demand that we prioritize hash lookups
# to buildins. So if we got a hash and it has a :first element we need to call that
# instead of sending the first message...
if obj.respond_to?(:has_key?) && obj.has_key?(key)
return lookup_and_evaluate(obj, key)
end
if obj.respond_to?(key)
return obj.send(key)
else
variable(key)
return nil
end
end
@@ -168,71 +217,34 @@ module Liquid
scope ||= @environments.last || @scopes.last
variable ||= lookup_and_evaluate(scope, key)
variable = variable.to_liquid
variable.context = self if variable.respond_to?(:context=)
return variable
end
# Resolves namespaced queries gracefully.
#
# Example
# @context['hash'] = {"name" => 'tobi'}
# assert_equal 'tobi', @context['hash.name']
# assert_equal 'tobi', @context['hash["name"]']
def variable(markup)
parts = markup.scan(VariableParser)
square_bracketed = /^\[(.*)\]$/
first_part = parts.shift
if first_part =~ square_bracketed
first_part = resolve($1)
end
if object = find_variable(first_part)
parts.each do |part|
part = resolve($1) if part_resolved = (part =~ square_bracketed)
# If object is a hash- or array-like object we look for the
# presence of the key and if its available we return it
if object.respond_to?(:[]) and
((object.respond_to?(:has_key?) and object.has_key?(part)) or
(object.respond_to?(:fetch) and part.is_a?(Integer)))
# if its a proc we will replace the entry with the proc
res = lookup_and_evaluate(object, part)
object = res.to_liquid
# Some special cases. If the part wasn't in square brackets and
# no key with the same name was found we interpret following calls
# as commands and call them on the current object
elsif !part_resolved and object.respond_to?(part) and ['size', 'first', 'last'].include?(part)
object = object.send(part.intern).to_liquid
# No key was present with the desired value and it wasn't one of the directly supported
# keywords either. The only thing we got left is to return nil
else
return nil
end
# If we are dealing with a drop here we have to
object.context = self if object.respond_to?(:context=)
end
end
object
end # variable
def lookup_and_evaluate(obj, key)
if (value = obj[key]).is_a?(Proc) && obj.respond_to?(:[]=)
obj[key] = (value.arity == 0) ? value.call : value.call(self)
else
value
return nil unless obj.respond_to?(:[])
if obj.is_a?(Array)
return nil unless key.is_a?(Integer)
end
end # lookup_and_evaluate
value = obj[key]
if value.is_a?(Proc)
# call the proc
value = (value.arity == 0) ? value.call : value.call(self)
# memozie if possible
obj[key] = value if obj.respond_to?(:[]=)
end
value
end
def harden(value)
value = value.to_liquid
value.context = self if value.respond_to?(:context=)
return value
end
def squash_instance_assigns_with_environments
@scopes.last.each_key do |k|
@@ -244,6 +256,7 @@ module Liquid
end
end
end # squash_instance_assigns_with_environments
end # Context
end # Liquid

View File

@@ -1,40 +1,40 @@
module Liquid
# A drop in liquid is a class which allows you to to export DOM like things to liquid
# A drop in liquid is a class which allows you to export DOM like things to liquid.
# Methods of drops are callable.
# The main use for liquid drops is the implement lazy loaded objects.
# The main use for liquid drops is to implement lazy loaded objects.
# If you would like to make data available to the web designers which you don't want loaded unless needed then
# a drop is a great way to do that
# a drop is a great way to do that.
#
# Example:
#
# class ProductDrop < Liquid::Drop
# def top_sales
# Shop.current.products.find(:all, :order => 'sales', :limit => 10 )
# class ProductDrop < Liquid::Drop
# def top_sales
# Shop.current.products.find(:all, :order => 'sales', :limit => 10 )
# end
# end
# end
#
# tmpl = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {%endfor%} ' )
# tmpl.render('product' => ProductDrop.new ) # will invoke top_sales query.
# tmpl = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {%endfor%} ' )
# tmpl.render('product' => ProductDrop.new ) # will invoke top_sales query.
#
# Your drop can either implement the methods sans any parameters or implement the before_method(name) method which is a
# catch all
# catch all.
class Drop
attr_writer :context
EMPTY_STRING = ''.freeze
# Catch all for the method
def before_method(method)
nil
end
# called by liquid to invoke a drop
def invoke_drop(method)
# for backward compatibility with Ruby 1.8
methods = self.class.public_instance_methods.map { |m| m.to_s }
if methods.include?(method.to_s)
send(method.to_sym)
def invoke_drop(method_or_key)
if method_or_key && method_or_key != EMPTY_STRING && self.class.public_method_defined?(method_or_key.to_s.to_sym)
send(method_or_key.to_s.to_sym)
else
before_method(method)
before_method(method_or_key)
end
end

View File

@@ -8,4 +8,4 @@ module Liquid
class StandardError < Error; end
class SyntaxError < Error; end
class StackLevelError < Error; end
end
end

View File

@@ -43,14 +43,20 @@ class Date # :nodoc:
end
end
def true.to_liquid # :nodoc:
self
class TrueClass
def to_liquid # :nodoc:
self
end
end
def false.to_liquid # :nodoc:
self
class FalseClass
def to_liquid # :nodoc:
self
end
end
def nil.to_liquid # :nodoc:
self
end
class NilClass
def to_liquid # :nodoc:
self
end
end

View File

@@ -14,7 +14,7 @@ module Liquid
# This will parse the template with a LocalFileSystem implementation rooted at 'template_path'.
class BlankFileSystem
# Called by Liquid to retrieve a template file
def read_template_file(template_path)
def read_template_file(template_path, context)
raise FileSystemError, "This liquid context does not allow includes."
end
end
@@ -38,7 +38,7 @@ module Liquid
@root = root
end
def read_template_file(template_path)
def read_template_file(template_path, context)
full_path = full_path(template_path)
raise FileSystemError, "No such template '#{template_path}'" unless File.exists?(full_path)

View File

@@ -1,11 +1,13 @@
module Liquid
class TableRow < Block
Syntax = /(\w+)\s+in\s+(#{VariableSignature}+)/
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
def initialize(tag_name, markup, tokens)
if markup =~ Syntax
@variable_name = $1
@collection_name = $2
@idx_i = "#{$1}-#{$2}-i"
@idx_col = "#{$1}-#{$2}-c"
@attributes = {}
markup.scan(TagAttributes) do |key, value|
@attributes[key] = value
@@ -18,13 +20,14 @@ module Liquid
end
def render(context)
context.registers[:tablerowloop] ||= Hash.new(0)
collection = context[@collection_name] or return ''
if @attributes['limit'] or @attributes['offset']
limit = context[@attributes['limit']] || -1
offset = context[@attributes['offset']] || 0
collection = collection[offset.to_i..(limit.to_i + offset.to_i - 1)]
end
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
@@ -36,38 +39,80 @@ module Liquid
result = ["<tr class=\"row1\">\n"]
context.stack do
context.registers[:tablerowloop][@idx]
context['tablerowloop'] = lambda { Tablerowloop.new(@idx_i, @idx_col, length) }
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)
}
context.registers[:tablerowloop][@idx_i] = index
context.registers[:tablerowloop][@idx_col] = col
context[@variable_name] = item
col += 1
result << ["<td class=\"col#{col}\">"] + render_all(@nodelist, context) + ['</td>']
result << "<td class=\"col#{col}\">" << render_all(@nodelist, context) << '</td>'
if col == cols and not (index == length - 1)
col = 0
row += 1
result << ["</tr>\n<tr class=\"row#{row}\">"]
result << "</tr>\n<tr class=\"row#{row}\">"
end
end
end
result + ["</tr>\n"]
result << "</tr>\n"
result
end
private
class Tablerowloop < Liquid::Drop
attr_accessor :length
def initialize(idx_i, idx_col, length)
@idx_i, @idx_col, @length = idx_i, idx_col, length
end
def index
@context.registers[:tablerowloop][@idx_i] + 1
end
def index0
@context.registers[:tablerowloop][@idx_i]
end
def rindex
length - @context.registers[:tablerowloop][@idx_i]
end
def rindex0
length - @context.registers[:tablerowloop][@idx_i] - 1
end
def first
(@context.registers[:tablerowloop][@idx_i] == 0)
end
def last
(@context.registers[:tablerowloop][@idx_i] == length - 1)
end
def col
@context.registers[:tablerowloop][@idx_col] + 1
end
def col0
@context.registers[:tablerowloop][@idx_col]
end
def col_first
(@context.registers[:tablerowloop][@idx_col] == 0)
end
def col_last
(@context.registers[:tablerowloop][@idx_col] == cols - 1)
end
end
end
Template.register_tag('tablerow', TableRow)

17
lib/liquid/interrupts.rb Normal file
View File

@@ -0,0 +1,17 @@
module Liquid
# An interrupt is any command that breaks processing of a block (ex: a for loop).
class Interrupt
attr_reader :message
def initialize(message=nil)
@message = message || "interrupt"
end
end
# Interrupt that is thrown whenever a {% break %} is called.
class BreakInterrupt < Interrupt; end
# Interrupt that is thrown whenever a {% continue %} is called.
class ContinueInterrupt < Interrupt; end
end

View File

@@ -30,7 +30,9 @@ module Liquid
end
def escape_once(input)
ActionView::Helpers::TagHelper.escape_once(input) rescue input
ActionView::Helpers::TagHelper.escape_once(input)
rescue NameError
input
end
alias_method :h, :escape
@@ -51,8 +53,17 @@ module Liquid
wordlist.length > l ? wordlist[0..l].join(" ") + truncate_string : input
end
# Split input string into an array of substrings separated by given pattern.
#
# Example:
# <div class="summary">{{ post | split '//' | first }}</div>
#
def split(input, pattern)
input.split(pattern)
end
def strip_html(input)
input.to_s.gsub(/<script.*?<\/script>/, '').gsub(/<.*?>/, '')
input.to_s.gsub(/<script.*?<\/script>/, '').gsub(/<!--.*?-->/, '').gsub(/<.*?>/, '')
end
# Remove all newlines from the string
@@ -158,6 +169,10 @@ module Liquid
return input.to_s
end
if ((input.is_a?(String) && !/^\d+$/.match(input.to_s).nil?) || input.is_a?(Integer)) && input.to_i > 0
input = Time.at(input.to_i)
end
date = input.is_a?(String) ? Time.parse(input) : input
if date.respond_to?(:strftime)
@@ -207,6 +222,10 @@ module Liquid
to_number(input) / to_number(operand)
end
def modulo(input, operand)
to_number(input) % to_number(operand)
end
private
def to_number(obj)

View File

@@ -14,7 +14,7 @@ module Liquid
# One of the strainer's responsibilities is to keep malicious method calls out
class Strainer < parent_object #:nodoc:
INTERNAL_METHOD = /^__/
@@required_methods = Set.new([:__id__, :__send__, :respond_to?, :extend, :methods, :class, :object_id])
@@required_methods = Set.new([:__id__, :__send__, :respond_to?, :kind_of?, :extend, :methods, :singleton_methods, :class, :object_id])
# Ruby 1.9.2 introduces Object#respond_to_missing?, which is invoked by Object#respond_to?
@@required_methods << :respond_to_missing? if Object.respond_to? :respond_to_missing?

View File

@@ -9,12 +9,12 @@ module Liquid
# {{ foo }}
#
class Assign < Tag
Syntax = /(#{VariableSignature}+)\s*=\s*(#{QuotedFragment}+)/
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/o
def initialize(tag_name, markup, tokens)
if markup =~ Syntax
@to = $1
@from = $2
@from = Variable.new($2)
else
raise SyntaxError.new("Syntax Error in 'assign' - Valid syntax: assign [var] = [source]")
end
@@ -23,7 +23,7 @@ module Liquid
end
def render(context)
context.scopes.last[@to] = context[@from]
context.scopes.last[@to] = @from.render(context)
''
end

21
lib/liquid/tags/break.rb Normal file
View File

@@ -0,0 +1,21 @@
module Liquid
# Break tag to be used to break out of a for loop.
#
# == Basic Usage:
# {% for item in collection %}
# {% if item.condition %}
# {% break %}
# {% endif %}
# {% endfor %}
#
class Break < Tag
def interrupt
BreakInterrupt.new
end
end
Template.register_tag('break', Break)
end

View File

@@ -26,7 +26,7 @@ module Liquid
def render(context)
output = super
context.scopes.last[@to] = output.join
context.scopes.last[@to] = output
''
end
end

View File

@@ -1,7 +1,7 @@
module Liquid
class Case < Block
Syntax = /(#{QuotedFragment})/
WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/
Syntax = /(#{QuotedFragment})/o
WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/o
def initialize(tag_name, markup, tokens)
@blocks = []
@@ -31,20 +31,16 @@ module Liquid
context.stack do
execute_else_block = true
@blocks.inject([]) do |output, block|
output = []
@blocks.each do |block|
if block.else?
return render_all(block.attachment, context) if execute_else_block
elsif block.evaluate(context)
execute_else_block = false
output += render_all(block.attachment, context)
output << render_all(block.attachment, context)
end
output
end
output
end
end

View File

@@ -0,0 +1,21 @@
module Liquid
# Continue tag to be used to break out of a for loop.
#
# == Basic Usage:
# {% for item in collection %}
# {% if item.condition %}
# {% continue %}
# {% endif %}
# {% endfor %}
#
class Continue < Tag
def interrupt
ContinueInterrupt.new
end
end
Template.register_tag('continue', Continue)
end

View File

@@ -13,8 +13,8 @@ module Liquid
# <div class="green"> Item five</div>
#
class Cycle < Tag
SimpleSyntax = /^#{QuotedFragment}+/
NamedSyntax = /^(#{QuotedFragment})\s*\:\s*(.*)/
SimpleSyntax = /^#{QuotedFragment}+/o
NamedSyntax = /^(#{QuotedFragment})\s*\:\s*(.*)/o
def initialize(tag_name, markup, tokens)
case markup
@@ -48,7 +48,7 @@ module Liquid
def variables_from_string(markup)
markup.split(',').collect do |var|
var =~ /\s*(#{QuotedFragment})\s*/
var =~ /\s*(#{QuotedFragment})\s*/o
$1 ? $1 : nil
end.compact
end
@@ -56,4 +56,4 @@ module Liquid
end
Template.register_tag('cycle', Cycle)
end
end

View File

@@ -0,0 +1,39 @@
module Liquid
# decrement is used in a place where one needs to insert a counter
# into a template, and needs the counter to survive across
# multiple instantiations of the template.
# NOTE: decrement is a pre-decrement, --i,
# while increment is post: i++.
#
# (To achieve the survival, the application must keep the context)
#
# if the variable does not exist, it is created with value 0.
# Hello: {% decrement variable %}
#
# gives you:
#
# Hello: -1
# Hello: -2
# Hello: -3
#
class Decrement < Tag
def initialize(tag_name, markup, tokens)
@variable = markup.strip
super
end
def render(context)
value = context.environments.first[@variable] ||= 0
value = value - 1
context.environments.first[@variable] = value
value.to_s
end
private
end
Template.register_tag('decrement', Decrement)
end

View File

@@ -13,6 +13,8 @@ module Liquid
# <div {% if forloop.first %}class="first"{% endif %}>
# Item {{ forloop.index }}: {{ item.name }}
# </div>
# {% else %}
# There is nothing in the collection.
# {% endfor %}
#
# You can also define a limit and offset much like SQL. Remember
@@ -42,13 +44,14 @@ module Liquid
# forloop.last:: Returns true if the item is the last item.
#
class For < Block
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
def initialize(tag_name, markup, tokens)
if markup =~ Syntax
@variable_name = $1
@collection_name = $2
@name = "#{$1}-#{$2}"
@name = "#{$1}-#{$2}"
@idx = "#{@name}-i"
@reversed = $3
@attributes = {}
markup.scan(TagAttributes) do |key, value|
@@ -58,16 +61,23 @@ module Liquid
raise SyntaxError.new("Syntax Error in 'for loop' - Valid syntax: for [item] in [collection]")
end
@nodelist = @for_block = []
super
end
def unknown_tag(tag, markup, tokens)
return super unless tag == 'else'
@nodelist = @else_block = []
end
def render(context)
def render(context)
context.registers[:for] ||= Hash.new(0)
collection = context[@collection_name]
collection = collection.to_a if collection.is_a?(Range)
return '' unless collection.respond_to?(:each)
# Maintains Ruby 1.8.7 String#each behaviour on 1.9
return render_else(context) unless iterable?(collection)
from = if @attributes['offset'] == 'continue'
context.registers[:for][@name].to_i
@@ -77,11 +87,10 @@ module Liquid
limit = context[@attributes['limit']]
to = limit ? limit.to_i + from : nil
segment = slice_collection_using_each(collection, from, to)
return '' if segment.empty?
segment = Utils.slice_collection_using_each(collection, from, to)
return render_else(context) if segment.empty?
segment.reverse! if @reversed
@@ -92,45 +101,68 @@ module Liquid
# Store our progress through the collection for the continue flag
context.registers[:for][@name] = from + segment.length
context.stack do
segment.each_with_index do |item, index|
context.stack do
context['forloop'] = lambda { Forloop.new(@name, @idx, length) }
segment.each_with_index do |item, index|
context.registers[:for][@idx] = index
context[@variable_name] = item
context['forloop'] = {
'name' => @name,
'length' => length,
'index' => index + 1,
'index0' => index,
'rindex' => length - index,
'rindex0' => length - index -1,
'first' => (index == 0),
'last' => (index == length - 1) }
result << render_all(@nodelist, context)
result << render_all(@for_block, context)
# Handle any interrupts if they exist.
if context.has_interrupt?
interrupt = context.pop_interrupt
break if interrupt.is_a? BreakInterrupt
next if interrupt.is_a? ContinueInterrupt
end
end
end
result
end
def slice_collection_using_each(collection, from, to)
segments = []
index = 0
yielded = 0
collection.each do |item|
if to && to <= index
break
end
if from <= index
segments << item
end
index += 1
end
segments
end
private
class Forloop < Liquid::Drop
attr_accessor :name, :length
def initialize(name, idx, length)
@name, @idx, @length = name, idx, length
end
def index
@context.registers[:for][@idx] + 1
end
def index0
@context.registers[:for][@idx]
end
def rindex
length - @context.registers[:for][@idx]
end
def rindex0
length - @context.registers[:for][@idx] - 1
end
def first
(@context.registers[:for][@idx] == 0)
end
def last
(@context.registers[:for][@idx] == length - 1)
end
end
def render_else(context)
return @else_block ? [render_all(@else_block, context)] : ''
end
def iterable?(collection)
collection.respond_to?(:each) || Utils.non_blank_string?(collection)
end
end
Template.register_tag('for', For)
end
end

View File

@@ -13,8 +13,8 @@ module Liquid
#
class If < Block
SyntaxHelp = "Syntax Error in tag 'if' - Valid syntax: if [expression]"
Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/
ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/
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
def initialize(tag_name, markup, tokens)
@blocks = []

View File

@@ -1,6 +1,6 @@
module Liquid
class Include < Tag
Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/
Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/o
def initialize(tag_name, markup, tokens)
if markup =~ Syntax
@@ -20,14 +20,12 @@ module Liquid
super
end
def parse(tokens)
def parse(tokens)
end
def render(context)
file_system = context.registers[:file_system] || Liquid::Template.file_system
source = file_system.read_template_file(context[@template_name])
partial = Liquid::Template.parse(source)
def render(context)
source = _read_template_from_file_system(context)
partial = Liquid::Template.parse(source)
variable = context[@variable_name || @template_name[1..-2]]
context.stack do
@@ -36,21 +34,32 @@ module Liquid
end
if variable.is_a?(Array)
variable.collect do |variable|
variable.collect do |variable|
context[@template_name[1..-2]] = variable
partial.render(context)
end
else
context[@template_name[1..-2]] = variable
partial.render(context)
end
end
end
private
def _read_template_from_file_system(context)
file_system = context.registers[:file_system] || Liquid::Template.file_system
# make read_template_file call backwards-compatible.
case file_system.method(:read_template_file).arity
when 1
file_system.read_template_file(context[@template_name])
when 2
file_system.read_template_file(context[@template_name], context)
else
raise ArgumentError, "file_system.read_template_file expects two parameters: (template_name, context)"
end
end
end
Template.register_tag('include', Include)
end
end

View File

@@ -0,0 +1,35 @@
module Liquid
# increment is used in a place where one needs to insert a counter
# into a template, and needs the counter to survive across
# multiple instantiations of the template.
# (To achieve the survival, the application must keep the context)
#
# if the variable does not exist, it is created with value 0.
# Hello: {% increment variable %}
#
# gives you:
#
# Hello: 0
# Hello: 1
# Hello: 2
#
class Increment < Tag
def initialize(tag_name, markup, tokens)
@variable = markup.strip
super
end
def render(context)
value = context.environments.first[@variable] ||= 0
context.environments.first[@variable] = value + 1
value.to_s
end
private
end
Template.register_tag('increment', Increment)
end

View File

@@ -1,42 +0,0 @@
module Liquid
class Literal < Block
# Class methods
# Converts a shorthand Liquid literal into its long representation.
#
# Currently the Template parser only knows how to handle the long version.
# So, it always checks if it is in the presence of a literal, in which case it gets converted through this method.
#
# Example:
# Liquid::Literal "{{{ hello world }}}" #=> "{% literal %} hello world {% endliteral %}"
def self.from_shorthand(literal)
literal =~ LiteralShorthand ? "{% literal %}#{$1}{% endliteral %}" : literal
end
# Public instance methods
def parse(tokens) # :nodoc:
@nodelist ||= []
@nodelist.clear
while token = tokens.shift
if token =~ FullToken && block_delimiter == $1
end_tag
return
else
@nodelist << token
end
end
# Make sure that its ok to end parsing in the current block.
# Effectively this method will throw and exception unless the current block is
# of type Document
assert_missing_delimitation!
end # parse
end
Template.register_tag('literal', Literal)
end

21
lib/liquid/tags/raw.rb Normal file
View File

@@ -0,0 +1,21 @@
module Liquid
class Raw < Block
def parse(tokens)
@nodelist ||= []
@nodelist.clear
while token = tokens.shift
if token =~ FullToken
if block_delimiter == $1
end_tag
return
end
end
@nodelist << token if not token.empty?
end
end
end
Template.register_tag('raw', Raw)
end

View File

@@ -13,19 +13,19 @@ module Liquid
# First condition is interpreted backwards ( if not )
block = @blocks.first
unless block.evaluate(context)
return render_all(block.attachment, context)
return render_all(block.attachment, context)
end
# After the first condition unless works just like if
@blocks[1..-1].each do |block|
if block.evaluate(context)
return render_all(block.attachment, context)
if block.evaluate(context)
return render_all(block.attachment, context)
end
end
end
''
end
end
end
end

View File

@@ -55,7 +55,7 @@ module Liquid
# Parse source code.
# Returns self for easy chaining
def parse(source)
@root = Document.new(tokenize(Liquid::Literal.from_shorthand(source)))
@root = Document.new(tokenize(source))
self
end
@@ -121,7 +121,7 @@ module Liquid
begin
# render the nodelist.
# for performance reasons we get a array back here. join will make a string out of it
@root.render(context).join
@root.render(context).join
ensure
@errors = context.errors
end

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

@@ -0,0 +1,31 @@
module Liquid
module Utils
def self.slice_collection_using_each(collection, from, to)
segments = []
index = 0
yielded = 0
# Maintains Ruby 1.8.7 String#each behaviour on 1.9
return [collection] if non_blank_string?(collection)
collection.each do |item|
if to && to <= index
break
end
if from <= index
segments << item
end
index += 1
end
segments
end
def self.non_blank_string?(collection)
collection.is_a?(String) && collection != ''
end
end
end

View File

@@ -11,21 +11,21 @@ module Liquid
# {{ user | link }}
#
class Variable
FilterParser = /(?:#{FilterSeparator}|(?:\s*(?!(?:#{FilterSeparator}))(?:#{QuotedFragment}|\S+)\s*)+)/
FilterParser = /(?:#{FilterSeparator}|(?:\s*(?!(?:#{FilterSeparator}))(?:#{QuotedFragment}|\S+)\s*)+)/o
attr_accessor :filters, :name
def initialize(markup)
@markup = markup
@name = nil
@filters = []
if match = markup.match(/\s*(#{QuotedFragment})(.*)/)
if match = markup.match(/\s*(#{QuotedFragment})(.*)/o)
@name = match[1]
if match[2].match(/#{FilterSeparator}\s*(.*)/)
if match[2].match(/#{FilterSeparator}\s*(.*)/o)
filters = Regexp.last_match(1).scan(FilterParser)
filters.each do |f|
if matches = f.match(/\s*(\w+)/)
filtername = matches[1]
filterargs = f.scan(/(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*(#{QuotedFragment})/).flatten
filterargs = f.scan(/(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*(#{QuotedFragment})/o).flatten
@filters << [filtername.to_sym, filterargs]
end
end
@@ -37,7 +37,7 @@ module Liquid
return '' if @name.nil?
@filters.inject(context[@name]) do |output, filter|
filterargs = filter[1].to_a.collect do |a|
context[a]
context[a]
end
begin
output = context.invoke(filter[0], output, *filterargs)

BIN
lib/liquid_ext.bundle Executable file

Binary file not shown.

View File

@@ -1,23 +1,21 @@
# -*- encoding: utf-8 -*-
# encoding: utf-8
Gem::Specification.new do |s|
s.platform = Gem::Platform::RUBY
s.name = "liquid"
s.version = '2.2.2'
s.version = "2.4.1"
s.platform = Gem::Platform::RUBY
s.summary = "A secure, non-evaling end user template engine with aesthetic markup."
s.authors = ["Tobias Luetke"]
s.email = ["tobi@leetsoft.com"]
s.homepage = "http://www.liquidmarkup.org"
s.description = "A secure, non-evaling end user template engine with aesthetic markup."
#s.description = "A secure, non-evaling end user template engine with aesthetic markup."
s.required_rubygems_version = ">= 1.3.7"
s.files = Dir.glob("{lib}/**/*") + %w(MIT-LICENSE README.md)
s.test_files = Dir.glob("{test}/**/*")
s.files = Dir.glob("{lib}/**/*") + %w(MIT-LICENSE README.md)
s.extra_rdoc_files = ["History.txt", "Manifest.txt", "README.md"]
s.extra_rdoc_files = ["History.md", "README.md"]
s.require_path = 'lib'
s.require_path = "lib"
end

11
performance/benchmark.rb Normal file
View File

@@ -0,0 +1,11 @@
require 'rubygems'
require 'benchmark'
require File.dirname(__FILE__) + '/theme_runner'
profiler = ThemeRunner.new
Benchmark.bm do |x|
# x.report("parse:") { 100.times { profiler.compile } }
x.report("parse & run:") { 100.times { profiler.run } }
end

17
performance/profile.rb Normal file
View File

@@ -0,0 +1,17 @@
require 'rubygems'
require 'ruby-prof' rescue fail("install ruby-prof extension/gem")
require File.dirname(__FILE__) + '/theme_runner'
profiler = ThemeRunner.new
puts 'Running profiler...'
results = profiler.run_profile
puts 'Success'
filename = (ENV['TMP'] || '/tmp') + "/callgrind.liquid.txt"
File.open(filename, "w+") do |fp|
RubyProf::CallTreePrinter.new(results).print(fp, :print_file => true)
end
$stderr.puts "wrote RubyProf::CallTreePrinter output to #{filename}"

View File

@@ -1,3 +1,4 @@
$:.unshift File.dirname(__FILE__) + '/../../lib'
require File.dirname(__FILE__) + '/../../lib/liquid'
require File.dirname(__FILE__) + '/comment_form'

View File

@@ -52,17 +52,17 @@ class Paginate < Liquid::Block
hellip_break = false
if page_count > 2
1.upto(page_count-1) do |page|
1.upto(page_count-1) do |page|
if current_page == page
pagination['parts'] << no_link(page)
pagination['parts'] << no_link(page)
elsif page == 1
pagination['parts'] << link(page, page)
elsif page == page_count -1
pagination['parts'] << link(page, page)
elsif page <= current_page - @attributes['window_size'] or page >= current_page + @attributes['window_size']
next if hellip_break
pagination['parts'] << no_link('&hellip;')
pagination['parts'] << no_link('&hellip;')
hellip_break = true
next
else
@@ -73,7 +73,7 @@ class Paginate < Liquid::Block
end
end
render_all(@nodelist, context)
render_all(@nodelist, context)
end
end

View File

@@ -13,55 +13,87 @@ require 'digest/md5'
require File.dirname(__FILE__) + '/shopify/liquid'
require File.dirname(__FILE__) + '/shopify/database.rb'
require "ruby-prof" rescue fail("install ruby-prof extension/gem")
class ThemeRunner
class ThemeProfiler
# Load all templates into memory, do this now so that
# we don't profile IO.
def initialize
@tests = Dir[File.dirname(__FILE__) + '/tests/**/*.liquid'].collect do |test|
next if File.basename(test) == 'theme.liquid'
theme_path = File.dirname(test) + '/theme.liquid'
[File.read(test), (File.file?(theme_path) ? File.read(theme_path) : nil), test]
end.compact
end
def profile
RubyProf.measure_mode = RubyProf::WALL_TIME
def compile
# Dup assigns because will make some changes to them
@tests.each do |liquid, layout, template_name|
tmpl = Liquid::Template.new
tmpl.parse(liquid)
tmpl = Liquid::Template.new
tmpl.parse(layout)
end
end
def run
# 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
page_template = File.basename(template_name, File.extname(template_name))
compile_and_render(liquid, layout, assigns, page_template)
end
end
def run_profile
RubyProf.measure_mode = RubyProf::PROCESS_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)
RubyProf.pause
# Profile compiling and rendering both
RubyProf.resume { html = compile_and_render(liquid, layout, assigns, page_template) }
# 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
RubyProf.stop
end
def compile_and_render(template, layout, assigns, page_template)
tmpl = Liquid::Template.new
tmpl.assigns['page_title'] = 'Page title'
tmpl.assigns['template'] = page_template
content_for_layout = tmpl.parse(template).render(assigns)
if layout
assigns['content_for_layout'] = content_for_layout
tmpl.parse(layout).render(assigns)
@@ -72,21 +104,4 @@ class ThemeProfiler
end
profiler = ThemeProfiler.new
puts 'Running profiler...'
results = profiler.profile
puts 'Success'
puts
[RubyProf::FlatPrinter, RubyProf::GraphPrinter, RubyProf::GraphHtmlPrinter, RubyProf::CallTreePrinter].each do |klass|
filename = (ENV['TMP'] || '/tmp') + (klass.name.include?('Html') ? "/liquid.#{klass.name.downcase}.html" : "/liquid.#{klass.name.downcase}.txt")
filename.gsub!(/:+/, '_')
File.open(filename, "w+") { |fp| klass.new(results).print(fp) }
$stderr.puts "wrote #{klass.name} output to #{filename}"
end

View File

@@ -1,547 +0,0 @@
# The Breakpoint library provides the convenience of
# being able to inspect and modify state, diagnose
# bugs all via IRB by simply setting breakpoints in
# your applications by the call of a method.
#
# This library was written and is supported by me,
# Florian Gross. I can be reached at flgr@ccan.de
# and enjoy getting feedback about my libraries.
#
# The whole library (including breakpoint_client.rb
# and binding_of_caller.rb) is licensed under the
# same license that Ruby uses. (Which is currently
# either the GNU General Public License or a custom
# one that allows for commercial usage.) If you for
# some good reason need to use this under another
# license please contact me.
require 'irb'
require 'caller'
require 'drb'
require 'drb/acl'
require 'thread'
module Breakpoint
id = %q$Id: breakpoint.rb 52 2005-02-26 19:43:19Z flgr $
current_version = id.split(" ")[2]
unless defined?(Version)
# The Version of ruby-breakpoint you are using as String of the
# 1.2.3 form where the digits stand for release, major and minor
# version respectively.
Version = "0.5.0"
end
extend self
# This will pop up an interactive ruby session at a
# pre-defined break point in a Ruby application. In
# this session you can examine the environment of
# the break point.
#
# You can get a list of variables in the context using
# local_variables via +local_variables+. You can then
# examine their values by typing their names.
#
# You can have a look at the call stack via +caller+.
#
# The source code around the location where the breakpoint
# was executed can be examined via +source_lines+. Its
# argument specifies how much lines of context to display.
# The default amount of context is 5 lines. Note that
# the call to +source_lines+ can raise an exception when
# it isn't able to read in the source code.
#
# breakpoints can also return a value. They will execute
# a supplied block for getting a default return value.
# A custom value can be returned from the session by doing
# +throw(:debug_return, value)+.
#
# You can also give names to break points which will be
# used in the message that is displayed upon execution
# of them.
#
# Here's a sample of how breakpoints should be placed:
#
# class Person
# def initialize(name, age)
# @name, @age = name, age
# breakpoint("Person#initialize")
# end
#
# attr_reader :age
# def name
# breakpoint("Person#name") { @name }
# end
# end
#
# person = Person.new("Random Person", 23)
# puts "Name: #{person.name}"
#
# And here is a sample debug session:
#
# Executing break point "Person#initialize" at file.rb:4 in `initialize'
# irb(#<Person:0x292fbe8>):001:0> local_variables
# => ["name", "age", "_", "__"]
# irb(#<Person:0x292fbe8>):002:0> [name, age]
# => ["Random Person", 23]
# irb(#<Person:0x292fbe8>):003:0> [@name, @age]
# => ["Random Person", 23]
# irb(#<Person:0x292fbe8>):004:0> self
# => #<Person:0x292fbe8 @age=23, @name="Random Person">
# irb(#<Person:0x292fbe8>):005:0> @age += 1; self
# => #<Person:0x292fbe8 @age=24, @name="Random Person">
# irb(#<Person:0x292fbe8>):006:0> exit
# Executing break point "Person#name" at file.rb:9 in `name'
# irb(#<Person:0x292fbe8>):001:0> throw(:debug_return, "Overriden name")
# Name: Overriden name
#
# Breakpoint sessions will automatically have a few
# convenience methods available. See Breakpoint::CommandBundle
# for a list of them.
#
# Breakpoints can also be used remotely over sockets.
# This is implemented by running part of the IRB session
# in the application and part of it in a special client.
# You have to call Breakpoint.activate_drb to enable
# support for remote breakpoints and then run
# breakpoint_client.rb which is distributed with this
# library. See the documentation of Breakpoint.activate_drb
# for details.
def breakpoint(id = nil, context = nil, &block)
callstack = caller
callstack.slice!(0, 3) if callstack.first["breakpoint"]
file, line, method = *callstack.first.match(/^(.+?):(\d+)(?::in `(.*?)')?/).captures
message = "Executing break point " + (id ? "#{id.inspect} " : "") +
"at #{file}:#{line}" + (method ? " in `#{method}'" : "")
if context then
return handle_breakpoint(context, message, file, line, &block)
end
Binding.of_caller do |binding_context|
handle_breakpoint(binding_context, message, file, line, &block)
end
end
# These commands are automatically available in all breakpoint shells.
module CommandBundle
# Proxy to a Breakpoint client. Lets you directly execute code
# in the context of the client.
class Client
def initialize(eval_handler) # :nodoc:
eval_handler.untaint
@eval_handler = eval_handler
end
instance_methods.each do |method|
next if method[/^__.+__$/]
undef_method method
end
# Executes the specified code at the client.
def eval(code)
@eval_handler.call(code)
end
# Will execute the specified statement at the client.
def method_missing(method, *args, &block)
if args.empty? and not block
result = eval "#{method}"
else
# This is a bit ugly. The alternative would be using an
# eval context instead of an eval handler for executing
# the code at the client. The problem with that approach
# is that we would have to handle special expressions
# like "self", "nil" or constants ourself which is hard.
remote = eval %{
result = lambda { |block, *args| #{method}(*args, &block) }
def result.call_with_block(*args, &block)
call(block, *args)
end
result
}
remote.call_with_block(*args, &block)
end
return result
end
end
# Returns the source code surrounding the location where the
# breakpoint was issued.
def source_lines(context = 5, return_line_numbers = false)
lines = File.readlines(@__bp_file).map { |line| line.chomp }
break_line = @__bp_line
start_line = [break_line - context, 1].max
end_line = break_line + context
result = lines[(start_line - 1) .. (end_line - 1)]
if return_line_numbers then
return [start_line, break_line, result]
else
return result
end
end
# Lets an object that will forward method calls to the breakpoint
# client. This is useful for outputting longer things at the client
# and so on. You can for example do these things:
#
# client.puts "Hello" # outputs "Hello" at client console
# # outputs "Hello" into the file temp.txt at the client
# client.File.open("temp.txt", "w") { |f| f.puts "Hello" }
def client()
if Breakpoint.use_drb? then
sleep(0.5) until Breakpoint.drb_service.eval_handler
Client.new(Breakpoint.drb_service.eval_handler)
else
Client.new(lambda { |code| eval(code, TOPLEVEL_BINDING) })
end
end
end
def handle_breakpoint(context, message, file = "", line = "", &block) # :nodoc:
catch(:debug_return) do |value|
eval(%{
@__bp_file = #{file.inspect}
@__bp_line = #{line}
extend Breakpoint::CommandBundle
extend DRbUndumped if self
}, context) rescue nil
if not use_drb? then
puts message
IRB.start(nil, IRB::WorkSpace.new(context))
else
@drb_service.add_breakpoint(context, message)
end
block.call if block
end
end
# These exceptions will be raised on failed asserts
# if Breakpoint.asserts_cause_exceptions is set to
# true.
class FailedAssertError < RuntimeError
end
# This asserts that the block evaluates to true.
# If it doesn't evaluate to true a breakpoint will
# automatically be created at that execution point.
#
# You can disable assert checking in production
# code by setting Breakpoint.optimize_asserts to
# true. (It will still be enabled when Ruby is run
# via the -d argument.)
#
# Example:
# person_name = "Foobar"
# assert { not person_name.nil? }
#
# Note: If you want to use this method from an
# unit test, you will have to call it by its full
# name, Breakpoint.assert.
def assert(context = nil, &condition)
return if Breakpoint.optimize_asserts and not $DEBUG
return if yield
callstack = caller
callstack.slice!(0, 3) if callstack.first["assert"]
file, line, method = *callstack.first.match(/^(.+?):(\d+)(?::in `(.*?)')?/).captures
message = "Assert failed at #{file}:#{line}#{" in `#{method}'" if method}."
if Breakpoint.asserts_cause_exceptions and not $DEBUG then
raise(Breakpoint::FailedAssertError, message)
end
message += " Executing implicit breakpoint."
if context then
return handle_breakpoint(context, message, file, line)
end
Binding.of_caller do |context|
handle_breakpoint(context, message, file, line)
end
end
# Whether asserts should be ignored if not in debug mode.
# Debug mode can be enabled by running ruby with the -d
# switch or by setting $DEBUG to true.
attr_accessor :optimize_asserts
self.optimize_asserts = false
# Whether an Exception should be raised on failed asserts
# in non-$DEBUG code or not. By default this is disabled.
attr_accessor :asserts_cause_exceptions
self.asserts_cause_exceptions = false
@use_drb = false
attr_reader :drb_service # :nodoc:
class DRbService # :nodoc:
include DRbUndumped
def initialize
@handler = @eval_handler = @collision_handler = nil
IRB.instance_eval { @CONF[:RC] = true }
IRB.run_config
end
def collision
sleep(0.5) until @collision_handler
@collision_handler.untaint
@collision_handler.call
end
def ping() end
def add_breakpoint(context, message)
workspace = IRB::WorkSpace.new(context)
workspace.extend(DRbUndumped)
sleep(0.5) until @handler
@handler.untaint
@handler.call(workspace, message)
rescue Errno::ECONNREFUSED, DRb::DRbConnError
raise if Breakpoint.use_drb?
end
attr_accessor :handler, :eval_handler, :collision_handler
end
# Will run Breakpoint in DRb mode. This will spawn a server
# that can be attached to via the breakpoint-client command
# whenever a breakpoint is executed. This is useful when you
# are debugging CGI applications or other applications where
# you can't access debug sessions via the standard input and
# output of your application.
#
# You can specify an URI where the DRb server will run at.
# This way you can specify the port the server runs on. The
# default URI is druby://localhost:42531.
#
# Please note that breakpoints will be skipped silently in
# case the DRb server can not spawned. (This can happen if
# the port is already used by another instance of your
# application on CGI or another application.)
#
# Also note that by default this will only allow access
# from localhost. You can however specify a list of
# allowed hosts or nil (to allow access from everywhere).
# But that will still not protect you from somebody
# reading the data as it goes through the net.
#
# A good approach for getting security and remote access
# is setting up an SSH tunnel between the DRb service
# and the client. This is usually done like this:
#
# $ ssh -L20000:127.0.0.1:20000 -R10000:127.0.0.1:10000 example.com
# (This will connect port 20000 at the client side to port
# 20000 at the server side, and port 10000 at the server
# side to port 10000 at the client side.)
#
# After that do this on the server side: (the code being debugged)
# Breakpoint.activate_drb("druby://127.0.0.1:20000", "localhost")
#
# And at the client side:
# ruby breakpoint_client.rb -c druby://127.0.0.1:10000 -s druby://127.0.0.1:20000
#
# Running through such a SSH proxy will also let you use
# breakpoint.rb in case you are behind a firewall.
#
# Detailed information about running DRb through firewalls is
# available at http://www.rubygarden.org/ruby?DrbTutorial
#
# == Security considerations
# Usually you will be fine when using the default druby:// URI and the default
# access control list. However, if you are sitting on a machine where there are
# local users that you likely can not trust (this is the case for example on
# most web hosts which have multiple users sitting on the same physical machine)
# you will be better off by doing client/server communication through a unix
# socket. This can be accomplished by calling with a drbunix:/ style URI, e.g.
# <code>Breakpoint.activate_drb('drbunix:/tmp/breakpoint_server')</code>. This
# will only work on Unix based platforms.
def activate_drb(uri = nil, allowed_hosts = ['localhost', '127.0.0.1', '::1'],
ignore_collisions = false)
return false if @use_drb
uri ||= 'druby://localhost:42531'
if allowed_hosts then
acl = ["deny", "all"]
Array(allowed_hosts).each do |host|
acl += ["allow", host]
end
DRb.install_acl(ACL.new(acl))
end
@use_drb = true
@drb_service = DRbService.new
did_collision = false
begin
@service = DRb.start_service(uri, @drb_service)
rescue Errno::EADDRINUSE
if ignore_collisions then
nil
else
# The port is already occupied by another
# Breakpoint service. We will try to tell
# the old service that we want its port.
# It will then forward that request to the
# user and retry.
unless did_collision then
DRbObject.new(nil, uri).collision
did_collision = true
end
sleep(10)
retry
end
end
return true
end
# Deactivates a running Breakpoint service.
def deactivate_drb
Thread.exclusive do
@service.stop_service unless @service.nil?
@service = nil
@use_drb = false
@drb_service = nil
end
end
# Returns true when Breakpoints are used over DRb.
# Breakpoint.activate_drb causes this to be true.
def use_drb?
@use_drb == true
end
end
module IRB # :nodoc:
class << self; remove_method :start; end
def self.start(ap_path = nil, main_context = nil, workspace = nil)
$0 = File::basename(ap_path, ".rb") if ap_path
# suppress some warnings about redefined constants
old_verbose, $VERBOSE = $VERBOSE, nil
IRB.setup(ap_path)
$VERBOSE = old_verbose
if @CONF[:SCRIPT] then
irb = Irb.new(main_context, @CONF[:SCRIPT])
else
irb = Irb.new(main_context)
end
if workspace then
irb.context.workspace = workspace
end
@CONF[:IRB_RC].call(irb.context) if @CONF[:IRB_RC]
@CONF[:MAIN_CONTEXT] = irb.context
old_sigint = trap("SIGINT") do
begin
irb.signal_handle
rescue RubyLex::TerminateLineInput
# ignored
end
end
catch(:IRB_EXIT) do
irb.eval_input
end
ensure
trap("SIGINT", old_sigint)
end
class << self
alias :old_CurrentContext :CurrentContext
remove_method :CurrentContext
remove_method :parse_opts
end
def IRB.CurrentContext
if old_CurrentContext.nil? and Breakpoint.use_drb? then
result = Object.new
def result.last_value; end
return result
else
old_CurrentContext
end
end
def IRB.parse_opts() end
class Context # :nodoc:
alias :old_evaluate :evaluate
def evaluate(line, line_no)
if line.chomp == "exit" then
exit
else
old_evaluate(line, line_no)
end
end
end
class WorkSpace # :nodoc:
alias :old_evaluate :evaluate
def evaluate(*args)
if Breakpoint.use_drb? then
result = old_evaluate(*args)
if args[0] != :no_proxy and
not [true, false, nil].include?(result)
then
result.extend(DRbUndumped) rescue nil
end
return result
else
old_evaluate(*args)
end
end
end
module InputCompletor # :nodoc:
def self.eval(code, context, *more)
# Big hack, this assumes that InputCompletor
# will only call eval() when it wants code
# to be executed in the IRB context.
IRB.conf[:MAIN_CONTEXT].workspace.evaluate(:no_proxy, code, *more)
end
end
end
module DRb # :nodoc:
class DRbObject # :nodoc:
undef :inspect if method_defined?(:inspect)
undef :clone if method_defined?(:clone)
end
end
# See Breakpoint.breakpoint
def breakpoint(id = nil, &block)
Binding.of_caller do |context|
Breakpoint.breakpoint(id, context, &block)
end
end
# See Breakpoint.assert
def assert(&block)
Binding.of_caller do |context|
Breakpoint.assert(context, &block)
end
end

View File

@@ -1,80 +0,0 @@
class Continuation # :nodoc:
def self.create(*args, &block) # :nodoc:
cc = nil; result = callcc {|c| cc = c; block.call(cc) if block and args.empty?}
result ||= args
return *[cc, *result]
end
end
class Binding; end # for RDoc
# This method returns the binding of the method that called your
# method. It will raise an Exception when you're not inside a method.
#
# It's used like this:
# def inc_counter(amount = 1)
# Binding.of_caller do |binding|
# # Create a lambda that will increase the variable 'counter'
# # in the caller of this method when called.
# inc = eval("lambda { |arg| counter += arg }", binding)
# # We can refer to amount from inside this block safely.
# inc.call(amount)
# end
# # No other statements can go here. Put them inside the block.
# end
# counter = 0
# 2.times { inc_counter }
# counter # => 2
#
# Binding.of_caller must be the last statement in the method.
# This means that you will have to put everything you want to
# do after the call to Binding.of_caller into the block of it.
# This should be no problem however, because Ruby has closures.
# If you don't do this an Exception will be raised. Because of
# the way that Binding.of_caller is implemented it has to be
# done this way.
def Binding.of_caller(&block)
old_critical = Thread.critical
Thread.critical = true
count = 0
cc, result, error, extra_data = Continuation.create(nil, nil)
error.call if error
tracer = lambda do |*args|
type, context, extra_data = args[0], args[4], args
if type == "return"
count += 1
# First this method and then calling one will return --
# the trace event of the second event gets the context
# of the method which called the method that called this
# method.
if count == 2
# It would be nice if we could restore the trace_func
# that was set before we swapped in our own one, but
# this is impossible without overloading set_trace_func
# in current Ruby.
set_trace_func(nil)
cc.call(eval("binding", context), nil, extra_data)
end
elsif type == "line" then
nil
elsif type == "c-return" and extra_data[3] == :set_trace_func then
nil
else
set_trace_func(nil)
error_msg = "Binding.of_caller used in non-method context or " +
"trailing statements of method using it aren't in the block."
cc.call(nil, lambda { raise(ArgumentError, error_msg) }, nil)
end
end
unless result
set_trace_func(tracer)
return nil
else
Thread.critical = old_critical
case block.arity
when 1 then yield(result)
else yield(result, extra_data)
end
end
end

View File

@@ -1,29 +0,0 @@
require 'test_helper'
class HtmlTagTest < Test::Unit::TestCase
include Liquid
def test_html_table
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 %}',
'numbers' => [1,2,3,4,5,6])
assert_template_result("<tr class=\"row1\">\n</tr>\n",
'{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}',
'numbers' => [])
end
def test_html_table_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",
'{% tablerow n in numbers cols:5%} {{n}} {% endtablerow %}',
'numbers' => [1,2,3,4,5,6])
end
def test_html_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",
'{% tablerow n in numbers cols:2%}{{tablerowloop.col}}{% endtablerow %}',
'numbers' => [1,2,3,4,5,6])
end
end # HtmlTagTest

View File

@@ -1,39 +0,0 @@
require 'test_helper'
class LiteralTagTest < Test::Unit::TestCase
include Liquid
def test_empty_literal
assert_template_result '', '{% literal %}{% endliteral %}'
assert_template_result '', '{{{}}}'
end
def test_simple_literal_value
assert_template_result 'howdy',
'{% literal %}howdy{% endliteral %}'
end
def test_literals_ignore_liquid_markup
expected = %({% if 'gnomeslab' contain 'liquid' %}yes{ % endif %})
template = %({% literal %}#{expected}{% endliteral %})
assert_template_result expected, template
end
def test_shorthand_syntax
expected = %({% if 'gnomeslab' contain 'liquid' %}yes{ % endif %})
template = %({{{#{expected}}}})
assert_template_result expected, template
end
# Class methods
def test_from_shorthand
assert_equal '{% literal %}gnomeslab{% endliteral %}', Liquid::Literal.from_shorthand('{{{gnomeslab}}}')
end
def test_from_shorthand_ignores_improper_syntax
text = "{% if 'hi' == 'hi' %}hi{% endif %}"
assert_equal text, Liquid::Literal.from_shorthand(text)
end
end # AssignTest

View File

@@ -12,4 +12,10 @@ class AssignTest < Test::Unit::TestCase
'{% assign foo = values %}.{{ foo[1] }}.',
'values' => %w{foo bar baz})
end
def test_assign_with_filter
assert_template_result('.bar.',
'{% assign foo = values | split: "," %}.{{ foo[1] }}.',
'values' => "foo,bar,baz")
end
end # AssignTest

View File

@@ -1,6 +1,6 @@
require 'test_helper'
class VariableTest < Test::Unit::TestCase
class BlockTest < Test::Unit::TestCase
include Liquid
def test_blankspace

View File

@@ -18,6 +18,11 @@ class ConditionTest < Test::Unit::TestCase
assert_evalutes_true '2', '>=', '1'
assert_evalutes_true '1', '<=', '2'
assert_evalutes_true '1', '<=', '1'
# negative numbers
assert_evalutes_true '1', '>', '-1'
assert_evalutes_true '-1', '<', '1'
assert_evalutes_true '1.0', '>', '-1.0'
assert_evalutes_true '-1.0', '<', '1.0'
end
def test_default_operators_evalute_false

View File

@@ -254,12 +254,16 @@ class ContextTest < Test::Unit::TestCase
@context['test'] = {'test' => [1,2,3,4,5]}
assert_equal 1, @context['test.test[0]']
end
def test_recoursive_array_notation_for_hash
@context['test'] = [{'test' => 'worked'}]
assert_equal 'worked', @context['test[0].test']
end
def test_hash_to_array_transition
@context['colors'] = {
'Blue' => ['003366','336699', '6699CC', '99CCFF'],
@@ -315,7 +319,7 @@ class ContextTest < Test::Unit::TestCase
@context['nested'] = {'var' => 'tags'}
@context['products'] = {'count' => 5, 'tags' => ['deepsnow', 'freestyle'] }
assert_equal 'deepsnow', @context['products[var].first']
#assert_equal 'deepsnow', @context['products[var].first']
assert_equal 'freestyle', @context['products[nested.var].last']
end

View File

@@ -13,10 +13,6 @@ class ContextDrop < Liquid::Drop
@context['forloop.index']
end
def break
Breakpoint.breakpoint
end
def before_method(method)
return @context[method]
end
@@ -36,7 +32,7 @@ class ProductDrop < Liquid::Drop
class CatchallDrop < Liquid::Drop
def before_method(method)
return 'method: ' << method
return 'method: ' << method.to_s
end
end
@@ -88,12 +84,17 @@ class DropsTest < Test::Unit::TestCase
end
def test_text_drop
def test_unknown_method
output = Liquid::Template.parse( ' {{ product.catchall.unknown }} ' ).render('product' => ProductDrop.new)
assert_equal ' method: unknown ', output
end
def test_integer_argument_drop
output = Liquid::Template.parse( ' {{ product.catchall[8] }} ' ).render('product' => ProductDrop.new)
assert_equal ' method: 8 ', output
end
def test_text_array_drop
output = Liquid::Template.parse( '{% for text in product.texts.array %} {{text}} {% endfor %}' ).render('product' => ProductDrop.new)
assert_equal ' text1 text2 ', output
@@ -150,4 +151,12 @@ class DropsTest < Test::Unit::TestCase
def test_enumerable_drop_size
assert_equal '3', Liquid::Template.parse( '{{collection.size}}').render('collection' => EnumerableDrop.new)
end
def test_empty_string_value_access
assert_equal '', Liquid::Template.parse('{{ product[value] }}').render('product' => ProductDrop.new, 'value' => '')
end
def test_nil_value_access
assert_equal '', Liquid::Template.parse('{{ product[value] }}').render('product' => ProductDrop.new, 'value' => nil)
end
end # DropsTest

View File

@@ -13,6 +13,10 @@ class ErrorDrop < Liquid::Drop
raise Liquid::SyntaxError, 'syntax error'
end
def exception
raise Exception, 'exception'
end
end
class ErrorHandlingTest < Test::Unit::TestCase
@@ -66,4 +70,12 @@ class ErrorHandlingTest < Test::Unit::TestCase
assert_equal Liquid::ArgumentError, template.errors.first.class
end
end
# Liquid should not catch Exceptions that are not subclasses of StandardError, like Interrupt and NoMemoryError
def test_exceptions_propagate
assert_raise Exception do
template = Liquid::Template.parse( ' {{ errors.exception }} ' )
template.render('errors' => ErrorDrop.new)
end
end
end # ErrorHandlingTest

View File

@@ -5,7 +5,7 @@ class FileSystemTest < Test::Unit::TestCase
def test_default
assert_raise(FileSystemError) do
BlankFileSystem.new.read_template_file("dummy")
BlankFileSystem.new.read_template_file("dummy", {'dummy'=>'smarty'})
end
end

View File

@@ -75,6 +75,12 @@ class FiltersTest < Test::Unit::TestCase
assert_equal "bla blub", Variable.new("var | strip_html").render(@context)
end
def test_strip_html_ignore_comments_with_html
@context['var'] = "<!-- split and some <ul> tag --><b>bla blub</a>"
assert_equal "bla blub", Variable.new("var | strip_html").render(@context)
end
def test_capitalize
@context['var'] = "blub"

View File

@@ -0,0 +1,89 @@
require 'test_helper'
class ParserTest < Test::Unit::TestCase
include Liquid
def test_strings
assert_equal [[:id, "string"]], Parser.parse('"string"')
assert_equal [[:id, "string"]], Parser.parse('\'string\'')
end
def test_integer
assert_equal [[:id, 1]], Parser.parse('1')
assert_equal [[:id, 100001]], Parser.parse('100001')
end
def test_float
assert_equal [[:id, 1.1]], Parser.parse('1.1')
assert_equal [[:id, 1.55435]], Parser.parse('1.55435')
end
def test_null
assert_equal [[:id, nil]], Parser.parse('null')
assert_equal [[:id, nil]], Parser.parse('nil')
end
def test_bool
assert_equal [[:id, true]], Parser.parse('true')
assert_equal [[:id, false]], Parser.parse('false')
end
def test_ranges
assert_equal [[:id, 1], [:id, 5], [:range, nil]], Parser.parse('(1..5)')
assert_equal [[:id, 100], [:id, 500], [:range, nil]], Parser.parse('(100..500)')
end
def test_ranges_with_lookups
assert_equal [[:id, 1], [:id, "test"], [:lookup, nil], [:range, nil]], Parser.parse('(1..test)')
end
def test_lookups
assert_equal [[:id, "variable"], [:lookup, nil]], Parser.parse('variable')
assert_equal [[:id, "underscored_variable"], [:lookup, nil]], Parser.parse('underscored_variable')
end
def test_global_hash
assert_equal [[:id, true], [:lookup, nil]], Parser.parse('[true]')
assert_equal [[:id, "string"], [:lookup, nil]], Parser.parse('["string"]')
assert_equal [[:id, 5.55], [:lookup, nil]], Parser.parse('[5.55]')
assert_equal [[:id, 0], [:lookup, nil]], Parser.parse('[0]')
assert_equal [[:id, "variable"], [:lookup, nil], [:lookup, nil]], Parser.parse('[variable]')
end
def test_descent
assert_equal [[:id, "variable1"], [:lookup, nil], [:id, "variable2"], [:call, nil]], Parser.parse('variable1.variable2')
assert_equal [[:id, "variable1"], [:lookup, nil], [:id, "variable2"], [:call, nil], [:id, "variable3"], [:call, nil]], Parser.parse('variable1.variable2.variable3')
end
def test_descent_hash
assert_equal [[:id, "variable1"], [:lookup, nil], [:id, "variable2"], [:call, nil]], Parser.parse('variable1["variable2"]')
assert_equal [[:id, "variable1"], [:lookup, nil], [:id, "variable2"], [:lookup, nil], [:call, nil]], Parser.parse('variable1[variable2]')
end
def test_buildin
assert_equal [[:id, "first"], [:lookup, nil]], Parser.parse('first')
assert_equal [[:id, "var"], [:lookup, nil], [:buildin, "first"]], Parser.parse('var.first')
assert_equal [[:id, "var"], [:lookup, nil], [:buildin, "last"]], Parser.parse('var.last')
assert_equal [[:id, "var"], [:lookup, nil], [:buildin, "size"]], Parser.parse('var.size')
end
def test_descent_hash_descent
assert_equal [[:id, "variable1"], [:lookup, nil], [:id, "test1"], [:lookup, nil], [:id, "test2"], [:call, nil], [:call, nil]],
Parser.parse('variable1[test1.test2]'), "resolove: variable1[test1.test2]"
# assert_equal [[:id, "variable1"], [:lookup, nil], [:id, "test1"], [:lookup, nil], [:id, "test2"], [:call, nil], [:call, nil]],
# Parser.parse('variable1[test1["test2"]]'), 'resolove: variable1[test1["test2"]]'
# assert_equal [[:id, "variable1"], [:lookup, nil], [:id, "test1"], [:lookup, nil], [:id, "test2"], [:lookup, nil], [:call, nil], [:call, nil]],
# Parser.parse('variable1[test1[test2]]'), "resolove: variable1[test1[test2]]"
end
end

View File

@@ -39,7 +39,7 @@ class ParsingQuirksTest < Test::Unit::TestCase
def test_meaningless_parens
assigns = {'b' => 'bar', 'c' => 'baz'}
markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false"
markup = "a == 'foo' or b == 'bar' and c == 'baz' or false"
assert_template_result(' YES ',"{% if #{markup} %} YES {% endif %}", assigns)
end

View File

@@ -41,9 +41,4 @@ class RegexpTest < Test::Unit::TestCase
assert_equal ['var', '["method"]', '[0]'], 'var["method"][0]'.scan(VariableParser)
assert_equal ['var', '[method]', '[0]', 'method'], 'var[method][0].method'.scan(VariableParser)
end
def test_literal_shorthand_regexp
assert_equal [["{% if 'gnomeslab' contains 'liquid' %}yes{% endif %}"]],
"{{{ {% if 'gnomeslab' contains 'liquid' %}yes{% endif %} }}}".scan(LiteralShorthand)
end
end # RegexpTest

View File

@@ -39,6 +39,14 @@ class StandardFiltersTest < Test::Unit::TestCase
assert_equal '1234567890', @filters.truncate('1234567890')
end
def test_strip
assert_equal ['12','34'], @filters.split('12~34', '~')
assert_equal ['A? ',' ,Z'], @filters.split('A? ~ ~ ~ ,Z', '~ ~ ~')
assert_equal ['A?Z'], @filters.split('A?Z', '~')
# Regexp works although Liquid does not support.
assert_equal ['A','Z'], @filters.split('AxZ', /x/)
end
def test_escape
assert_equal '&lt;strong&gt;', @filters.escape('<strong>')
assert_equal '&lt;strong&gt;', @filters.h('<strong>')
@@ -97,6 +105,9 @@ class StandardFiltersTest < Test::Unit::TestCase
assert_equal "07/16/2004", @filters.date("Fri Jul 16 01:00:00 2004", "%m/%d/%Y")
assert_equal nil, @filters.date(nil, "%B")
assert_equal "07/05/2006", @filters.date(1152098955, "%m/%d/%Y")
assert_equal "07/05/2006", @filters.date("1152098955", "%m/%d/%Y")
end
@@ -162,6 +173,10 @@ class StandardFiltersTest < Test::Unit::TestCase
assert_template_result "Liquid error: divided by 0", "{{ 5 | divided_by:0 }}"
end
def test_modulo
assert_template_result "1", "{{ 3 | modulo:2 }}"
end
def test_append
assigns = {'a' => 'bc', 'b' => 'd' }
assert_template_result('bcd',"{{ a | append: 'd'}}",assigns)

View File

@@ -0,0 +1,16 @@
require 'test_helper'
class BreakTagTest < Test::Unit::TestCase
include Liquid
# tests that no weird errors are raised if break is called outside of a
# block
def test_break_with_no_block
assigns = {'i' => 1}
markup = '{% break %}'
expected = ''
assert_template_result(expected, markup, assigns)
end
end

View File

@@ -0,0 +1,16 @@
require 'test_helper'
class ContinueTagTest < Test::Unit::TestCase
include Liquid
# tests that no weird errors are raised if continue is called outside of a
# block
def test_continue_with_no_block
assigns = {}
markup = '{% continue %}'
expected = ''
assert_template_result(expected, markup, assigns)
end
end

View File

@@ -0,0 +1,284 @@
require 'test_helper'
class ForTagTest < Test::Unit::TestCase
include Liquid
def test_for
assert_template_result(' yo yo yo yo ','{%for item in array%} yo {%endfor%}','array' => [1,2,3,4])
assert_template_result('yoyo','{%for item in array%}yo{%endfor%}','array' => [1,2])
assert_template_result(' yo ','{%for item in array%} yo {%endfor%}','array' => [1])
assert_template_result('','{%for item in array%}{%endfor%}','array' => [1,2])
expected = <<HERE
yo
yo
yo
HERE
template = <<HERE
{%for item in array%}
yo
{%endfor%}
HERE
assert_template_result(expected,template,'array' => [1,2,3])
end
def test_for_reversed
assigns = {'array' => [ 1, 2, 3] }
assert_template_result('321','{%for item in array reversed %}{{item}}{%endfor%}',assigns)
end
def test_for_with_range
assert_template_result(' 1 2 3 ','{%for item in (1..3) %} {{item}} {%endfor%}')
end
def test_for_with_variable
assert_template_result(' 1 2 3 ','{%for item in array%} {{item}} {%endfor%}','array' => [1,2,3])
assert_template_result('123','{%for item in array%}{{item}}{%endfor%}','array' => [1,2,3])
assert_template_result('123','{% for item in array %}{{item}}{% endfor %}','array' => [1,2,3])
assert_template_result('abcd','{%for item in array%}{{item}}{%endfor%}','array' => ['a','b','c','d'])
assert_template_result('a b c','{%for item in array%}{{item}}{%endfor%}','array' => ['a',' ','b',' ','c'])
assert_template_result('abc','{%for item in array%}{{item}}{%endfor%}','array' => ['a','','b','','c'])
end
def test_for_helpers
assigns = {'array' => [1,2,3] }
assert_template_result(' 1/3 2/3 3/3 ',
'{%for item in array%} {{forloop.index}}/{{forloop.length}} {%endfor%}',
assigns)
assert_template_result(' 1 2 3 ', '{%for item in array%} {{forloop.index}} {%endfor%}', assigns)
assert_template_result(' 0 1 2 ', '{%for item in array%} {{forloop.index0}} {%endfor%}', assigns)
assert_template_result(' 2 1 0 ', '{%for item in array%} {{forloop.rindex0}} {%endfor%}', assigns)
assert_template_result(' 3 2 1 ', '{%for item in array%} {{forloop.rindex}} {%endfor%}', assigns)
assert_template_result(' true false false ', '{%for item in array%} {{forloop.first}} {%endfor%}', assigns)
assert_template_result(' false false true ', '{%for item in array%} {{forloop.last}} {%endfor%}', assigns)
end
def test_for_and_if
assigns = {'array' => [1,2,3] }
assert_template_result('+--',
'{%for item in array%}{% if forloop.first %}+{% else %}-{% endif %}{%endfor%}',
assigns)
end
def test_for_else
assert_template_result('+++', '{%for item in array%}+{%else%}-{%endfor%}', 'array'=>[1,2,3])
assert_template_result('-', '{%for item in array%}+{%else%}-{%endfor%}', 'array'=>[])
assert_template_result('-', '{%for item in array%}+{%else%}-{%endfor%}', 'array'=>nil)
end
def test_limiting
assigns = {'array' => [1,2,3,4,5,6,7,8,9,0]}
assert_template_result('12', '{%for i in array limit:2 %}{{ i }}{%endfor%}', assigns)
assert_template_result('1234', '{%for i in array limit:4 %}{{ i }}{%endfor%}', assigns)
assert_template_result('3456', '{%for i in array limit:4 offset:2 %}{{ i }}{%endfor%}', assigns)
assert_template_result('3456', '{%for i in array limit: 4 offset: 2 %}{{ i }}{%endfor%}', assigns)
end
def test_dynamic_variable_limiting
assigns = {'array' => [1,2,3,4,5,6,7,8,9,0]}
assigns['limit'] = 2
assigns['offset'] = 2
assert_template_result('34', '{%for i in array limit: limit offset: offset %}{{ i }}{%endfor%}', assigns)
end
def test_nested_for
assigns = {'array' => [[1,2],[3,4],[5,6]] }
assert_template_result('123456', '{%for item in array%}{%for i in item%}{{ i }}{%endfor%}{%endfor%}', assigns)
end
def test_offset_only
assigns = {'array' => [1,2,3,4,5,6,7,8,9,0]}
assert_template_result('890', '{%for i in array offset:7 %}{{ i }}{%endfor%}', assigns)
end
def test_pause_resume
assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,0]}}
markup = <<-MKUP
{%for i in array.items limit: 3 %}{{i}}{%endfor%}
next
{%for i in array.items offset:continue limit: 3 %}{{i}}{%endfor%}
next
{%for i in array.items offset:continue limit: 3 %}{{i}}{%endfor%}
MKUP
expected = <<-XPCTD
123
next
456
next
789
XPCTD
assert_template_result(expected,markup,assigns)
end
def test_pause_resume_limit
assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,0]}}
markup = <<-MKUP
{%for i in array.items limit:3 %}{{i}}{%endfor%}
next
{%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%}
next
{%for i in array.items offset:continue limit:1 %}{{i}}{%endfor%}
MKUP
expected = <<-XPCTD
123
next
456
next
7
XPCTD
assert_template_result(expected,markup,assigns)
end
def test_pause_resume_BIG_limit
assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,0]}}
markup = <<-MKUP
{%for i in array.items limit:3 %}{{i}}{%endfor%}
next
{%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%}
next
{%for i in array.items offset:continue limit:1000 %}{{i}}{%endfor%}
MKUP
expected = <<-XPCTD
123
next
456
next
7890
XPCTD
assert_template_result(expected,markup,assigns)
end
def test_pause_resume_BIG_offset
assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,0]}}
markup = %q({%for i in array.items limit:3 %}{{i}}{%endfor%}
next
{%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%}
next
{%for i in array.items offset:continue limit:3 offset:1000 %}{{i}}{%endfor%})
expected = %q(123
next
456
next
)
assert_template_result(expected,markup,assigns)
end
def test_for_with_break
assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,10]}}
markup = '{% for i in array.items %}{% break %}{% endfor %}'
expected = ""
assert_template_result(expected,markup,assigns)
markup = '{% for i in array.items %}{{ i }}{% break %}{% endfor %}'
expected = "1"
assert_template_result(expected,markup,assigns)
markup = '{% for i in array.items %}{% break %}{{ i }}{% endfor %}'
expected = ""
assert_template_result(expected,markup,assigns)
markup = '{% for i in array.items %}{{ i }}{% if i > 3 %}{% break %}{% endif %}{% endfor %}'
expected = "1234"
assert_template_result(expected,markup,assigns)
# tests to ensure it only breaks out of the local for loop
# and not all of them.
assigns = {'array' => [[1,2],[3,4],[5,6]] }
markup = '{% for item in array %}' +
'{% for i in item %}' +
'{% if i == 1 %}' +
'{% break %}' +
'{% endif %}' +
'{{ i }}' +
'{% endfor %}' +
'{% endfor %}'
expected = '3456'
assert_template_result(expected, markup, assigns)
# test break does nothing when unreached
assigns = {'array' => {'items' => [1,2,3,4,5]}}
markup = '{% for i in array.items %}{% if i == 9999 %}{% break %}{% endif %}{{ i }}{% endfor %}'
expected = '12345'
assert_template_result(expected, markup, assigns)
end
def test_for_with_continue
assigns = {'array' => {'items' => [1,2,3,4,5]}}
markup = '{% for i in array.items %}{% continue %}{% endfor %}'
expected = ""
assert_template_result(expected,markup,assigns)
markup = '{% for i in array.items %}{{ i }}{% continue %}{% endfor %}'
expected = "12345"
assert_template_result(expected,markup,assigns)
markup = '{% for i in array.items %}{% continue %}{{ i }}{% endfor %}'
expected = ""
assert_template_result(expected,markup,assigns)
markup = '{% for i in array.items %}{% if i > 3 %}{% continue %}{% endif %}{{ i }}{% endfor %}'
expected = "123"
assert_template_result(expected,markup,assigns)
markup = '{% for i in array.items %}{% if i == 3 %}{% continue %}{% else %}{{ i }}{% endif %}{% endfor %}'
expected = "1245"
assert_template_result(expected,markup,assigns)
# tests to ensure it only continues the local for loop and not all of them.
assigns = {'array' => [[1,2],[3,4],[5,6]] }
markup = '{% for item in array %}' +
'{% for i in item %}' +
'{% if i == 1 %}' +
'{% continue %}' +
'{% endif %}' +
'{{ i }}' +
'{% endfor %}' +
'{% endfor %}'
expected = '23456'
assert_template_result(expected, markup, assigns)
# test continue does nothing when unreached
assigns = {'array' => {'items' => [1,2,3,4,5]}}
markup = '{% for i in array.items %}{% if i == 9999 %}{% continue %}{% endif %}{{ i }}{% endfor %}'
expected = '12345'
assert_template_result(expected, markup, assigns)
end
def test_for_tag_string
# ruby 1.8.7 "String".each => Enumerator with single "String" element.
# ruby 1.9.3 no longer supports .each on String though we mimic
# the functionality for backwards compatibility
assert_template_result('test string',
'{%for val in string%}{{val}}{%endfor%}',
'string' => "test string")
assert_template_result('test string',
'{%for val in string limit:1%}{{val}}{%endfor%}',
'string' => "test string")
assert_template_result('val-string-1-1-0-1-0-true-true-test string',
'{%for val in string%}' +
'{{forloop.name}}-' +
'{{forloop.index}}-' +
'{{forloop.length}}-' +
'{{forloop.index0}}-' +
'{{forloop.rindex}}-' +
'{{forloop.rindex0}}-' +
'{{forloop.first}}-' +
'{{forloop.last}}-' +
'{{val}}{%endfor%}',
'string' => "test string")
end
def test_blank_string_not_iterable
assert_template_result('', "{% for char in characters %}I WILL NOT BE OUTPUT{% endfor %}", 'characters' => '')
end
end

View File

@@ -0,0 +1,63 @@
require 'test_helper'
class HtmlTagTest < Test::Unit::TestCase
include Liquid
class ArrayDrop < Liquid::Drop
include Enumerable
def initialize(array)
@array = array
end
def each(&block)
@array.each(&block)
end
end
def test_html_table
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 %}',
'numbers' => [1,2,3,4,5,6])
assert_template_result("<tr class=\"row1\">\n</tr>\n",
'{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}',
'numbers' => [])
end
def test_html_table_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",
'{% tablerow n in numbers cols:5%} {{n}} {% endtablerow %}',
'numbers' => [1,2,3,4,5,6])
end
def test_html_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",
'{% tablerow n in numbers cols:2%}{{tablerowloop.col}}{% endtablerow %}',
'numbers' => [1,2,3,4,5,6])
end
def test_quoted_fragment
assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
"{% tablerow n in collections.frontpage cols:3%} {{n}} {% endtablerow %}",
'collections' => {'frontpage' => [1,2,3,4,5,6]})
assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
"{% tablerow n in collections['frontpage'] cols:3%} {{n}} {% endtablerow %}",
'collections' => {'frontpage' => [1,2,3,4,5,6]})
end
def test_enumerable_drop
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 %}',
'numbers' => ArrayDrop.new([1,2,3,4,5,6]))
end
def test_offset_and_limit
assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
'{% tablerow n in numbers cols:3 offset:1 limit:6%} {{n}} {% endtablerow %}',
'numbers' => [0,1,2,3,4,5,6,7])
end
end # HtmlTagTest

View File

@@ -1,6 +1,6 @@
require 'test_helper'
class IfElseTest < Test::Unit::TestCase
class IfElseTagTest < Test::Unit::TestCase
include Liquid
def test_if

View File

@@ -1,7 +1,7 @@
require 'test_helper'
class TestFileSystem
def read_template_file(template_path)
def read_template_file(template_path, context)
case template_path
when "product"
"Product: {{ product.title }} "
@@ -34,7 +34,7 @@ class TestFileSystem
end
class OtherFileSystem
def read_template_file(template_path)
def read_template_file(template_path, context)
'from OtherFileSystem'
end
end
@@ -104,7 +104,7 @@ class IncludeTagTest < Test::Unit::TestCase
def test_recursively_included_template_does_not_produce_endless_loop
infinite_file_system = Class.new do
def read_template_file(template_path)
def read_template_file(template_path, context)
"-{% include 'loop' %}"
end
end
@@ -117,6 +117,18 @@ class IncludeTagTest < Test::Unit::TestCase
end
def test_backwards_compatability_support_for_overridden_read_template_file
infinite_file_system = Class.new do
def read_template_file(template_path) # testing only one argument here.
"- hi mom"
end
end
Liquid::Template.file_system = infinite_file_system.new
Template.parse("{% include 'hi_mom' %}").render!
end
def test_dynamically_choosen_template
assert_equal "Test123", Template.parse("{% include template %}").render("template" => 'Test123')

View File

@@ -0,0 +1,24 @@
require 'test_helper'
class IncrementTagTest < Test::Unit::TestCase
include Liquid
def test_inc
assert_template_result('0','{%increment port %}', {})
assert_template_result('0 1','{%increment port %} {%increment port%}', {})
assert_template_result('0 0 1 2 1',
'{%increment port %} {%increment starboard%} ' +
'{%increment port %} {%increment port%} ' +
'{%increment starboard %}', {})
end
def test_dec
assert_template_result('9','{%decrement port %}', { 'port' => 10})
assert_template_result('-1 -2','{%decrement port %} {%decrement port%}', {})
assert_template_result('1 5 2 2 5',
'{%increment port %} {%increment starboard%} ' +
'{%increment port %} {%decrement port%} ' +
'{%decrement starboard %}', { 'port' => 1, 'starboard' => 5 })
end
end

View File

@@ -0,0 +1,15 @@
require 'test_helper'
class RawTagTest < Test::Unit::TestCase
include Liquid
def test_tag_in_raw
assert_template_result '{% comment %} test {% endcomment %}',
'{% raw %}{% comment %} test {% endcomment %}{% endraw %}'
end
def test_output_in_raw
assert_template_result '{{ test }}',
'{% raw %}{{ test }}{% endraw %}'
end
end

View File

@@ -47,160 +47,6 @@ class StandardTagTest < Test::Unit::TestCase
{%endcomment%}bar')
end
def test_for
assert_template_result(' yo yo yo yo ','{%for item in array%} yo {%endfor%}','array' => [1,2,3,4])
assert_template_result('yoyo','{%for item in array%}yo{%endfor%}','array' => [1,2])
assert_template_result(' yo ','{%for item in array%} yo {%endfor%}','array' => [1])
assert_template_result('','{%for item in array%}{%endfor%}','array' => [1,2])
expected = <<HERE
yo
yo
yo
HERE
template = <<HERE
{%for item in array%}
yo
{%endfor%}
HERE
assert_template_result(expected,template,'array' => [1,2,3])
end
def test_for_with_range
assert_template_result(' 1 2 3 ','{%for item in (1..3) %} {{item}} {%endfor%}')
end
def test_for_with_variable
assert_template_result(' 1 2 3 ','{%for item in array%} {{item}} {%endfor%}','array' => [1,2,3])
assert_template_result('123','{%for item in array%}{{item}}{%endfor%}','array' => [1,2,3])
assert_template_result('123','{% for item in array %}{{item}}{% endfor %}','array' => [1,2,3])
assert_template_result('abcd','{%for item in array%}{{item}}{%endfor%}','array' => ['a','b','c','d'])
assert_template_result('a b c','{%for item in array%}{{item}}{%endfor%}','array' => ['a',' ','b',' ','c'])
assert_template_result('abc','{%for item in array%}{{item}}{%endfor%}','array' => ['a','','b','','c'])
end
def test_for_helpers
assigns = {'array' => [1,2,3] }
assert_template_result(' 1/3 2/3 3/3 ',
'{%for item in array%} {{forloop.index}}/{{forloop.length}} {%endfor%}',
assigns)
assert_template_result(' 1 2 3 ', '{%for item in array%} {{forloop.index}} {%endfor%}', assigns)
assert_template_result(' 0 1 2 ', '{%for item in array%} {{forloop.index0}} {%endfor%}', assigns)
assert_template_result(' 2 1 0 ', '{%for item in array%} {{forloop.rindex0}} {%endfor%}', assigns)
assert_template_result(' 3 2 1 ', '{%for item in array%} {{forloop.rindex}} {%endfor%}', assigns)
assert_template_result(' true false false ', '{%for item in array%} {{forloop.first}} {%endfor%}', assigns)
assert_template_result(' false false true ', '{%for item in array%} {{forloop.last}} {%endfor%}', assigns)
end
def test_for_and_if
assigns = {'array' => [1,2,3] }
assert_template_result('+--',
'{%for item in array%}{% if forloop.first %}+{% else %}-{% endif %}{%endfor%}',
assigns)
end
def test_limiting
assigns = {'array' => [1,2,3,4,5,6,7,8,9,0]}
assert_template_result('12', '{%for i in array limit:2 %}{{ i }}{%endfor%}', assigns)
assert_template_result('1234', '{%for i in array limit:4 %}{{ i }}{%endfor%}', assigns)
assert_template_result('3456', '{%for i in array limit:4 offset:2 %}{{ i }}{%endfor%}', assigns)
assert_template_result('3456', '{%for i in array limit: 4 offset: 2 %}{{ i }}{%endfor%}', assigns)
end
def test_dynamic_variable_limiting
assigns = {'array' => [1,2,3,4,5,6,7,8,9,0]}
assigns['limit'] = 2
assigns['offset'] = 2
assert_template_result('34', '{%for i in array limit: limit offset: offset %}{{ i }}{%endfor%}', assigns)
end
def test_nested_for
assigns = {'array' => [[1,2],[3,4],[5,6]] }
assert_template_result('123456', '{%for item in array%}{%for i in item%}{{ i }}{%endfor%}{%endfor%}', assigns)
end
def test_offset_only
assigns = {'array' => [1,2,3,4,5,6,7,8,9,0]}
assert_template_result('890', '{%for i in array offset:7 %}{{ i }}{%endfor%}', assigns)
end
def test_pause_resume
assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,0]}}
markup = <<-MKUP
{%for i in array.items limit: 3 %}{{i}}{%endfor%}
next
{%for i in array.items offset:continue limit: 3 %}{{i}}{%endfor%}
next
{%for i in array.items offset:continue limit: 3 %}{{i}}{%endfor%}
MKUP
expected = <<-XPCTD
123
next
456
next
789
XPCTD
assert_template_result(expected,markup,assigns)
end
def test_pause_resume_limit
assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,0]}}
markup = <<-MKUP
{%for i in array.items limit:3 %}{{i}}{%endfor%}
next
{%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%}
next
{%for i in array.items offset:continue limit:1 %}{{i}}{%endfor%}
MKUP
expected = <<-XPCTD
123
next
456
next
7
XPCTD
assert_template_result(expected,markup,assigns)
end
def test_pause_resume_BIG_limit
assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,0]}}
markup = <<-MKUP
{%for i in array.items limit:3 %}{{i}}{%endfor%}
next
{%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%}
next
{%for i in array.items offset:continue limit:1000 %}{{i}}{%endfor%}
MKUP
expected = <<-XPCTD
123
next
456
next
7890
XPCTD
assert_template_result(expected,markup,assigns)
end
def test_pause_resume_BIG_offset
assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,0]}}
markup = %q({%for i in array.items limit:3 %}{{i}}{%endfor%}
next
{%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%}
next
{%for i in array.items offset:continue limit:3 offset:1000 %}{{i}}{%endfor%})
expected = %q(123
next
456
next
)
assert_template_result(expected,markup,assigns)
end
def test_assign
assigns = {'var' => 'content' }
assert_template_result('var2: var2:content', 'var2:{{var2}} {%assign var2 = var%} var2:{{var2}}', assigns)
@@ -439,12 +285,6 @@ HERE
assert_template_result('', '{% if null == true %}?{% endif %}', {})
end
def test_for_reversed
assigns = {'array' => [ 1, 2, 3] }
assert_template_result('321','{%for item in array reversed %}{{item}}{%endfor%}',assigns)
end
def test_ifchanged
assigns = {'array' => [ 1, 1, 2, 2, 3, 3] }
assert_template_result('123','{%for item in array%}{%ifchanged%}{{item}}{% endifchanged %}{%endfor%}',assigns)

View File

@@ -1,6 +1,6 @@
require 'test_helper'
class UnlessElseTest < Test::Unit::TestCase
class UnlessElseTagTest < Test::Unit::TestCase
include Liquid
def test_unless

View File

@@ -1,20 +1,17 @@
#!/usr/bin/env ruby
extras_path = File.join File.dirname(__FILE__), 'extra'
$LOAD_PATH.unshift(extras_path) unless $LOAD_PATH.include? extras_path
require 'rubygems' unless RUBY_VERSION =~ /^(?:1.9.*)$/
require 'test/unit'
require 'test/unit/assertions'
require 'caller'
require 'breakpoint'
require 'ruby-debug'
require File.join File.dirname(__FILE__), '..', 'lib', 'liquid'
begin
require 'ruby-debug'
rescue LoadError
puts "Couldn't load ruby-debug. gem install ruby-debug if you need it."
end
require File.join(File.dirname(__FILE__), '..', 'lib', 'liquid')
module Test
module Unit
module Assertions
include Liquid
@@ -28,7 +25,5 @@ module Test
assert_match expected, Template.parse(template).render(assigns)
end
end # Assertions
end # Unit
end # Test