Compare commits

...

135 Commits

Author SHA1 Message Date
Lourens Naudé
e853bf5b84 Also dump object distribution 2015-05-06 10:45:20 -04:00
Lourens Naudé
27248f1eb1 Lazy init context errors and interrupts to prevent excess allocation 2015-05-05 23:40:52 -04:00
Lourens Naudé
174839fbef Reuse filters array during variable parsing 2015-05-05 23:30:05 -04:00
Lourens Naudé
01a86728f2 Remove needless intermediate local var in VariableLookup#initialize 2015-05-05 23:21:28 -04:00
Lourens Naudé
0e38f88b58 Recycle the array buffer in BlockBody#render 2015-05-05 23:02:08 -04:00
Lourens Naudé
5a48edae6a Introduce support for memory usage profiles 2015-05-05 23:00:28 -04:00
Justin Li
76c24db039 Remove ruby-head from allowed failures 2015-05-05 12:49:04 -04:00
James Reid-Smith
251ce7483c Merge pull request #441 from Shopify/remove_context_from_read_template_file
Removed context from read_template_file, fixed tests to match new arity
2015-04-27 12:13:36 -04:00
James Reid-Smith
4592afcc8b Updated History.md and removed a couple remaining methods using the old signature 2015-04-27 15:45:44 +00:00
James Reid-Smith
448766b0c4 Removed context from read_template_file, fixed tests to match new arity 2015-04-27 15:27:03 +00:00
Justin Li
6390652c3f Update changelog with backported patches 2015-04-24 16:09:37 -04:00
Justin Li
f266aee2e5 Slightly more concise issue# reference in changelog 2015-04-21 23:40:42 -04:00
Justin Li
df0649a031 Update changelog 2015-04-21 23:36:54 -04:00
Justin Li
78a5972487 Merge pull request #541 from Shopify/history-sync
Sync History.md for Liquid 4
2015-04-21 23:34:29 -04:00
Justin Li
298ae3357c Merge pull request #551 from Shopify/expose-variable-name
Merge pull request 551
2015-04-21 23:33:13 -04:00
Justin Li
f1f3f57647 Remove command_lookups reader 2015-04-21 00:25:51 -04:00
Justin Li
e5dd63e1fc Expose name, lookups, and command flags from VariableLookup 2015-04-20 17:36:04 -04:00
Justin Li
881f86d698 Merge pull request #550 from Shopify/minitest-fail-workaround
Disable minitest expectation interface due to reckless modification of Object
2015-04-20 10:22:19 -04:00
Justin Li
a1b209d212 Disable minitest expectation interface due to reckless modification of Object 2015-04-20 10:15:19 -04:00
Thierry Joyal
8e5926669b Merge pull request #545 from Shopify/explode-invokable_methods-on-drop
Explode invokable_methods method on Liquid::Drop
2015-04-02 09:02:48 -07:00
Thierry Joyal
8736b602ea Explode invokable_methods method on Liquid::Drop 2015-04-02 13:16:07 +00:00
Justin Li
b8365af07d Add changes for 4.0.0 2015-03-25 14:53:43 -04:00
Justin Li
53842a471e Create history section for 4.0.0 2015-03-25 14:40:19 -04:00
Justin Li
86a82d3039 Merge pull request #540 from Shopify/array-concat
Add array concat filter
2015-03-25 01:42:22 -04:00
Justin Li
2b78e74b4e Add test for concat filter with non-array input 2015-03-25 01:34:47 -04:00
divecch
db396dd739 adding concat filter to append arrays 2015-03-25 01:31:22 -04:00
Justin Li
3213db54d6 Merge pull request #520 from Shopify/forloop-parentloop
Add forloop.parentloop as a reference to the parent loop
2015-03-25 01:22:35 -04:00
Justin Li
97a3f145a1 Merge pull request #499 from kreynolds/to_date_downcase_regression
Fix case sensitivity regression in date standard filter
2015-03-25 01:22:04 -04:00
Florian Weingarten
2fbe813770 Merge pull request #539 from Dorian/patch-1
Update module_ex.rb code documentation and code style
2015-03-24 15:21:22 +01:00
Dorian Marié
23a23c6419 Update module_ex.rb code documentation and code style
Didn't look good on rubydoc.info: http://i.imgur.com/469N92P.png
2015-03-24 14:09:08 +01:00
Dylan Thacker-Smith
63eb1aac69 Merge pull request #519 from Shopify/remove-filter-method-blacklist
Allow filters to redefine Object methods to make them invokable.
2015-02-04 18:07:51 -05:00
Justin Li
205bd19d3f Add forloop.parentloop as a reference to the parent loop 2015-02-04 12:43:09 -05:00
Dylan Thacker-Smith
950f062041 Allow filters to redefine Object methods to make them invokable. 2015-02-03 13:51:33 -05:00
Tobias Lütke
3476a556dd Merge pull request #512 from Shopify/fix_tobi_name
Fix Tobi last name on gemspec
2015-01-23 21:24:04 -05:00
Arthur Neves
d2ef9cef10 master is 4.0.0 2015-01-23 10:49:07 -05:00
Arthur Neves
0021c93fef Add ruby 2.2 to travis
and allow failure on ruby head
2015-01-23 10:42:26 -05:00
Arthur Neves
dcf7064460 Fix Tobi last name on gemspec 2015-01-23 10:21:40 -05:00
Florian Weingarten
bebd3570ee Merge pull request #506 from Shopify/fix_capture_with_hyphen
Use VariableSignature as Syntax for Capture tag to allow hyphens in variable names
2015-01-10 23:27:00 -05:00
Florian Weingarten
7cfee1616a Use VariableSignature as Syntax for Capture tag to allow hyphens in variable names 2015-01-09 14:15:26 +00:00
Arthur Nogueira Neves
4b0a7c5d1d Merge pull request #504 from alfredxing/duplicate-keys
Remove duplicate `index0` key in TableRow tag
2014-12-30 13:15:10 -05:00
Alfred Xing
5df1a262ad Remove duplicate key in hash 2014-12-25 12:12:42 -08:00
Kelley Reynolds
84fddba2e1 Remove regex for downcase and is_a?(String) 2014-12-18 13:01:23 -05:00
Kelley Reynolds
8b0774b519 Fix case sensitivity regression in date standard filter 2014-12-16 10:37:05 -05:00
Justin Li
e2f8b28f56 Merge pull request #492 from Shopify/resource-counting-perf
Resource counting perf
2014-12-11 16:05:41 -05:00
Justin Li
3080f95a4f Make render_length tests stricter 2014-12-11 10:41:47 -05:00
Justin Li
cc57908c03 Add test for render_length persisting between block bodies 2014-12-11 10:38:47 -05:00
Justin Li
4df4f218cf Use same template instance 2014-12-09 17:25:15 -05:00
Justin Li
c2f71ee86b Reset resource consumption before each render 2014-12-09 17:23:07 -05:00
Justin Li
9f7e601110 Convert render output to strings in BlockBody 2014-12-05 15:17:09 -05:00
Justin Li
3755031c18 Merge pull request #485 from Shopify/lazy-load-profiler-hooks
Defer loading profiler hooks
2014-12-05 15:10:16 -05:00
Justin Li
b628477af1 Disambiguate checking if Liquid::Profiler is defined 2014-12-04 17:51:54 -05:00
Justin Li
dd455a6361 Force user to require the profiler themselves 2014-12-04 17:48:26 -05:00
Justin Li
8c70682d6b Don't automatically load hooks 2014-12-04 17:39:41 -05:00
Justin Li
742b3c69bb Remove commented code 2014-12-04 16:30:37 -05:00
Justin Li
1593b784a7 Simplify interface for setting template resource limits 2014-12-04 16:18:21 -05:00
Justin Li
db00ec8b32 Move resource limit tracking to its own class 2014-12-04 16:18:09 -05:00
Justin Li
3ca40b5dea Merge pull request #491 from Shopify/drop-ruby-1-9
Drop Ruby 1.9 from CI, add Ruby head
2014-12-03 12:52:10 -05:00
Justin Li
378775992f Drop Ruby 1.9 from CI, add Ruby head 2014-12-02 14:33:51 -05:00
Florian Weingarten
319400ea23 Merge pull request #489 from alex-ross/patch-1
Fixes syntax error in documentation for unless tag
2014-11-19 14:02:58 +01:00
Alexander Ross
289a03f9d7 Fixes syntax error in documentation for unless tag 2014-11-19 10:49:58 +01:00
Justin Li
a0710f4c70 Merge pull request #486 from Shopify/fix-exponential-warnings
Fix #warnings taking exponential time to compute
2014-11-12 17:22:16 -05:00
Justin Li
737be1a0c1 Use Timeout#timeout for warnings tests 2014-11-12 17:03:48 -05:00
Justin Li
1673098126 Handle potential case where warnings returns nil 2014-11-12 16:46:10 -05:00
Justin Li
422bafd66a Fix #warnings taking exponential time to compute 2014-11-12 16:12:00 -05:00
Justin Li
c0aab820ed Lazily load profiler hooks 2014-11-12 00:05:01 -05:00
Florian Weingarten
3321cffe08 Merge pull request #482 from joshk/patch-1
Use the new beta build env on Travis
2014-11-07 03:06:52 +01:00
Josh Kalderimis
f2772518b0 Use the new beta build env on Travis
job start in seconds, instead of 20-120 seconds
2014-11-07 14:54:21 +13:00
Justin Li
76ef675eb2 Merge pull request #481 from Shopify/fix-nil-blank
Coerce regex @blank output to a boolean
2014-11-06 13:03:15 -05:00
Justin Li
e5fd4d929f Coerce regex @blank output to a boolean 2014-11-05 20:44:06 -05:00
Justin Li
2e42c7be1f Merge pull request #480 from Shopify/number_variables
Add quirks test for variables with number prefixes
2014-11-05 12:05:21 -05:00
Justin Li
95b031ee04 Add quirks test for extra dots in ranges 2014-11-05 11:41:12 -05:00
Justin Li
4d97a714a9 Add quirks test for variables with number prefixes 2014-11-05 10:56:58 -05:00
Justin Li
aa182f64b4 Merge pull request #479 from Shopify/tweaks-for-c
Tweaks for C
2014-11-04 14:02:14 -05:00
Justin Li
4e870302b1 Add env var for saving stackprof graphviz output 2014-11-04 18:38:14 +00:00
Justin Li
098c89b5f5 Convenience methods for raising terminator syntax errors 2014-11-04 18:38:08 +00:00
Justin Li
70c45f8cd8 Use SVG badge URLs
[ci skip]
2014-11-03 17:41:42 -05:00
Justin Li
12d526a05c Merge pull request #458 from Shopify/block-body
Create a BlockBody class to decouple block body parsing from tags.
2014-11-03 17:34:39 -05:00
Dylan Thacker-Smith
2fd8ad08c0 Remove unused local variable that was accidentally added. 2014-11-03 17:07:42 -05:00
Dylan Thacker-Smith
15e1d46125 Avoid storing options instance variable in BlockBody.
There is no need to pass parse options to the BlockBody initializer, since
it does all the parsing in the parse method, unlike tags which parse the
tag markup in the initializer.
2014-11-03 17:07:42 -05:00
Dylan Thacker-Smith
73fcd42403 Create a BlockBody class to decouple block body parsing from tags. 2014-11-03 17:07:42 -05:00
Justin Li
263e90e772 Merge pull request #478 from Shopify/numbers-in-identifiers
Use a single token for identifiers
2014-10-30 21:59:26 -04:00
Justin Li
81770f094d Remove unnecessary + 2014-10-29 13:39:43 -04:00
Justin Li
dd5ee81089 Disallow number and dash identifier prefixes 2014-10-29 12:08:00 -04:00
Justin Li
a07e382617 Use a single token for identifiers 2014-10-29 11:28:41 -04:00
Justin Li
4dc682313f Merge pull request #476 from Shopify/missing-variable-name-error
Disallow filters with no variable in strict mode
2014-10-27 13:56:11 -04:00
Justin Li
5616ddf00e Remove obsolete comment 2014-10-27 13:44:14 -04:00
Justin Li
fcb23a4cd2 Disallow filters with no variable in strict mode 2014-10-27 13:34:27 -04:00
Justin Li
a8f60ff6b1 Merge pull request #472 from Shopify/fix-leaky-test
Fix test leaking error_mode, fix equality check for VariableLookup
2014-10-23 10:12:41 -04:00
Justin Li
a206c8301d Fix test leaking error_mode, fix equality check for VariableLookup 2014-10-22 15:40:41 -04:00
Justin Li
ee0de01480 Merge pull request #469 from Shopify/falsy-variable-fix
Fix case where a variable name is falsy
2014-10-21 15:06:34 -04:00
Justin Li
887b05e6ed Clarify test name 2014-10-21 14:06:30 -04:00
Justin Li
5d68e8803f Ensure nil works as a variable name 2014-10-21 14:03:10 -04:00
Justin Li
dedd1d3dc0 Fix case where a variable name is falsy 2014-10-21 12:09:26 -04:00
Dylan Thacker-Smith
d9ae36ec40 Merge pull request #466 from Shopify/remove-expression-cache
Remove expression cache
2014-10-20 13:57:17 -04:00
Dylan Thacker-Smith
b9ac3fef8f Remove the quotes from the partial string in the profiler timing objects. 2014-10-18 16:26:18 -04:00
Dylan Thacker-Smith
f5faa4858c Remove parsed expression cache. 2014-10-18 15:03:40 -04:00
Dylan Thacker-Smith
bc5e444d04 Use Expression.parse and Context#evaluate in the Include class. 2014-10-18 15:03:40 -04:00
Dylan Thacker-Smith
3a4b63f37e Use Expression.parse and Context#evaluate in the TableRow class. 2014-10-18 15:03:40 -04:00
Dylan Thacker-Smith
a1a128db19 Refactor Condition so that it takes a parsed expression. 2014-10-18 15:03:40 -04:00
Dylan Thacker-Smith
d502b9282a Use Expression.parse and Context#evaluate in the For class. 2014-10-18 15:03:36 -04:00
Dylan Thacker-Smith
fee8e41466 Use Expression.parse and Context#evaluate in the Cycle class. 2014-10-18 14:27:58 -04:00
Dylan Thacker-Smith
37260f17ff Use Expression.parse and Context#evaluate in the Condition class. 2014-10-18 14:27:58 -04:00
Florian Weingarten
2da9d49478 Merge pull request #465 from Shopify/avoid_multi_assigns
Avoid parallel assignments
2014-10-18 16:19:02 +02:00
Florian Weingarten
7196a2d58e Avoid parallel assignments 2014-10-18 13:58:32 +00:00
Justin Li
a056f6521c Merge pull request #463 from Shopify/stricter-identifiers
Separate ? and - into special tokens
2014-10-17 13:45:48 -04:00
Justin Li
de16db9b72 Don't allow - to end a variable name 2014-10-17 13:38:07 -04:00
Justin Li
b4ea483c4e Separate ? and - into special tokens 2014-10-17 13:30:54 -04:00
Justin Li
7843bcca8d Merge pull request #443 from Shopify/completely-parse-variables
Parse expressions in Liquid::Variable#parse.
2014-10-17 13:12:46 -04:00
Florian Weingarten
76ea5596ff Merge pull request #462 from Shopify/flat_map
nodelist flat_map over map.flatten
2014-10-17 18:32:00 +02:00
Florian Weingarten
f9318e8c93 flat_map 2014-10-17 16:11:12 +00:00
Florian Weingarten
71253ec6f9 Merge pull request #459 from Shopify/pop_vs_shift
Use pop over shift to avoid reverse
2014-10-15 21:42:53 +02:00
Florian Weingarten
0fa075b879 Use pop over shift to avoid reverse 2014-10-15 19:26:39 +00:00
Dylan Thacker-Smith
6d080afd22 Merge pull request #446 from Shopify/remove-end-tag
Remove unused Block#end_tag method.
2014-10-14 03:03:31 -04:00
Dylan Thacker-Smith
a67e2a0a00 Remove unused Block#end_tag method.
Although the method is called, it is defined with an empty body and not
overridden to do anything else.
2014-10-14 02:58:11 -04:00
Dylan Thacker-Smith
f387508666 Parse expressions in Liquid::Variable#parse. 2014-10-08 21:06:59 -04:00
Florian Weingarten
632b1fb702 Merge pull request #455 from Shopify/parse_error_line_numbers
Line numbers for all parse errors
2014-10-04 17:53:30 +02:00
Dylan Thacker-Smith
d84870d7a5 Test line number of errors in nested blocks. 2014-10-03 16:25:12 -05:00
Florian Weingarten
584b492e70 Line numbers for all parse errors 2014-10-03 21:00:31 +00:00
Dylan Thacker-Smith
b79c9cb9bf Merge pull request #453 from Shopify/no-modify-default-resource-limit
Avoid modifying the default resources limits hash.
2014-10-01 19:02:09 -05:00
Dylan Thacker-Smith
cf5ccede50 Avoid modifying the default resources limits hash. 2014-10-01 18:51:06 -05:00
Evan Huus
23622a9739 Merge pull request #440 from Shopify/drop-tainting
Variable tainting
2014-09-22 13:43:35 -04:00
Florian Weingarten
7ba5a6ab75 Merge pull request #450 from Shopify/additional_test_for_includes
Regression test for including assignments
2014-09-18 21:05:37 +02:00
Florian Weingarten
be3d261e11 Regression test for including assignments 2014-09-18 10:37:44 +00:00
Evan Huus
eeb061ef44 Address code review comments
- clean up comment wording
- fix potentially leaky tests
2014-09-16 17:23:26 +00:00
Evan Huus
67b2c320a1 Add tainting tests 2014-09-16 17:23:26 +00:00
Evan Huus
1d151885be Auto-untaint variables passed through 'escape' 2014-09-16 17:23:26 +00:00
Evan Huus
e836024dd9 Check and handle when a tainted variable is used 2014-09-16 17:23:26 +00:00
Dylan Thacker-Smith
638455ed92 Merge pull request #448 from Shopify/remove-unused-filter-not-found-error
Remove Liquid::FilterNotFoundError since it is never raised.
2014-09-15 17:43:33 -04:00
Dylan Thacker-Smith
b2a74883e9 Remove Liquid::FilterNotFoundError since it is never raised. 2014-09-15 17:42:07 -04:00
Dylan Thacker-Smith
6875e5e16f Merge pull request #449 from Shopify/fix-flaky-total-render-time-test
Fix flaky test which assumes total_render_time can't be 0.
2014-09-15 17:41:41 -04:00
Dylan Thacker-Smith
a5717a3f8d Fix flaky test which assumes total_render_time can't be 0.
jruby has millisecond precision for Time.now, so total_render_time can be 0
due to this lack of precision.
2014-09-15 17:26:55 -04:00
Dylan Thacker-Smith
804fcfebd1 Merge pull request #444 from Shopify/remove-block-children
Avoid keeping track of two lists of nodes during parsing.
2014-09-15 09:56:08 -04:00
Dylan Thacker-Smith
b37ee5684a Merge pull request #445 from Shopify/prefer-super-over-render-all
Use super rather than render_all in single block render classes.
2014-09-15 09:53:54 -04:00
Dylan Thacker-Smith
0573b63b4c Use super rather than render_all in single block render classes. 2014-09-12 16:58:07 -04:00
Dylan Thacker-Smith
29c21d7867 Avoid keeping track of two lists of nodes during parsing. 2014-09-12 16:43:00 -04:00
73 changed files with 1100 additions and 602 deletions

View File

@@ -1,13 +1,17 @@
language: ruby
rvm: rvm:
- 1.9
- 2.0 - 2.0
- 2.1 - 2.1
- jruby-19mode - 2.2
- ruby-head
- jruby-head - jruby-head
- rbx-2 - rbx-2
sudo: false
matrix: matrix:
allow_failures: allow_failures:
- rvm: rbx-2
- rvm: jruby-head - rvm: jruby-head
script: "rake test" script: "rake test"

View File

@@ -2,6 +2,7 @@ source 'https://rubygems.org'
gemspec gemspec
gem 'stackprof', platforms: :mri_21 gem 'stackprof', platforms: :mri_21
gem 'allocation_tracer', platforms: :mri_21
group :test do group :test do
gem 'spy', '0.4.1' gem 'spy', '0.4.1'

View File

@@ -1,45 +1,69 @@
# Liquid Version History # Liquid Change Log
## 3.0.0 / not yet released / branch "master" ## 4.0.0 / not yet released / branch "master"
### Changed
* Add forloop.parentloop as a reference to the parent loop (#520) [Justin Li, pushrax]
* Block parsing moved to BlockBody class (#458) [Dylan Thacker-Smith, dylanahsmith]
* Add concat filter to concatenate arrays (#429) [Diogo Beato, dvbeato]
* Ruby 1.9 support dropped (#491) [Justin Li, pushrax]
* Liquid::Template.file_system's read_template_file method is no longer passed the context. (#441) [James Reid-Smith, sunblaze]
* ... ### Fixed
* Fixed condition with wrong data types, see #423 [Bogdan Gusiev] * Fix capturing into variables with a hyphen in the name (#505) [Florian Weingarten, fw42]
* Add url_encode to standard filters, see #421 [Derrick Reimer, djreimer] * Fix case sensitivity regression in date standard filter (#499) [Kelley Reynolds, kreynolds]
* Disallow filters with no variable in strict mode (#475) [Justin Li, pushrax]
* Disallow variable names in the strict parser that are not valid in the lax parser (#463) [Justin Li, pushrax]
* Fix BlockBody#warnings taking exponential time to compute (#486) [Justin Li, pushrax]
## 3.0.2 / 2015-04-24 / branch "3-0-stable"
* Expose VariableLookup private members (#551) [Justin Li, pushrax]
* Documentation fixes
## 3.0.1 / 2015-01-23
* Remove duplicate `index0` key in TableRow tag (#502) [Alfred Xing]
## 3.0.0 / 2014-11-12
* Removed Block#end_tag. Instead, override parse with `super` followed by your code. See #446 [Dylan Thacker-Smith, dylanahsmith]
* Fixed condition with wrong data types (#423) [Bogdan Gusiev]
* Add url_encode to standard filters (#421) [Derrick Reimer, djreimer]
* Add uniq to standard filters [Florian Weingarten, fw42] * Add uniq to standard filters [Florian Weingarten, fw42]
* Add exception_handler feature, see #397 and #254 [Bogdan Gusiev, bogdan and Florian Weingarten, fw42] * Add exception_handler feature (#397) and #254 [Bogdan Gusiev, bogdan and Florian Weingarten, fw42]
* Optimize variable parsing to avoid repeated regex evaluation during template rendering #383 [Jason Hiltz-Laforge, jasonhl] * Optimize variable parsing to avoid repeated regex evaluation during template rendering #383 [Jason Hiltz-Laforge, jasonhl]
* Optimize checking for block interrupts to reduce object allocation #380 [Jason Hiltz-Laforge, jasonhl] * Optimize checking for block interrupts to reduce object allocation #380 [Jason Hiltz-Laforge, jasonhl]
* Properly set context rethrow_errors on render! #349 [Thierry Joyal, tjoyal] * Properly set context rethrow_errors on render! #349 [Thierry Joyal, tjoyal]
* Fix broken rendering of variables which are equal to false, see #345 [Florian Weingarten, fw42] * Fix broken rendering of variables which are equal to false (#345) [Florian Weingarten, fw42]
* Remove ActionView template handler [Dylan Thacker-Smith, dylanahsmith] * Remove ActionView template handler [Dylan Thacker-Smith, dylanahsmith]
* Freeze lots of string literals for new Ruby 2.1 optimization, see #297 [Florian Weingarten, fw42] * Freeze lots of string literals for new Ruby 2.1 optimization (#297) [Florian Weingarten, fw42]
* Allow newlines in tags and variables, see #324 [Dylan Thacker-Smith, dylanahsmith] * Allow newlines in tags and variables (#324) [Dylan Thacker-Smith, dylanahsmith]
* Tag#parse is called after initialize, which now takes options instead of tokens as the 3rd argument. See #321 [Dylan Thacker-Smith, dylanahsmith] * Tag#parse is called after initialize, which now takes options instead of tokens as the 3rd argument. See #321 [Dylan Thacker-Smith, dylanahsmith]
* Raise `Liquid::ArgumentError` instead of `::ArgumentError` when filter has wrong number of arguments #309 [Bogdan Gusiev, bogdan] * Raise `Liquid::ArgumentError` instead of `::ArgumentError` when filter has wrong number of arguments #309 [Bogdan Gusiev, bogdan]
* Add a to_s default for liquid drops, see #306 [Adam Doeler, releod] * Add a to_s default for liquid drops (#306) [Adam Doeler, releod]
* Add strip, lstrip, and rstrip to standard filters [Florian Weingarten, fw42] * Add strip, lstrip, and rstrip to standard filters [Florian Weingarten, fw42]
* Make if, for & case tags return complete and consistent nodelists, see #250 [Nick Jones, dntj] * Make if, for & case tags return complete and consistent nodelists (#250) [Nick Jones, dntj]
* Prevent arbitrary method invocation on condition objects, see #274 [Dylan Thacker-Smith, dylanahsmith] * Prevent arbitrary method invocation on condition objects (#274) [Dylan Thacker-Smith, dylanahsmith]
* Don't call to_sym when creating conditions for security reasons, see #273 [Bouke van der Bijl, bouk] * Don't call to_sym when creating conditions for security reasons (#273) [Bouke van der Bijl, bouk]
* Fix resource counting bug with respond_to?(:length), see #263 [Florian Weingarten, fw42] * Fix resource counting bug with respond_to?(:length) (#263) [Florian Weingarten, fw42]
* Allow specifying custom patterns for template filenames, see #284 [Andrei Gladkyi, agladkyi] * Allow specifying custom patterns for template filenames (#284) [Andrei Gladkyi, agladkyi]
* Allow drops to optimize loading a slice of elements, see #282 [Tom Burns, boourns] * Allow drops to optimize loading a slice of elements (#282) [Tom Burns, boourns]
* Support for passing variables to snippets in subdirs, see #271 [Joost Hietbrink, joost] * Support for passing variables to snippets in subdirs (#271) [Joost Hietbrink, joost]
* Add a class cache to avoid runtime extend calls, see #249 [James Tucker, raggi] * Add a class cache to avoid runtime extend calls (#249) [James Tucker, raggi]
* Remove some legacy Ruby 1.8 compatibility code, see #276 [Florian Weingarten, fw42] * Remove some legacy Ruby 1.8 compatibility code (#276) [Florian Weingarten, fw42]
* Add default filter to standard filters, see #267 [Derrick Reimer, djreimer] * Add default filter to standard filters (#267) [Derrick Reimer, djreimer]
* Add optional strict parsing and warn parsing, see #235 [Tristan Hume, trishume] * Add optional strict parsing and warn parsing (#235) [Tristan Hume, trishume]
* Add I18n syntax error translation, see #241 [Simon Hørup Eskildsen, Sirupsen] * Add I18n syntax error translation (#241) [Simon Hørup Eskildsen, Sirupsen]
* Make sort filter work on enumerable drops, see #239 [Florian Weingarten, fw42] * Make sort filter work on enumerable drops (#239) [Florian Weingarten, fw42]
* Fix clashing method names in enumerable drops, see #238 [Florian Weingarten, fw42] * Fix clashing method names in enumerable drops (#238) [Florian Weingarten, fw42]
* Make map filter work on enumerable drops, see #233 [Florian Weingarten, fw42] * Make map filter work on enumerable drops (#233) [Florian Weingarten, fw42]
* Improved whitespace stripping for blank blocks, related to #216 [Florian Weingarten, fw42] * Improved whitespace stripping for blank blocks, related to #216 [Florian Weingarten, fw42]
## 2.6.1 / 2014-01-10 / branch "2-6-stable" ## 2.6.1 / 2014-01-10 / branch "2-6-stable"
Security fix, cherry-picked from master (4e14a65): Security fix, cherry-picked from master (4e14a65):
* Don't call to_sym when creating conditions for security reasons, see #273 [Bouke van der Bijl, bouk] * Don't call to_sym when creating conditions for security reasons (#273) [Bouke van der Bijl, bouk]
* Prevent arbitrary method invocation on condition objects, see #274 [Dylan Thacker-Smith, dylanahsmith] * Prevent arbitrary method invocation on condition objects (#274) [Dylan Thacker-Smith, dylanahsmith]
## 2.6.0 / 2013-11-25 ## 2.6.0 / 2013-11-25
@@ -68,12 +92,12 @@ The following releases will only be tested against Ruby 1.9 and Ruby 2.0 and are
## 2.5.5 / 2014-01-10 / branch "2-5-stable" ## 2.5.5 / 2014-01-10 / branch "2-5-stable"
Security fix, cherry-picked from master (4e14a65): Security fix, cherry-picked from master (4e14a65):
* Don't call to_sym when creating conditions for security reasons, see #273 [Bouke van der Bijl, bouk] * Don't call to_sym when creating conditions for security reasons (#273) [Bouke van der Bijl, bouk]
* Prevent arbitrary method invocation on condition objects, see #274 [Dylan Thacker-Smith, dylanahsmith] * Prevent arbitrary method invocation on condition objects (#274) [Dylan Thacker-Smith, dylanahsmith]
## 2.5.4 / 2013-11-11 ## 2.5.4 / 2013-11-11
* Fix "can't convert Fixnum into String" for "replace", see #173, [wǒ_is神仙, jsw0528] * Fix "can't convert Fixnum into String" for "replace" (#173), [wǒ_is神仙, jsw0528]
## 2.5.3 / 2013-10-09 ## 2.5.3 / 2013-10-09

View File

@@ -1,5 +1,5 @@
[![Build Status](https://secure.travis-ci.org/Shopify/liquid.png?branch=master)](http://travis-ci.org/Shopify/liquid) [![Build Status](https://api.travis-ci.org/Shopify/liquid.svg?branch=master)](http://travis-ci.org/Shopify/liquid)
[![Inline docs](http://inch-ci.org/github/Shopify/liquid.png)](http://inch-ci.org/github/Shopify/liquid) [![Inline docs](http://inch-ci.org/github/Shopify/liquid.svg?branch=master)](http://inch-ci.org/github/Shopify/liquid)
# Liquid template engine # Liquid template engine

View File

@@ -71,6 +71,20 @@ namespace :profile do
end end
namespace :memory do
desc "Run the liquid memory tracer"
task :run do
ruby "./performance/memory.rb"
end
desc "Run the liquid memory tracer with strict parsing"
task :strict do
ruby "./performance/memory.rb strict"
end
end
desc "Run example" desc "Run example"
task :example do task :example do
ruby "-w -d -Ilib example/server/server.rb" ruby "-w -d -Ilib example/server/server.rb"

View File

@@ -23,7 +23,7 @@ class Servlet < LiquidServlet
end end
def products def products
{ 'products' => products_list, 'description' => description, 'section' => 'Snowboards', 'cool_products' => true} { 'products' => products_list, 'more_products' => more_products_list, 'description' => description, 'section' => 'Snowboards', 'cool_products' => true}
end end
private private
@@ -34,6 +34,11 @@ 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'}] {'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
def more_products_list
[{'name' => 'Arbor Catalyst', 'price' => 39900, 'description' => 'the *arbor catalyst* is an advanced drop-through for freestyle and flatground performance and versatility' },
{'name' => 'Arbor Fish', 'price' => 40000, 'description' => 'the *arbor fish* is a compact pin that features an extended wheelbase and time-honored teardrop shape'}]
end
def description def description
"List of Products ~ This is a list of products with price and description." "List of Products ~ This is a list of products with price and description."
end end

View File

@@ -11,7 +11,8 @@ class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet
private private
def handle(type, req, res) def handle(type, req, res)
@request, @response = req, res @request = req
@response = res
@request.path_info =~ /(\w+)\z/ @request.path_info =~ /(\w+)\z/
@action = $1 || 'index' @action = $1 || 'index'

View File

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

View File

@@ -57,11 +57,13 @@ require 'liquid/context'
require 'liquid/parser_switching' require 'liquid/parser_switching'
require 'liquid/tag' require 'liquid/tag'
require 'liquid/block' require 'liquid/block'
require 'liquid/block_body'
require 'liquid/document' require 'liquid/document'
require 'liquid/variable' require 'liquid/variable'
require 'liquid/variable_lookup' require 'liquid/variable_lookup'
require 'liquid/range_lookup' require 'liquid/range_lookup'
require 'liquid/file_system' require 'liquid/file_system'
require 'liquid/resource_limits'
require 'liquid/template' require 'liquid/template'
require 'liquid/standardfilters' require 'liquid/standardfilters'
require 'liquid/condition' require 'liquid/condition'
@@ -72,6 +74,3 @@ require 'liquid/token'
# Load all the tags of the standard library # Load all the tags of the standard library
# #
Dir[File.dirname(__FILE__) + '/liquid/tags/*.rb'].each { |f| require f } Dir[File.dirname(__FILE__) + '/liquid/tags/*.rb'].each { |f| require f }
require 'liquid/profiler'
require 'liquid/profiler/hooks'

View File

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

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

@@ -0,0 +1,132 @@
module Liquid
class BlockBody
FullToken = /\A#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
ContentOfVariable = /\A#{VariableStart}(.*)#{VariableEnd}\z/om
TAGSTART = "{%".freeze
VARSTART = "{{".freeze
attr_reader :nodelist
def initialize
@nodelist = []
@blank = true
end
def parse(tokens, options)
while token = tokens.shift
begin
unless token.empty?
case
when token.start_with?(TAGSTART)
if token =~ FullToken
tag_name = $1
markup = $2
# fetch the tag from registered blocks
if tag = Template.tags[tag_name]
markup = token.child(markup) if token.is_a?(Token)
new_tag = tag.parse(tag_name, markup, tokens, options)
new_tag.line_number = token.line_number if token.is_a?(Token)
@blank &&= new_tag.blank?
@nodelist << new_tag
else
# end parsing if we reach an unknown tag and let the caller decide
# determine how to proceed
return yield tag_name, markup
end
else
raise_missing_tag_terminator(token, options)
end
when token.start_with?(VARSTART)
new_var = create_variable(token, options)
new_var.line_number = token.line_number if token.is_a?(Token)
@nodelist << new_var
@blank = false
else
@nodelist << token
@blank &&= !!(token =~ /\A\s*\z/)
end
end
rescue SyntaxError => e
e.set_line_number_from_token(token)
raise
end
end
yield nil, nil
end
def blank?
@blank
end
def warnings
all_warnings = []
nodelist.each do |node|
all_warnings.concat(node.warnings || []) if node.respond_to?(:warnings)
end
all_warnings
end
def render(context)
@output ||= []
@output.clear
context.resource_limits.render_score += @nodelist.length
@nodelist.each do |token|
# Break out if we have any unhanded interrupts.
break if context.has_interrupt?
begin
# If we get an Interrupt that means the block must stop processing. An
# Interrupt is any command that stops block execution such as {% break %}
# or {% continue %}
if token.is_a?(Continue) or token.is_a?(Break)
context.push_interrupt(token.interrupt)
break
end
token_output = render_token(token, context)
unless token.is_a?(Block) && token.blank?
@output << token_output
end
rescue MemoryError => e
raise e
rescue ::StandardError => e
@output << context.handle_error(e, token)
end
end
@output.join
end
private
def render_token(token, context)
token_output = (token.respond_to?(:render) ? token.render(context) : token)
token_str = token_output.is_a?(Array) ? token_output.join : token_output.to_s
context.resource_limits.render_length += token_str.length
if context.resource_limits.reached?
raise MemoryError.new("Memory limits exceeded".freeze)
end
token_str
end
def create_variable(token, options)
token.scan(ContentOfVariable) do |content|
markup = token.is_a?(Token) ? token.child(content.first) : content.first
return Variable.new(markup, options)
end
raise_missing_variable_terminator(token, options)
end
def raise_missing_tag_terminator(token, options)
raise SyntaxError.new(options[:locale].t("errors.syntax.tag_termination".freeze, :token => token, :tag_end => TagEnd.inspect))
end
def raise_missing_variable_terminator(token, options)
raise SyntaxError.new(options[:locale].t("errors.syntax.variable_termination".freeze, :token => token, :tag_end => VariableEnd.inspect))
end
end
end

View File

@@ -3,7 +3,7 @@ module Liquid
# #
# Example: # Example:
# #
# c = Condition.new('1', '==', '1') # c = Condition.new(1, '==', 1)
# c.evaluate #=> true # c.evaluate #=> true
# #
class Condition #:nodoc: class Condition #:nodoc:
@@ -28,7 +28,9 @@ module Liquid
attr_accessor :left, :operator, :right attr_accessor :left, :operator, :right
def initialize(left = nil, operator = nil, right = nil) def initialize(left = nil, operator = nil, right = nil)
@left, @operator, @right = left, operator, right @left = left
@operator = operator
@right = right
@child_relation = nil @child_relation = nil
@child_condition = nil @child_condition = nil
end end
@@ -47,11 +49,13 @@ module Liquid
end end
def or(condition) def or(condition)
@child_relation, @child_condition = :or, condition @child_relation = :or
@child_condition = condition
end end
def and(condition) def and(condition)
@child_relation, @child_condition = :and, condition @child_relation = :and
@child_condition = condition
end end
def attach(attachment) def attach(attachment)
@@ -92,9 +96,10 @@ module Liquid
# If the operator is empty this means that the decision statement is just # If the operator is empty this means that the decision statement is just
# a single variable. We can just poll this variable from the context and # a single variable. We can just poll this variable from the context and
# return this as the result. # return this as the result.
return context[left] if op == nil return context.evaluate(left) if op == nil
left, right = context[left], context[right] left = context.evaluate(left)
right = context.evaluate(right)
operation = self.class.operators[op] || raise(Liquid::ArgumentError.new("Unknown operator #{op}")) operation = self.class.operators[op] || raise(Liquid::ArgumentError.new("Unknown operator #{op}"))

View File

@@ -13,18 +13,14 @@ module Liquid
# #
# context['bob'] #=> nil class Context # context['bob'] #=> nil class Context
class Context class Context
attr_reader :scopes, :errors, :registers, :environments, :resource_limits attr_reader :scopes, :registers, :environments, :resource_limits
attr_accessor :exception_handler attr_accessor :exception_handler
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil) def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil)
@environments = [environments].flatten @environments = [environments].flatten
@scopes = [(outer_scope || {})] @scopes = [(outer_scope || {})]
@registers = registers @registers = registers
@errors = [] @resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
@resource_limits = resource_limits || Template.default_resource_limits
@resource_limits[:render_score_current] = 0
@resource_limits[:assign_score_current] = 0
@parsed_expression = Hash.new{ |cache, markup| cache[markup] = Expression.parse(markup) }
squash_instance_assigns_with_environments squash_instance_assigns_with_environments
@this_stack_used = false @this_stack_used = false
@@ -33,22 +29,12 @@ module Liquid
self.exception_handler = ->(e) { true } self.exception_handler = ->(e) { true }
end end
@interrupts = [] @interrupts = nil
@filters = [] @filters = []
end end
def increment_used_resources(key, obj) def errors
@resource_limits[key] += if obj.kind_of?(String) || obj.kind_of?(Array) || obj.kind_of?(Hash) @errors ||= []
obj.length
else
1
end
end
def resource_limits_reached?
(@resource_limits[:render_length_limit] && @resource_limits[:render_length_current] > @resource_limits[:render_length_limit]) ||
(@resource_limits[:render_score_limit] && @resource_limits[:render_score_current] > @resource_limits[:render_score_limit] ) ||
(@resource_limits[:assign_score_limit] && @resource_limits[:assign_score_current] > @resource_limits[:assign_score_limit] )
end end
def strainer def strainer
@@ -61,36 +47,23 @@ module Liquid
# for that # for that
def add_filters(filters) def add_filters(filters)
filters = [filters].flatten.compact filters = [filters].flatten.compact
filters.each do |f| @filters += filters
raise ArgumentError, "Expected module but got: #{f.class}" unless f.is_a?(Module) @strainer = nil
Strainer.add_known_filter(f)
end
# If strainer is already setup then there's no choice but to use a runtime
# extend call. If strainer is not yet created, we can utilize strainers
# cached class based API, which avoids busting the method cache.
if @strainer
filters.each do |f|
strainer.extend(f)
end
else
@filters.concat filters
end
end end
# are there any not handled interrupts? # are there any not handled interrupts?
def has_interrupt? def has_interrupt?
!@interrupts.empty? @interrupts && !@interrupts.empty?
end end
# push an interrupt to the stack. this interrupt is considered not handled. # push an interrupt to the stack. this interrupt is considered not handled.
def push_interrupt(e) def push_interrupt(e)
@interrupts.push(e) (@interrupts ||= []).push(e)
end end
# pop an interrupt from the stack # pop an interrupt from the stack
def pop_interrupt def pop_interrupt
@interrupts.pop @interrupts.pop if @interrupts
end end
@@ -170,7 +143,7 @@ module Liquid
# Example: # Example:
# products == empty #=> products.empty? # products == empty #=> products.empty?
def [](expression) def [](expression)
evaluate(@parsed_expression[expression]) evaluate(Expression.parse(expression))
end end
def has_key?(key) def has_key?(key)

View File

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

View File

@@ -62,6 +62,10 @@ module Liquid
# Check for method existence without invoking respond_to?, which creates symbols # Check for method existence without invoking respond_to?, which creates symbols
def self.invokable?(method_name) def self.invokable?(method_name)
self.invokable_methods.include?(method_name.to_s)
end
def self.invokable_methods
unless @invokable_methods unless @invokable_methods
blacklist = Liquid::Drop.public_instance_methods + [:each] blacklist = Liquid::Drop.public_instance_methods + [:each]
if include?(Enumerable) if include?(Enumerable)
@@ -71,7 +75,7 @@ module Liquid
whitelist = [:to_liquid] + (public_instance_methods - blacklist) whitelist = [:to_liquid] + (public_instance_methods - blacklist)
@invokable_methods = Set.new(whitelist.map(&:to_s)) @invokable_methods = Set.new(whitelist.map(&:to_s))
end end
@invokable_methods.include?(method_name.to_s) @invokable_methods
end end
end end
end end

View File

@@ -18,6 +18,7 @@ module Liquid
def set_line_number_from_token(token) def set_line_number_from_token(token)
return unless token.respond_to?(:line_number) return unless token.respond_to?(:line_number)
return if self.line_number
self.line_number = token.line_number self.line_number = token.line_number
end end
@@ -50,10 +51,10 @@ module Liquid
class ArgumentError < Error; end class ArgumentError < Error; end
class ContextError < Error; end class ContextError < Error; end
class FilterNotFound < Error; end
class FileSystemError < Error; end class FileSystemError < Error; end
class StandardError < Error; end class StandardError < Error; end
class SyntaxError < Error; end class SyntaxError < Error; end
class StackLevelError < Error; end class StackLevelError < Error; end
class TaintedError < Error; end
class MemoryError < Error; end class MemoryError < Error; end
end end

View File

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

View File

@@ -9,9 +9,11 @@ module Liquid
'['.freeze => :open_square, '['.freeze => :open_square,
']'.freeze => :close_square, ']'.freeze => :close_square,
'('.freeze => :open_round, '('.freeze => :open_round,
')'.freeze => :close_round ')'.freeze => :close_round,
'?'.freeze => :question,
'-'.freeze => :dash
} }
IDENTIFIER = /[\w\-?!]+/ IDENTIFIER = /[a-zA-Z_][\w-]*\??/
SINGLE_STRING_LITERAL = /'[^\']*'/ SINGLE_STRING_LITERAL = /'[^\']*'/
DOUBLE_STRING_LITERAL = /"[^\"]*"/ DOUBLE_STRING_LITERAL = /"[^\"]*"/
NUMBER_LITERAL = /-?\d+(\.\d+)?/ NUMBER_LITERAL = /-?\d+(\.\d+)?/

View File

@@ -14,7 +14,8 @@
include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]" include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]"
unknown_tag: "Unknown tag '%{tag}'" unknown_tag: "Unknown tag '%{tag}'"
invalid_delimiter: "'end' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}" invalid_delimiter: "'end' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}"
unexpected_else: "%{block_name} tag does not expect else tag" unexpected_else: "%{block_name} tag does not expect 'else' tag"
unexpected_outer_tag: "Unexpected outer '%{tag}' tag"
tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}" tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"
variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}" variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}"
tag_never_closed: "'%{block_name}' tag was never closed" tag_never_closed: "'%{block_name}' tag was never closed"

View File

@@ -13,6 +13,7 @@
# def an_allowed_method # def an_allowed_method
# 'this comes from an allowed method' # 'this comes from an allowed method'
# end # end
#
# def unallowed_method # def unallowed_method
# 'this will never be an output' # 'this will never be an output'
# end # end
@@ -26,7 +27,7 @@
# 'and this from another allowed method' # 'and this from another allowed method'
# end # end
# end # end
# end #
# #
# usage: # usage:
# @something = SomeClass.new # @something = SomeClass.new
@@ -41,16 +42,18 @@
# association models. # association models.
# #
class Module class Module
def liquid_methods(*allowed_methods) def liquid_methods(*allowed_methods)
drop_class = eval "class #{self.to_s}::LiquidDropClass < Liquid::Drop; self; end" drop_class = eval "class #{self.to_s}::LiquidDropClass < Liquid::Drop; self; end"
define_method :to_liquid do define_method :to_liquid do
drop_class.new(self) drop_class.new(self)
end end
drop_class.class_eval do drop_class.class_eval do
def initialize(object) def initialize(object)
@object = object @object = object
end end
allowed_methods.each do |sym| allowed_methods.each do |sym|
define_method sym do define_method sym do
@object.send sym @object.send sym
@@ -58,5 +61,4 @@ class Module
end end
end end
end end
end end

View File

@@ -1,9 +1,12 @@
require 'liquid/profiler/hooks'
module Liquid module Liquid
# Profiler enables support for profiling template rendering to help track down performance issues. # Profiler enables support for profiling template rendering to help track down performance issues.
# #
# To enable profiling, pass the <tt>profile: true</tt> option to <tt>Liquid::Template.parse</tt>. Then, after # To enable profiling, first require 'liquid/profiler'.
# <tt>Liquid::Template#render</tt> is called, the template object makes available an instance of this # Then, to profile a parse/render cycle, pass the <tt>profile: true</tt> option to <tt>Liquid::Template.parse</tt>.
# After <tt>Liquid::Template#render</tt> is called, the template object makes available an instance of this
# class via the <tt>Liquid::Template#profiler</tt> method. # class via the <tt>Liquid::Template#profiler</tt> method.
# #
# template = Liquid::Template.parse(template_content, profile: true) # template = Liquid::Template.parse(template_content, profile: true)

View File

@@ -1,5 +1,5 @@
module Liquid module Liquid
class Block < Tag class BlockBody
def render_token_with_profiling(token, context) def render_token_with_profiling(token, context)
Profiler.profile_token_render(token) do Profiler.profile_token_render(token) do
render_token_without_profiling(token, context) render_token_without_profiling(token, context)
@@ -12,7 +12,7 @@ module Liquid
class Include < Tag class Include < Tag
def render_with_profiling(context) def render_with_profiling(context)
Profiler.profile_children(@template_name) do Profiler.profile_children(context.evaluate(@template_name).to_s) do
render_without_profiling(context) render_without_profiling(context)
end end
end end

View File

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

View File

@@ -34,7 +34,7 @@ module Liquid
end end
def escape(input) def escape(input)
CGI.escapeHTML(input) rescue input CGI.escapeHTML(input).untaint rescue input
end end
alias_method :h, :escape alias_method :h, :escape
@@ -177,6 +177,10 @@ module Liquid
input.to_s + string.to_s input.to_s + string.to_s
end end
def concat(input, array)
InputIterator.new(input).concat(array)
end
# prepend a string to another # prepend a string to another
def prepend(input, string) def prepend(input, string)
string.to_s + input.to_s string.to_s + input.to_s
@@ -306,6 +310,8 @@ module Liquid
def to_date(obj) def to_date(obj)
return obj if obj.respond_to?(:strftime) return obj if obj.respond_to?(:strftime)
obj = obj.downcase if obj.is_a?(String)
case obj case obj
when 'now'.freeze, 'today'.freeze when 'now'.freeze, 'today'.freeze
Time.now Time.now
@@ -344,6 +350,10 @@ module Liquid
to_a.join(glue) to_a.join(glue)
end end
def concat(args)
to_a.concat args
end
def reverse def reverse
reverse_each.to_a reverse_each.to_a
end end

View File

@@ -8,12 +8,13 @@ module Liquid
# The Strainer only allows method calls defined in filters given to it via Strainer.global_filter, # The Strainer only allows method calls defined in filters given to it via Strainer.global_filter,
# Context#add_filters or Template.register_filter # Context#add_filters or Template.register_filter
class Strainer #:nodoc: class Strainer #:nodoc:
@@filters = [] @@global_strainer = Class.new(Strainer) do
@@known_filters = Set.new @filter_methods = Set.new
@@known_methods = Set.new end
@@strainer_class_cache = Hash.new do |hash, filters| @@strainer_class_cache = Hash.new do |hash, filters|
hash[filters] = Class.new(Strainer) do hash[filters] = Class.new(@@global_strainer) do
filters.each { |f| include f } @filter_methods = @@global_strainer.filter_methods.dup
filters.each { |f| add_filter(f) }
end end
end end
@@ -21,33 +22,32 @@ module Liquid
@context = context @context = context
end end
def self.filter_methods
@filter_methods
end
def self.add_filter(filter)
raise ArgumentError, "Expected module but got: #{f.class}" unless filter.is_a?(Module)
unless self.class.include?(filter)
self.send(:include, filter)
@filter_methods.merge(filter.public_instance_methods.map(&:to_s))
end
end
def self.global_filter(filter) def self.global_filter(filter)
raise ArgumentError, "Passed filter is not a module" unless filter.is_a?(Module) @@global_strainer.add_filter(filter)
add_known_filter(filter)
@@filters << filter unless @@filters.include?(filter)
end end
def self.add_known_filter(filter) def self.invokable?(method)
unless @@known_filters.include?(filter) @filter_methods.include?(method.to_s)
@@method_blacklist ||= Set.new(Strainer.instance_methods.map(&:to_s))
new_methods = filter.instance_methods.map(&:to_s)
new_methods.reject!{ |m| @@method_blacklist.include?(m) }
@@known_methods.merge(new_methods)
@@known_filters.add(filter)
end
end
def self.strainer_class_cache
@@strainer_class_cache
end end
def self.create(context, filters = []) def self.create(context, filters = [])
filters = @@filters + filters @@strainer_class_cache[filters].new(context)
strainer_class_cache[filters].new(context)
end end
def invoke(method, *args) def invoke(method, *args)
if invokable?(method) if self.class.invokable?(method)
send(method, *args) send(method, *args)
else else
args.first args.first
@@ -55,9 +55,5 @@ module Liquid
rescue ::ArgumentError => e rescue ::ArgumentError => e
raise Liquid::ArgumentError.new(e.message) raise Liquid::ArgumentError.new(e.message)
end end
def invokable?(method)
@@known_methods.include?(method.to_s) && respond_to?(method)
end
end end
end end

View File

@@ -25,7 +25,10 @@ module Liquid
def render(context) def render(context)
val = @from.render(context) val = @from.render(context)
context.scopes.last[@to] = val context.scopes.last[@to] = val
context.increment_used_resources(:assign_score_current, val)
inc = val.instance_of?(String) || val.instance_of?(Array) || val.instance_of?(Hash) ? val.length : 1
context.resource_limits.assign_score += inc
''.freeze ''.freeze
end end

View File

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

View File

@@ -8,18 +8,24 @@ module Liquid
@blocks = [] @blocks = []
if markup =~ Syntax if markup =~ Syntax
@left = $1 @left = Expression.parse($1)
else else
raise SyntaxError.new(options[:locale].t("errors.syntax.case".freeze)) raise SyntaxError.new(options[:locale].t("errors.syntax.case".freeze))
end end
end end
def parse(tokens)
body = BlockBody.new
while more = parse_body(body, tokens)
body = @blocks.last.attachment
end
end
def nodelist def nodelist
@blocks.map(&:attachment).flatten @blocks.map(&:attachment)
end end
def unknown_tag(tag, markup, tokens) def unknown_tag(tag, markup, tokens)
@nodelist = []
case tag case tag
when 'when'.freeze when 'when'.freeze
record_when_condition(markup) record_when_condition(markup)
@@ -37,10 +43,10 @@ module Liquid
output = '' output = ''
@blocks.each do |block| @blocks.each do |block|
if block.else? if block.else?
return render_all(block.attachment, context) if execute_else_block return block.attachment.render(context) if execute_else_block
elsif block.evaluate(context) elsif block.evaluate(context)
execute_else_block = false execute_else_block = false
output << render_all(block.attachment, context) output << block.attachment.render(context)
end end
end end
output output
@@ -50,17 +56,18 @@ module Liquid
private private
def record_when_condition(markup) def record_when_condition(markup)
body = BlockBody.new
while markup while markup
# Create a new nodelist and assign it to the new block
if not markup =~ WhenSyntax if not markup =~ WhenSyntax
raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_when".freeze)) raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_when".freeze))
end end
markup = $2 markup = $2
block = Condition.new(@left, '=='.freeze, $1) block = Condition.new(@left, '=='.freeze, Expression.parse($1))
block.attach(@nodelist) block.attach(body)
@blocks.push(block) @blocks << block
end end
end end
@@ -70,7 +77,7 @@ module Liquid
end end
block = ElseCondition.new block = ElseCondition.new
block.attach(@nodelist) block.attach(BlockBody.new)
@blocks << block @blocks << block
end end
end end

View File

@@ -20,10 +20,10 @@ module Liquid
case markup case markup
when NamedSyntax when NamedSyntax
@variables = variables_from_string($2) @variables = variables_from_string($2)
@name = $1 @name = Expression.parse($1)
when SimpleSyntax when SimpleSyntax
@variables = variables_from_string(markup) @variables = variables_from_string(markup)
@name = "'#{@variables.to_s}'" @name = @variables.to_s
else else
raise SyntaxError.new(options[:locale].t("errors.syntax.cycle".freeze)) raise SyntaxError.new(options[:locale].t("errors.syntax.cycle".freeze))
end end
@@ -33,9 +33,9 @@ module Liquid
context.registers[:cycle] ||= Hash.new(0) context.registers[:cycle] ||= Hash.new(0)
context.stack do context.stack do
key = context[@name] key = context.evaluate(@name)
iteration = context.registers[:cycle][key] iteration = context.registers[:cycle][key]
result = context[@variables[iteration]] result = context.evaluate(@variables[iteration])
iteration += 1 iteration += 1
iteration = 0 if iteration >= @variables.size iteration = 0 if iteration >= @variables.size
context.registers[:cycle][key] = iteration context.registers[:cycle][key] = iteration
@@ -48,7 +48,7 @@ module Liquid
def variables_from_string(markup) def variables_from_string(markup)
markup.split(',').collect do |var| markup.split(',').collect do |var|
var =~ /\s*(#{QuotedFragment})\s*/o var =~ /\s*(#{QuotedFragment})\s*/o
$1 ? $1 : nil $1 ? Expression.parse($1) : nil
end.compact end.compact
end end
end end

View File

@@ -42,6 +42,7 @@ module Liquid
# where 0 is the last item. # where 0 is the last item.
# forloop.first:: Returns true if the item is the first item. # forloop.first:: Returns true if the item is the first item.
# forloop.last:: Returns true if the item is the last item. # forloop.last:: Returns true if the item is the last item.
# forloop.parentloop:: Provides access to the parent loop, if present.
# #
class For < Block class For < Block
Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
@@ -49,38 +50,40 @@ module Liquid
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
parse_with_selected_parser(markup) parse_with_selected_parser(markup)
@nodelist = @for_block = [] @for_block = BlockBody.new
end
def parse(tokens)
if more = parse_body(@for_block, tokens)
parse_body(@else_block, tokens)
end
end end
def nodelist def nodelist
if @else_block @else_block ? [@for_block, @else_block] : [@for_block]
@for_block + @else_block
else
@for_block
end
end end
def unknown_tag(tag, markup, tokens) def unknown_tag(tag, markup, tokens)
return super unless tag == 'else'.freeze return super unless tag == 'else'.freeze
@nodelist = @else_block = [] @else_block = BlockBody.new
end end
def render(context) def render(context)
context.registers[:for] ||= Hash.new(0) context.registers[:for] ||= Hash.new(0)
collection = context[@collection_name] collection = context.evaluate(@collection_name)
collection = collection.to_a if collection.is_a?(Range) collection = collection.to_a if collection.is_a?(Range)
# Maintains Ruby 1.8.7 String#each behaviour on 1.9 # Maintains Ruby 1.8.7 String#each behaviour on 1.9
return render_else(context) unless iterable?(collection) return render_else(context) unless iterable?(collection)
from = if @attributes['offset'.freeze] == 'continue'.freeze from = if @from == :continue
context.registers[:for][@name].to_i context.registers[:for][@name].to_i
else else
context[@attributes['offset'.freeze]].to_i context.evaluate(@from).to_i
end end
limit = context[@attributes['limit'.freeze]] limit = context.evaluate(@limit)
to = limit ? limit.to_i + from : nil to = limit ? limit.to_i + from : nil
segment = Utils.slice_collection(collection, from, to) segment = Utils.slice_collection(collection, from, to)
@@ -96,6 +99,8 @@ module Liquid
# Store our progress through the collection for the continue flag # Store our progress through the collection for the continue flag
context.registers[:for][@name] = from + segment.length context.registers[:for][@name] = from + segment.length
parent_loop = context['forloop'.freeze]
context.stack do context.stack do
segment.each_with_index do |item, index| segment.each_with_index do |item, index|
context[@variable_name] = item context[@variable_name] = item
@@ -107,10 +112,11 @@ module Liquid
'rindex'.freeze => length - index, 'rindex'.freeze => length - index,
'rindex0'.freeze => length - index - 1, 'rindex0'.freeze => length - index - 1,
'first'.freeze => (index == 0), 'first'.freeze => (index == 0),
'last'.freeze => (index == length - 1) 'last'.freeze => (index == length - 1),
'parentloop'.freeze => parent_loop
} }
result << render_all(@for_block, context) result << @for_block.render(context)
# Handle any interrupts if they exist. # Handle any interrupts if they exist.
if context.has_interrupt? if context.has_interrupt?
@@ -128,12 +134,12 @@ module Liquid
def lax_parse(markup) def lax_parse(markup)
if markup =~ Syntax if markup =~ Syntax
@variable_name = $1 @variable_name = $1
@collection_name = $2 collection_name = $2
@name = "#{$1}-#{$2}"
@reversed = $3 @reversed = $3
@attributes = {} @name = "#{@variable_name}-#{collection_name}"
@collection_name = Expression.parse(collection_name)
markup.scan(TagAttributes) do |key, value| markup.scan(TagAttributes) do |key, value|
@attributes[key] = value set_attribute(key, value)
end end
else else
raise SyntaxError.new(options[:locale].t("errors.syntax.for".freeze)) raise SyntaxError.new(options[:locale].t("errors.syntax.for".freeze))
@@ -144,26 +150,38 @@ module Liquid
p = Parser.new(markup) p = Parser.new(markup)
@variable_name = p.consume(:id) @variable_name = p.consume(:id)
raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_in".freeze)) unless p.id?('in'.freeze) raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_in".freeze)) unless p.id?('in'.freeze)
@collection_name = p.expression collection_name = p.expression
@name = "#{@variable_name}-#{@collection_name}" @name = "#{@variable_name}-#{collection_name}"
@collection_name = Expression.parse(collection_name)
@reversed = p.id?('reversed'.freeze) @reversed = p.id?('reversed'.freeze)
@attributes = {}
while p.look(:id) && p.look(:colon, 1) while p.look(:id) && p.look(:colon, 1)
unless attribute = p.id?('limit'.freeze) || p.id?('offset'.freeze) unless attribute = p.id?('limit'.freeze) || p.id?('offset'.freeze)
raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_attribute".freeze)) raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_attribute".freeze))
end end
p.consume p.consume
val = p.expression set_attribute(attribute, p.expression)
@attributes[attribute] = val
end end
p.consume(:end_of_string) p.consume(:end_of_string)
end end
private private
def set_attribute(key, expr)
case key
when 'offset'.freeze
@from = if expr == 'continue'.freeze
:continue
else
Expression.parse(expr)
end
when 'limit'.freeze
@limit = Expression.parse(expr)
end
end
def render_else(context) def render_else(context)
return @else_block ? [render_all(@else_block, context)] : ''.freeze @else_block ? @else_block.render(context) : ''.freeze
end end
def iterable?(collection) def iterable?(collection)

View File

@@ -20,8 +20,13 @@ module Liquid
push_block('if'.freeze, markup) push_block('if'.freeze, markup)
end end
def parse(tokens)
while more = parse_body(@blocks.last.attachment, tokens)
end
end
def nodelist def nodelist
@blocks.map(&:attachment).flatten @blocks.map(&:attachment)
end end
def unknown_tag(tag, markup, tokens) def unknown_tag(tag, markup, tokens)
@@ -36,7 +41,7 @@ module Liquid
context.stack do context.stack do
@blocks.each do |block| @blocks.each do |block|
if block.evaluate(context) if block.evaluate(context)
return render_all(block.attachment, context) return block.attachment.render(context)
end end
end end
''.freeze ''.freeze
@@ -53,21 +58,21 @@ module Liquid
end end
@blocks.push(block) @blocks.push(block)
@nodelist = block.attach(Array.new) block.attach(BlockBody.new)
end end
def lax_parse(markup) def lax_parse(markup)
expressions = markup.scan(ExpressionsAndOperators).reverse expressions = markup.scan(ExpressionsAndOperators)
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.shift =~ Syntax raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop =~ Syntax
condition = Condition.new($1, $2, $3) condition = Condition.new(Expression.parse($1), $2, Expression.parse($3))
while not expressions.empty? while not expressions.empty?
operator = (expressions.shift).to_s.strip operator = expressions.pop.to_s.strip
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.shift.to_s =~ Syntax raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop.to_s =~ Syntax
new_condition = Condition.new($1, $2, $3) new_condition = Condition.new(Expression.parse($1), $2, Expression.parse($3))
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless BOOLEAN_OPERATORS.include?(operator) raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless BOOLEAN_OPERATORS.include?(operator)
new_condition.send(operator, condition) new_condition.send(operator, condition)
condition = new_condition condition = new_condition
@@ -92,9 +97,9 @@ module Liquid
end end
def parse_comparison(p) def parse_comparison(p)
a = p.expression a = Expression.parse(p.expression)
if op = p.consume?(:comparison) if op = p.consume?(:comparison)
b = p.expression b = Expression.parse(p.expression)
Condition.new(a, op, b) Condition.new(a, op, b)
else else
Condition.new(a) Condition.new(a)

View File

@@ -4,7 +4,7 @@ module Liquid
def render(context) def render(context)
context.stack do context.stack do
output = render_all(@nodelist, context) output = super
if output != context.registers[:ifchanged] if output != context.registers[:ifchanged]
context.registers[:ifchanged] = output context.registers[:ifchanged] = output

View File

@@ -22,12 +22,16 @@ module Liquid
if markup =~ Syntax if markup =~ Syntax
@template_name = $1 template_name = $1
@variable_name = $3 variable_name = $3
@variable_name = Expression.parse(variable_name || template_name[1..-2])
@context_variable_name = template_name[1..-2].split('/'.freeze).last
@template_name = Expression.parse(template_name)
@attributes = {} @attributes = {}
markup.scan(TagAttributes) do |key, value| markup.scan(TagAttributes) do |key, value|
@attributes[key] = value @attributes[key] = Expression.parse(value)
end end
else else
@@ -40,21 +44,20 @@ module Liquid
def render(context) def render(context)
partial = load_cached_partial(context) partial = load_cached_partial(context)
variable = context[@variable_name || @template_name[1..-2]] variable = context.evaluate(@variable_name)
context.stack do context.stack do
@attributes.each do |key, value| @attributes.each do |key, value|
context[key] = context[value] context[key] = context.evaluate(value)
end end
context_variable_name = @template_name[1..-2].split('/'.freeze).last
if variable.is_a?(Array) if variable.is_a?(Array)
variable.collect do |var| variable.collect do |var|
context[context_variable_name] = var context[@context_variable_name] = var
partial.render(context) partial.render(context)
end end
else else
context[context_variable_name] = variable context[@context_variable_name] = variable
partial.render(context) partial.render(context)
end end
end end
@@ -63,7 +66,7 @@ module Liquid
private private
def load_cached_partial(context) def load_cached_partial(context)
cached_partials = context.registers[:cached_partials] || {} cached_partials = context.registers[:cached_partials] || {}
template_name = context[@template_name] template_name = context.evaluate(@template_name)
if cached = cached_partials[template_name] if cached = cached_partials[template_name]
return cached return cached
@@ -78,15 +81,7 @@ module Liquid
def read_template_from_file_system(context) def read_template_from_file_system(context)
file_system = context.registers[:file_system] || Liquid::Template.file_system file_system = context.registers[:file_system] || Liquid::Template.file_system
# make read_template_file call backwards-compatible. file_system.read_template_file(context.evaluate(@template_name))
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
def pass_options def pass_options

View File

@@ -3,18 +3,26 @@ module Liquid
FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
def parse(tokens) def parse(tokens)
@nodelist ||= [] @body = ''
@nodelist.clear
while token = tokens.shift while token = tokens.shift
if token =~ FullTokenPossiblyInvalid if token =~ FullTokenPossiblyInvalid
@nodelist << $1 if $1 != "".freeze @body << $1 if $1 != "".freeze
if block_delimiter == $2 return if block_delimiter == $2
end_tag end
return @body << token if not token.empty?
end end
end end
@nodelist << token if not token.empty?
def render(context)
@body
end end
def nodelist
[@body]
end
def blank?
@body.empty?
end end
end end

View File

@@ -6,10 +6,10 @@ module Liquid
super super
if markup =~ Syntax if markup =~ Syntax
@variable_name = $1 @variable_name = $1
@collection_name = $2 @collection_name = Expression.parse($2)
@attributes = {} @attributes = {}
markup.scan(TagAttributes) do |key, value| markup.scan(TagAttributes) do |key, value|
@attributes[key] = value @attributes[key] = Expression.parse(value)
end end
else else
raise SyntaxError.new(options[:locale].t("errors.syntax.table_row".freeze)) raise SyntaxError.new(options[:locale].t("errors.syntax.table_row".freeze))
@@ -17,16 +17,16 @@ module Liquid
end end
def render(context) def render(context)
collection = context[@collection_name] or return ''.freeze collection = context.evaluate(@collection_name) or return ''.freeze
from = @attributes['offset'.freeze] ? context[@attributes['offset'.freeze]].to_i : 0 from = @attributes.key?('offset'.freeze) ? context.evaluate(@attributes['offset'.freeze]).to_i : 0
to = @attributes['limit'.freeze] ? from + context[@attributes['limit'.freeze]].to_i : nil to = @attributes.key?('limit'.freeze) ? from + context.evaluate(@attributes['limit'.freeze]).to_i : nil
collection = Utils.slice_collection(collection, from, to) collection = Utils.slice_collection(collection, from, to)
length = collection.length length = collection.length
cols = context[@attributes['cols'.freeze]].to_i cols = context.evaluate(@attributes['cols'.freeze]).to_i
row = 1 row = 1
col = 0 col = 0
@@ -42,7 +42,6 @@ module Liquid
'index0'.freeze => index, 'index0'.freeze => index,
'col'.freeze => col + 1, 'col'.freeze => col + 1,
'col0'.freeze => col, 'col0'.freeze => col,
'index0'.freeze => index,
'rindex'.freeze => length - index, 'rindex'.freeze => length - index,
'rindex0'.freeze => length - index - 1, 'rindex0'.freeze => length - index - 1,
'first'.freeze => (index == 0), 'first'.freeze => (index == 0),
@@ -54,7 +53,7 @@ module Liquid
col += 1 col += 1
result << "<td class=\"col#{col}\">" << render_all(@nodelist, context) << '</td>' result << "<td class=\"col#{col}\">" << super << '</td>'
if col == cols and (index != length - 1) if col == cols and (index != length - 1)
col = 0 col = 0

View File

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

View File

@@ -18,7 +18,9 @@ module Liquid
:locale => I18n.new :locale => I18n.new
} }
attr_accessor :root, :resource_limits attr_accessor :root
attr_reader :resource_limits
@@file_system = BlankFileSystem.new @@file_system = BlankFileSystem.new
class TagRegistry class TagRegistry
@@ -60,6 +62,12 @@ module Liquid
# :strict will enforce correct syntax. # :strict will enforce correct syntax.
attr_writer :error_mode attr_writer :error_mode
# Sets how strict the taint checker should be.
# :lax is the default, and ignores the taint flag completely
# :warn adds a warning, but does not interrupt the rendering
# :error raises an error when tainted output is used
attr_writer :taint_mode
def file_system def file_system
@@file_system @@file_system
end end
@@ -80,6 +88,10 @@ module Liquid
@error_mode || :lax @error_mode || :lax
end end
def taint_mode
@taint_mode || :lax
end
# Pass a module with filter methods which should be available # Pass a module with filter methods which should be available
# to all liquid views. Good for registering the standard library # to all liquid views. Good for registering the standard library
def register_filter(mod) def register_filter(mod)
@@ -100,7 +112,7 @@ module Liquid
end end
def initialize def initialize
@resource_limits = self.class.default_resource_limits.dup @resource_limits = ResourceLimits.new(self.class.default_resource_limits)
end end
# Parse source code. # Parse source code.
@@ -193,6 +205,9 @@ module Liquid
context.add_filters(args.pop) context.add_filters(args.pop)
end end
# Retrying a render resets resource usage
context.resource_limits.reset
begin begin
# render the nodelist. # render the nodelist.
# for performance reasons we get an array back here. join will make a string out of it. # for performance reasons we get an array back here. join will make a string out of it.
@@ -240,6 +255,8 @@ module Liquid
def with_profiling def with_profiling
if @profiling && !@options[:included] if @profiling && !@options[:included]
raise "Profiler not loaded, require 'liquid/profiler' first" unless defined?(Liquid::Profiler)
@profiler = Profiler.new @profiler = Profiler.new
@profiler.start @profiler.start

View File

@@ -12,7 +12,6 @@ module Liquid
# #
class Variable class Variable
FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o
EasyParse = /\A *(\w+(?:\.\w+)*) *\z/
attr_accessor :filters, :name, :warnings attr_accessor :filters, :name, :warnings
attr_accessor :line_number attr_accessor :line_number
include ParserSwitching include ParserSwitching
@@ -34,16 +33,19 @@ module Liquid
end end
def lax_parse(markup) def lax_parse(markup)
@filters = [] @filters ||= []
if markup =~ /\s*(#{QuotedFragment})(.*)/om @filters.clear
@name = Regexp.last_match(1) if markup =~ /(#{QuotedFragment})(.*)/om
if Regexp.last_match(2) =~ /#{FilterSeparator}\s*(.*)/om name_markup = $1
filters = Regexp.last_match(1).scan(FilterParser) filter_markup = $2
@name = Expression.parse(name_markup)
if filter_markup =~ /#{FilterSeparator}\s*(.*)/om
filters = $1.scan(FilterParser)
filters.each do |f| filters.each do |f|
if f =~ /\w+/ if f =~ /\w+/
filtername = Regexp.last_match(0) filtername = Regexp.last_match(0)
filterargs = f.scan(/(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o).flatten filterargs = f.scan(/(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o).flatten
@filters << [filtername, filterargs] @filters << parse_filter_expressions(filtername, filterargs)
end end
end end
end end
@@ -51,21 +53,15 @@ module Liquid
end end
def strict_parse(markup) def strict_parse(markup)
# Very simple valid cases @filters ||= []
if markup =~ EasyParse @filters.clear
@name = $1
@filters = []
return
end
@filters = []
p = Parser.new(markup) p = Parser.new(markup)
# Could be just filters with no input
@name = p.look(:pipe) ? ''.freeze : p.expression @name = Expression.parse(p.expression)
while p.consume?(:pipe) while p.consume?(:pipe)
filtername = p.consume(:id) filtername = p.consume(:id)
filterargs = p.consume?(:colon) ? parse_filterargs(p) : [] filterargs = p.consume?(:colon) ? parse_filterargs(p) : []
@filters << [filtername, filterargs] @filters << parse_filter_expressions(filtername, filterargs)
end end
p.consume(:end_of_string) p.consume(:end_of_string)
end end
@@ -81,22 +77,51 @@ module Liquid
end end
def render(context) def render(context)
return ''.freeze if @name.nil? @filters.inject(context.evaluate(@name)) do |output, (filter_name, filter_args, filter_kwargs)|
@filters.inject(context[@name]) do |output, filter| filter_args = evaluate_filter_expressions(context, filter_args, filter_kwargs)
filterargs = [] output = context.invoke(filter_name, output, *filter_args)
end.tap{ |obj| taint_check(obj) }
end
private
def parse_filter_expressions(filter_name, unparsed_args)
filter_args = []
keyword_args = {} keyword_args = {}
filter[1].to_a.each do |a| unparsed_args.each do |a|
if matches = a.match(/\A#{TagAttributes}\z/o) if matches = a.match(/\A#{TagAttributes}\z/o)
keyword_args[matches[1]] = context[matches[2]] keyword_args[matches[1]] = Expression.parse(matches[2])
else else
filterargs << context[a] filter_args << Expression.parse(a)
end end
end end
filterargs << keyword_args unless keyword_args.empty? result = [filter_name, filter_args]
begin result << keyword_args unless keyword_args.empty?
output = context.invoke(filter[0], output, *filterargs) result
rescue FilterNotFound end
raise FilterNotFound, "Error - filter '#{filter[0]}' in '#{@markup.strip}' could not be found."
def evaluate_filter_expressions(context, filter_args, filter_kwargs)
parsed_args = filter_args.map{ |expr| context.evaluate(expr) }
if filter_kwargs
parsed_kwargs = {}
filter_kwargs.each do |key, expr|
parsed_kwargs[key] = context.evaluate(expr)
end
parsed_args << parsed_kwargs
end
parsed_args
end
def taint_check(obj)
if obj.tainted?
@markup =~ QuotedFragment
name = Regexp.last_match(0)
case Template.taint_mode
when :warn
@warnings ||= []
@warnings << "variable '#{name}' is tainted and was not escaped"
when :error
raise TaintedError, "Error - variable '#{name}' is tainted and was not escaped"
end end
end end
end end

View File

@@ -3,26 +3,27 @@ module Liquid
SQUARE_BRACKETED = /\A\[(.*)\]\z/m SQUARE_BRACKETED = /\A\[(.*)\]\z/m
COMMAND_METHODS = ['size'.freeze, 'first'.freeze, 'last'.freeze] COMMAND_METHODS = ['size'.freeze, 'first'.freeze, 'last'.freeze]
attr_reader :name, :lookups
def self.parse(markup) def self.parse(markup)
new(markup) new(markup)
end end
def initialize(markup) def initialize(markup)
lookups = markup.scan(VariableParser) @lookups = markup.scan(VariableParser)
name = lookups.shift name = @lookups.shift
if name =~ SQUARE_BRACKETED if name =~ SQUARE_BRACKETED
name = Expression.parse($1) name = Expression.parse($1)
end end
@name = name @name = name
@lookups = lookups
@command_flags = 0 @command_flags = 0
@lookups.each_index do |i| @lookups.each_index do |i|
lookup = lookups[i] lookup = @lookups[i]
if lookup =~ SQUARE_BRACKETED if lookup =~ SQUARE_BRACKETED
lookups[i] = Expression.parse($1) @lookups[i] = Expression.parse($1)
elsif COMMAND_METHODS.include?(lookup) elsif COMMAND_METHODS.include?(lookup)
@command_flags |= 1 << i @command_flags |= 1 << i
end end
@@ -64,5 +65,15 @@ module Liquid
object object
end end
def ==(other)
self.class == other.class && self.state == other.state
end
protected
def state
[@name, @lookups, @command_flags]
end
end end
end end

View File

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

View File

@@ -9,7 +9,7 @@ Gem::Specification.new do |s|
s.version = Liquid::VERSION s.version = Liquid::VERSION
s.platform = Gem::Platform::RUBY s.platform = Gem::Platform::RUBY
s.summary = "A secure, non-evaling end user template engine with aesthetic markup." s.summary = "A secure, non-evaling end user template engine with aesthetic markup."
s.authors = ["Tobias Luetke"] s.authors = ["Tobias Lütke"]
s.email = ["tobi@leetsoft.com"] s.email = ["tobi@leetsoft.com"]
s.homepage = "http://www.liquidmarkup.org" s.homepage = "http://www.liquidmarkup.org"
s.license = "MIT" s.license = "MIT"

19
performance/memory.rb Normal file
View File

@@ -0,0 +1,19 @@
at_exit do
p 'Objects distribution:'
require 'pp'
pp ObjectSpace.count_objects
end
require 'allocation_tracer' rescue fail("install allocation_tracer extension/gem")
require File.dirname(__FILE__) + '/theme_runner'
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new
require 'allocation_tracer/trace'
puts "Profiling memory usage..."
200.times do
profiler.run
end

View File

@@ -8,10 +8,17 @@ profiler.run
[:cpu, :object].each do |profile_type| [:cpu, :object].each do |profile_type|
puts "Profiling in #{profile_type.to_s} mode..." puts "Profiling in #{profile_type.to_s} mode..."
results = StackProf.run(mode: profile_type) do results = StackProf.run(mode: profile_type) do
100.times do 200.times do
profiler.run profiler.run
end end
end end
if profile_type == :cpu && graph_filename = ENV['GRAPH_FILENAME']
File.open(graph_filename, 'w') do |f|
StackProf::Report.new(results).print_graphviz(nil, f)
end
end
StackProf::Report.new(results).print_text(false, 20) StackProf::Report.new(results).print_text(false, 20)
File.write(ENV['FILENAME'] + "." + profile_type.to_s, Marshal.dump(results)) if ENV['FILENAME'] File.write(ENV['FILENAME'] + "." + profile_type.to_s, Marshal.dump(results)) if ENV['FILENAME']
end end

View File

@@ -4,8 +4,6 @@ class Paginate < Liquid::Block
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
@nodelist = []
if markup =~ Syntax if markup =~ Syntax
@collection_name = $1 @collection_name = $1
@page_size = if $2 @page_size = if $2
@@ -73,7 +71,7 @@ class Paginate < Liquid::Block
end end
end end
render_all(@nodelist, context) super
end end
end end

View File

@@ -17,7 +17,7 @@ class ThemeRunner
end end
# Called by Liquid to retrieve a template file # Called by Liquid to retrieve a template file
def read_template_file(template_path, context) def read_template_file(template_path)
File.read(@path + '/' + template_path + '.liquid') File.read(@path + '/' + template_path + '.liquid')
end end
end end

View File

@@ -3,6 +3,16 @@ require 'test_helper'
class AssignTest < Minitest::Test class AssignTest < Minitest::Test
include Liquid include Liquid
def test_assign_with_hyphen_in_variable_name
template_source = <<-END_TEMPLATE
{% assign this-thing = 'Print this-thing' %}
{{ this-thing }}
END_TEMPLATE
template = Template.parse(template_source)
rendered = template.render!
assert_equal "Print this-thing", rendered.strip
end
def test_assigned_variable def test_assigned_variable
assert_template_result('.foo.', assert_template_result('.foo.',
'{% assign foo = values %}.{{ foo[0] }}.', '{% assign foo = values %}.{{ foo[0] }}.',

View File

@@ -9,7 +9,7 @@ class FoobarTag < Liquid::Tag
end end
class BlankTestFileSystem class BlankTestFileSystem
def read_template_file(template_path, context) def read_template_file(template_path)
template_path template_path
end end
end end

View File

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

View File

@@ -23,12 +23,10 @@ class ContextTest < Minitest::Test
end end
def test_has_key_will_not_add_an_error_for_missing_keys def test_has_key_will_not_add_an_error_for_missing_keys
Template.error_mode = :strict with_error_mode :strict do
context = Context.new context = Context.new
context.has_key?('unknown') context.has_key?('unknown')
assert_empty context.errors assert_empty context.errors
end end
end
end end

View File

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

View File

@@ -48,6 +48,10 @@ class ProductDrop < Liquid::Drop
ContextDrop.new ContextDrop.new
end end
def user_input
"foo".taint
end
protected protected
def callmenot def callmenot
"protected" "protected"
@@ -108,6 +112,30 @@ class DropsTest < Minitest::Test
assert_equal ' ', tpl.render!('product' => ProductDrop.new) assert_equal ' ', tpl.render!('product' => ProductDrop.new)
end end
def test_rendering_raises_on_tainted_attr
with_taint_mode(:error) do
tpl = Liquid::Template.parse('{{ product.user_input }}')
assert_raises TaintedError do
tpl.render!('product' => ProductDrop.new)
end
end
end
def test_rendering_warns_on_tainted_attr
with_taint_mode(:warn) do
tpl = Liquid::Template.parse('{{ product.user_input }}')
tpl.render!('product' => ProductDrop.new)
assert_match /tainted/, tpl.warnings.first
end
end
def test_rendering_doesnt_raise_on_escaped_tainted_attr
with_taint_mode(:error) do
tpl = Liquid::Template.parse('{{ product.user_input | escape }}')
tpl.render!('product' => ProductDrop.new)
end
end
def test_drop_does_only_respond_to_whitelisted_methods def test_drop_does_only_respond_to_whitelisted_methods
assert_equal "", Liquid::Template.parse("{{ product.inspect }}").render!('product' => ProductDrop.new) assert_equal "", Liquid::Template.parse("{{ product.inspect }}").render!('product' => ProductDrop.new)
assert_equal "", Liquid::Template.parse("{{ product.pretty_inspect }}").render!('product' => ProductDrop.new) assert_equal "", Liquid::Template.parse("{{ product.pretty_inspect }}").render!('product' => ProductDrop.new)

View File

@@ -100,6 +100,73 @@ class ErrorHandlingTest < Minitest::Test
assert_equal Liquid::ArgumentError, template.errors.first.class assert_equal Liquid::ArgumentError, template.errors.first.class
end end
def test_with_line_numbers_adds_numbers_to_parser_errors
err = assert_raises(SyntaxError) do
template = Liquid::Template.parse(%q{
foobar
{% "cat" | foobar %}
bla
},
:line_numbers => true
)
end
assert_match /Liquid syntax error \(line 4\)/, err.message
end
def test_parsing_warn_with_line_numbers_adds_numbers_to_lexer_errors
template = Liquid::Template.parse(%q{
foobar
{% if 1 =! 2 %}ok{% endif %}
bla
},
:error_mode => :warn,
:line_numbers => true
)
assert_equal ['Liquid syntax error (line 4): Unexpected character = in "1 =! 2"'],
template.warnings.map(&:message)
end
def test_parsing_strict_with_line_numbers_adds_numbers_to_lexer_errors
err = assert_raises(SyntaxError) do
Liquid::Template.parse(%q{
foobar
{% if 1 =! 2 %}ok{% endif %}
bla
},
:error_mode => :strict,
:line_numbers => true
)
end
assert_equal 'Liquid syntax error (line 4): Unexpected character = in "1 =! 2"', err.message
end
def test_syntax_errors_in_nested_blocks_have_correct_line_number
err = assert_raises(SyntaxError) do
Liquid::Template.parse(%q{
foobar
{% if 1 != 2 %}
{% foo %}
{% endif %}
bla
},
:line_numbers => true
)
end
assert_equal "Liquid syntax error (line 5): Unknown tag 'foo'", err.message
end
def test_strict_error_messages def test_strict_error_messages
err = assert_raises(SyntaxError) do err = assert_raises(SyntaxError) do
Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', :error_mode => :strict) Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', :error_mode => :strict)

View File

@@ -25,6 +25,12 @@ end
class FiltersTest < Minitest::Test class FiltersTest < Minitest::Test
include Liquid include Liquid
module OverrideObjectMethodFilter
def tap(input)
"tap overridden"
end
end
def setup def setup
@context = Context.new @context = Context.new
end end
@@ -105,6 +111,13 @@ class FiltersTest < Minitest::Test
output = Variable.new(%! 'hello %{first_name}, %{last_name}' | substitute: first_name: surname, last_name: 'doe' !).render(@context) output = Variable.new(%! 'hello %{first_name}, %{last_name}' | substitute: first_name: surname, last_name: 'doe' !).render(@context)
assert_equal 'hello john, doe', output assert_equal 'hello john, doe', output
end end
def test_override_object_method_in_filter
assert_equal "tap overridden", Template.parse("{{var | tap}}").render!({ 'var' => 1000 }, :filters => [OverrideObjectMethodFilter])
# tap still treated as a non-existent filter
assert_equal "1000", Template.parse("{{var | tap}}").render!({ 'var' => 1000 })
end
end end
class FiltersInTemplate < Minitest::Test class FiltersInTemplate < Minitest::Test

View File

@@ -28,11 +28,14 @@ class ParsingQuirksTest < Minitest::Test
def test_error_on_empty_filter def test_error_on_empty_filter
assert Template.parse("{{test}}") assert Template.parse("{{test}}")
with_error_mode(:lax) do
assert Template.parse("{{|test}}") assert Template.parse("{{|test}}")
with_error_mode(:strict) do
assert_raises(SyntaxError) do
Template.parse("{{test |a|b|}}")
end end
with_error_mode(:strict) do
assert_raises(SyntaxError) { Template.parse("{{|test}}") }
assert_raises(SyntaxError) { Template.parse("{{test |a|b|}}") }
end end
end end
@@ -100,4 +103,17 @@ class ParsingQuirksTest < Minitest::Test
end end
end end
def test_invalid_variables_work
with_error_mode(:lax) do
assert_template_result('bar', "{% assign 123foo = 'bar' %}{{ 123foo }}")
assert_template_result('123', "{% assign 123 = 'bar' %}{{ 123 }}")
end
end
def test_extra_dots_in_ranges
with_error_mode(:lax) do
assert_template_result('12345', "{% for i in (1...5) %}{{ i }}{% endfor %}")
end
end
end # ParsingQuirksTest end # ParsingQuirksTest

View File

@@ -4,7 +4,7 @@ class RenderProfilingTest < Minitest::Test
include Liquid include Liquid
class ProfilingFileSystem class ProfilingFileSystem
def read_template_file(template_path, context) def read_template_file(template_path)
"Rendering template {% assign template_name = '#{template_path}'%}\n{{ template_name }}" "Rendering template {% assign template_name = '#{template_path}'%}\n{{ template_name }}"
end end
end end
@@ -72,7 +72,7 @@ class RenderProfilingTest < Minitest::Test
t = Template.parse("{% include 'a_template' %}", :profile => true) t = Template.parse("{% include 'a_template' %}", :profile => true)
t.render! t.render!
assert t.profiler.total_render_time > 0, "Total render time was not calculated" assert t.profiler.total_render_time >= 0, "Total render time was not calculated"
end end
def test_profiling_uses_include_to_mark_children def test_profiling_uses_include_to_mark_children
@@ -89,7 +89,7 @@ class RenderProfilingTest < Minitest::Test
include_node = t.profiler[1] include_node = t.profiler[1]
include_node.children.each do |child| include_node.children.each do |child|
assert_equal "'a_template'", child.partial assert_equal "a_template", child.partial
end end
end end
@@ -99,12 +99,12 @@ class RenderProfilingTest < Minitest::Test
a_template = t.profiler[1] a_template = t.profiler[1]
a_template.children.each do |child| a_template.children.each do |child|
assert_equal "'a_template'", child.partial assert_equal "a_template", child.partial
end end
b_template = t.profiler[2] b_template = t.profiler[2]
b_template.children.each do |child| b_template.children.each do |child|
assert_equal "'b_template'", child.partial assert_equal "b_template", child.partial
end end
end end
@@ -114,12 +114,12 @@ class RenderProfilingTest < Minitest::Test
a_template1 = t.profiler[1] a_template1 = t.profiler[1]
a_template1.children.each do |child| a_template1.children.each do |child|
assert_equal "'a_template'", child.partial assert_equal "a_template", child.partial
end end
a_template2 = t.profiler[2] a_template2 = t.profiler[2]
a_template2.children.each do |child| a_template2.children.each do |child|
assert_equal "'a_template'", child.partial assert_equal "a_template", child.partial
end end
end end

View File

@@ -249,6 +249,7 @@ class StandardFiltersTest < Minitest::Test
assert_equal "07/16/2004", @filters.date("Fri Jul 16 01:00:00 2004", "%m/%d/%Y") assert_equal "07/16/2004", @filters.date("Fri Jul 16 01:00:00 2004", "%m/%d/%Y")
assert_equal "#{Date.today.year}", @filters.date('now', '%Y') assert_equal "#{Date.today.year}", @filters.date('now', '%Y')
assert_equal "#{Date.today.year}", @filters.date('today', '%Y') assert_equal "#{Date.today.year}", @filters.date('today', '%Y')
assert_equal "#{Date.today.year}", @filters.date('Today', '%Y')
assert_equal nil, @filters.date(nil, "%B") assert_equal nil, @filters.date(nil, "%B")
@@ -358,6 +359,17 @@ class StandardFiltersTest < Minitest::Test
assert_template_result('bcd',"{{ a | append: b}}",assigns) assert_template_result('bcd',"{{ a | append: b}}",assigns)
end end
def test_concat
assert_equal [1, 2, 3, 4], @filters.concat([1, 2], [3, 4])
assert_equal [1, 2, 'a'], @filters.concat([1, 2], ['a'])
assert_equal [1, 2, 10], @filters.concat([1, 2], [10])
assert_raises(TypeError) do
# no implicit conversion of Fixnum into Array
@filters.concat([1, 2], 10)
end
end
def test_prepend def test_prepend
assigns = {'a' => 'bc', 'b' => 'a' } assigns = {'a' => 'bc', 'b' => 'a' }
assert_template_result('abc',"{{ a | prepend: 'a'}}",assigns) assert_template_result('abc',"{{ a | prepend: 'a'}}",assigns)

View File

@@ -298,6 +298,22 @@ HERE
'string' => "test string") 'string' => "test string")
end end
def test_for_parentloop_references_parent_loop
assert_template_result('1.1 1.2 1.3 2.1 2.2 2.3 ',
'{% for inner in outer %}{% for k in inner %}' +
'{{ forloop.parentloop.index }}.{{ forloop.index }} ' +
'{% endfor %}{% endfor %}',
'outer' => [[1, 1, 1], [1, 1, 1]])
end
def test_for_parentloop_nil_when_not_present
assert_template_result('.1 .2 ',
'{% for inner in outer %}' +
'{{ forloop.parentloop.index }}.{{ forloop.index }} ' +
'{% endfor %}',
'outer' => [[1, 1, 1], [1, 1, 1]])
end
def test_blank_string_not_iterable def test_blank_string_not_iterable
assert_template_result('', "{% for char in characters %}I WILL NOT BE OUTPUT{% endfor %}", 'characters' => '') assert_template_result('', "{% for char in characters %}I WILL NOT BE OUTPUT{% endfor %}", 'characters' => '')
end end

View File

@@ -10,6 +10,11 @@ class IfElseTagTest < Minitest::Test
assert_template_result(' you rock ?','{% if false %} you suck {% endif %} {% if true %} you rock {% endif %}?') assert_template_result(' you rock ?','{% if false %} you suck {% endif %} {% if true %} you rock {% endif %}?')
end end
def test_literal_comparisons
assert_template_result(' NO ','{% assign v = false %}{% if v %} YES {% else %} NO {% endif %}')
assert_template_result(' YES ','{% assign v = nil %}{% if v == nil %} YES {% else %} NO {% endif %}')
end
def test_if_else def test_if_else
assert_template_result(' YES ','{% if false %} NO {% else %} YES {% endif %}') assert_template_result(' YES ','{% if false %} NO {% else %} YES {% endif %}')
assert_template_result(' YES ','{% if true %} YES {% else %} NO {% endif %}') assert_template_result(' YES ','{% if true %} YES {% else %} NO {% endif %}')

View File

@@ -1,7 +1,7 @@
require 'test_helper' require 'test_helper'
class TestFileSystem class TestFileSystem
def read_template_file(template_path, context) def read_template_file(template_path)
case template_path case template_path
when "product" when "product"
"Product: {{ product.title }} " "Product: {{ product.title }} "
@@ -27,6 +27,9 @@ class TestFileSystem
when "pick_a_source" when "pick_a_source"
"from TestFileSystem" "from TestFileSystem"
when 'assignments'
"{% assign foo = 'bar' %}"
else else
template_path template_path
end end
@@ -34,14 +37,14 @@ class TestFileSystem
end end
class OtherFileSystem class OtherFileSystem
def read_template_file(template_path, context) def read_template_file(template_path)
'from OtherFileSystem' 'from OtherFileSystem'
end end
end end
class CountingFileSystem class CountingFileSystem
attr_reader :count attr_reader :count
def read_template_file(template_path, context) def read_template_file(template_path)
@count ||= 0 @count ||= 0
@count += 1 @count += 1
'from CountingFileSystem' 'from CountingFileSystem'
@@ -108,6 +111,10 @@ class IncludeTagTest < Minitest::Test
'echo1' => 'test123', 'more_echos' => { "echo2" => 'test321'} 'echo1' => 'test123', 'more_echos' => { "echo2" => 'test321'}
end end
def test_included_templates_assigns_variables
assert_template_result "bar", "{% include 'assignments' %}{{ foo }}"
end
def test_nested_include_tag def test_nested_include_tag
assert_template_result "body body_detail", "{% include 'body' %}" assert_template_result "body body_detail", "{% include 'body' %}"
@@ -125,7 +132,7 @@ class IncludeTagTest < Minitest::Test
def test_recursively_included_template_does_not_produce_endless_loop def test_recursively_included_template_does_not_produce_endless_loop
infinite_file_system = Class.new do infinite_file_system = Class.new do
def read_template_file(template_path, context) def read_template_file(template_path)
"-{% include 'loop' %}" "-{% include 'loop' %}"
end end
end end
@@ -138,18 +145,6 @@ class IncludeTagTest < Minitest::Test
end 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 def test_dynamically_choosen_template
assert_template_result "Test123", "{% include template %}", "template" => 'Test123' assert_template_result "Test123", "{% include template %}", "template" => 'Test123'
assert_template_result "Test321", "{% include template %}", "template" => 'Test321' assert_template_result "Test321", "{% include template %}", "template" => 'Test321'

View File

@@ -1,4 +1,5 @@
require 'test_helper' require 'test_helper'
require 'timeout'
class TemplateContextDrop < Liquid::Drop class TemplateContextDrop < Liquid::Drop
def before_method(method) def before_method(method)
@@ -37,6 +38,16 @@ class TemplateTest < Minitest::Test
assert_equal 'from instance assigns', t.parse("{{ foo }}").render! assert_equal 'from instance assigns', t.parse("{{ foo }}").render!
end end
def test_warnings_is_not_exponential_time
str = "false"
100.times do
str = "{% if true %}true{% else %}#{str}{% endif %}"
end
t = Template.parse(str)
assert_equal [], Timeout::timeout(1) { t.warnings }
end
def test_instance_assigns_persist_on_same_template_parsing_between_renders def test_instance_assigns_persist_on_same_template_parsing_between_renders
t = Template.new.parse("{{ foo }}{% assign foo = 'foo' %}{{ foo }}") t = Template.new.parse("{{ foo }}{% assign foo = 'foo' %}{{ foo }}")
assert_equal 'foo', t.render! assert_equal 'foo', t.render!
@@ -82,57 +93,92 @@ class TemplateTest < Minitest::Test
def test_resource_limits_works_with_custom_length_method def test_resource_limits_works_with_custom_length_method
t = Template.parse("{% assign foo = bar %}") t = Template.parse("{% assign foo = bar %}")
t.resource_limits = { :render_length_limit => 42 } t.resource_limits.render_length_limit = 42
assert_equal "", t.render!("bar" => SomethingWithLength.new) assert_equal "", t.render!("bar" => SomethingWithLength.new)
end end
def test_resource_limits_render_length def test_resource_limits_render_length
t = Template.parse("0123456789") t = Template.parse("0123456789")
t.resource_limits = { :render_length_limit => 5 } t.resource_limits.render_length_limit = 5
assert_equal "Liquid error: Memory limits exceeded", t.render() assert_equal "Liquid error: Memory limits exceeded", t.render()
assert t.resource_limits[:reached] assert t.resource_limits.reached?
t.resource_limits = { :render_length_limit => 10 }
t.resource_limits.render_length_limit = 10
assert_equal "0123456789", t.render!() assert_equal "0123456789", t.render!()
refute_nil t.resource_limits[:render_length_current] refute_nil t.resource_limits.render_length
end end
def test_resource_limits_render_score def test_resource_limits_render_score
t = Template.parse("{% for a in (1..10) %} {% for a in (1..10) %} foo {% endfor %} {% endfor %}") t = Template.parse("{% for a in (1..10) %} {% for a in (1..10) %} foo {% endfor %} {% endfor %}")
t.resource_limits = { :render_score_limit => 50 } t.resource_limits.render_score_limit = 50
assert_equal "Liquid error: Memory limits exceeded", t.render() assert_equal "Liquid error: Memory limits exceeded", t.render()
assert t.resource_limits[:reached] assert t.resource_limits.reached?
t = Template.parse("{% for a in (1..100) %} foo {% endfor %}") t = Template.parse("{% for a in (1..100) %} foo {% endfor %}")
t.resource_limits = { :render_score_limit => 50 } t.resource_limits.render_score_limit = 50
assert_equal "Liquid error: Memory limits exceeded", t.render() assert_equal "Liquid error: Memory limits exceeded", t.render()
assert t.resource_limits[:reached] assert t.resource_limits.reached?
t.resource_limits = { :render_score_limit => 200 }
t.resource_limits.render_score_limit = 200
assert_equal (" foo " * 100), t.render!() assert_equal (" foo " * 100), t.render!()
refute_nil t.resource_limits[:render_score_current] refute_nil t.resource_limits.render_score
end end
def test_resource_limits_assign_score def test_resource_limits_assign_score
t = Template.parse("{% assign foo = 42 %}{% assign bar = 23 %}") t = Template.parse("{% assign foo = 42 %}{% assign bar = 23 %}")
t.resource_limits = { :assign_score_limit => 1 } t.resource_limits.assign_score_limit = 1
assert_equal "Liquid error: Memory limits exceeded", t.render() assert_equal "Liquid error: Memory limits exceeded", t.render()
assert t.resource_limits[:reached] assert t.resource_limits.reached?
t.resource_limits = { :assign_score_limit => 2 }
t.resource_limits.assign_score_limit = 2
assert_equal "", t.render!() assert_equal "", t.render!()
refute_nil t.resource_limits[:assign_score_current] refute_nil t.resource_limits.assign_score
end end
def test_resource_limits_aborts_rendering_after_first_error def test_resource_limits_aborts_rendering_after_first_error
t = Template.parse("{% for a in (1..100) %} foo1 {% endfor %} bar {% for a in (1..100) %} foo2 {% endfor %}") t = Template.parse("{% for a in (1..100) %} foo1 {% endfor %} bar {% for a in (1..100) %} foo2 {% endfor %}")
t.resource_limits = { :render_score_limit => 50 } t.resource_limits.render_score_limit = 50
assert_equal "Liquid error: Memory limits exceeded", t.render() assert_equal "Liquid error: Memory limits exceeded", t.render()
assert t.resource_limits[:reached] assert t.resource_limits.reached?
end end
def test_resource_limits_hash_in_template_gets_updated_even_if_no_limits_are_set def test_resource_limits_hash_in_template_gets_updated_even_if_no_limits_are_set
t = Template.parse("{% for a in (1..100) %} {% assign foo = 1 %} {% endfor %}") t = Template.parse("{% for a in (1..100) %} {% assign foo = 1 %} {% endfor %}")
t.render!() t.render!()
assert t.resource_limits[:assign_score_current] > 0 assert t.resource_limits.assign_score > 0
assert t.resource_limits[:render_score_current] > 0 assert t.resource_limits.render_score > 0
assert t.resource_limits[:render_length_current] > 0 assert t.resource_limits.render_length > 0
end
def test_render_length_persists_between_blocks
t = Template.parse("{% if true %}aaaa{% endif %}")
t.resource_limits.render_length_limit = 7
assert_equal "Liquid error: Memory limits exceeded", t.render()
t.resource_limits.render_length_limit = 8
assert_equal "aaaa", t.render()
t = Template.parse("{% if true %}aaaa{% endif %}{% if true %}bbb{% endif %}")
t.resource_limits.render_length_limit = 13
assert_equal "Liquid error: Memory limits exceeded", t.render()
t.resource_limits.render_length_limit = 14
assert_equal "aaaabbb", t.render()
t = Template.parse("{% if true %}a{% endif %}{% if true %}b{% endif %}{% if true %}a{% endif %}{% if true %}b{% endif %}{% if true %}a{% endif %}{% if true %}b{% endif %}")
t.resource_limits.render_length_limit = 5
assert_equal "Liquid error: Memory limits exceeded", t.render()
t.resource_limits.render_length_limit = 11
assert_equal "Liquid error: Memory limits exceeded", t.render()
t.resource_limits.render_length_limit = 12
assert_equal "ababab", t.render()
end
def test_default_resource_limits_unaffected_by_render_with_context
context = Context.new
t = Template.parse("{% for a in (1..100) %} {% assign foo = 1 %} {% endfor %}")
t.render!(context)
assert context.resource_limits.assign_score > 0
assert context.resource_limits.render_score > 0
assert context.resource_limits.render_length > 0
end end
def test_can_use_drop_as_context def test_can_use_drop_as_context

View File

@@ -31,6 +31,12 @@ class VariableTest < Minitest::Test
def test_false_renders_as_false def test_false_renders_as_false
assert_equal 'false', Template.parse("{{ foo }}").render!('foo' => false) assert_equal 'false', Template.parse("{{ foo }}").render!('foo' => false)
assert_equal 'false', Template.parse("{{ false }}").render!
end
def test_nil_renders_as_empty_string
assert_equal '', Template.parse("{{ nil }}").render!
assert_equal 'cat', Template.parse("{{ nil | append: 'cat' }}").render!
end end
def test_preset_assigns def test_preset_assigns

View File

@@ -1,10 +1,12 @@
#!/usr/bin/env ruby #!/usr/bin/env ruby
ENV["MT_NO_EXPECTATIONS"] = "1"
require 'minitest/autorun' require 'minitest/autorun'
require 'spy/integration' require 'spy/integration'
$:.unshift(File.join(File.expand_path(File.dirname(__FILE__)), '..', 'lib')) $:.unshift(File.join(File.expand_path(File.dirname(__FILE__)), '..', 'lib'))
require 'liquid.rb' require 'liquid.rb'
require 'liquid/profiler'
mode = :strict mode = :strict
if env_mode = ENV['LIQUID_PARSER_MODE'] if env_mode = ENV['LIQUID_PARSER_MODE']
@@ -48,13 +50,27 @@ module Minitest
end end
def with_global_filter(*globals) def with_global_filter(*globals)
original_filters = Array.new(Liquid::Strainer.class_variable_get(:@@filters)) original_global_strainer = Liquid::Strainer.class_variable_get(:@@global_strainer)
Liquid::Strainer.class_variable_set(:@@global_strainer, Class.new(Liquid::Strainer) do
@filter_methods = Set.new
end)
Liquid::Strainer.class_variable_get(:@@strainer_class_cache).clear
globals.each do |global| globals.each do |global|
Liquid::Template.register_filter(global) Liquid::Template.register_filter(global)
end end
yield yield
ensure ensure
Liquid::Strainer.class_variable_set(:@@filters, original_filters) Liquid::Strainer.class_variable_get(:@@strainer_class_cache).clear
Liquid::Strainer.class_variable_set(:@@global_strainer, original_global_strainer)
end
def with_taint_mode(mode)
old_mode = Liquid::Template.taint_mode
Liquid::Template.taint_mode = mode
yield
ensure
Liquid::Template.taint_mode = old_mode
end end
def with_error_mode(mode) def with_error_mode(mode)

View File

@@ -4,110 +4,111 @@ class ConditionUnitTest < Minitest::Test
include Liquid include Liquid
def test_basic_condition def test_basic_condition
assert_equal false, Condition.new('1', '==', '2').evaluate assert_equal false, Condition.new(1, '==', 2).evaluate
assert_equal true, Condition.new('1', '==', '1').evaluate assert_equal true, Condition.new(1, '==', 1).evaluate
end end
def test_default_operators_evalute_true def test_default_operators_evalute_true
assert_evalutes_true '1', '==', '1' assert_evalutes_true 1, '==', 1
assert_evalutes_true '1', '!=', '2' assert_evalutes_true 1, '!=', 2
assert_evalutes_true '1', '<>', '2' assert_evalutes_true 1, '<>', 2
assert_evalutes_true '1', '<', '2' assert_evalutes_true 1, '<', 2
assert_evalutes_true '2', '>', '1' assert_evalutes_true 2, '>', 1
assert_evalutes_true '1', '>=', '1' assert_evalutes_true 1, '>=', 1
assert_evalutes_true '2', '>=', '1' assert_evalutes_true 2, '>=', 1
assert_evalutes_true '1', '<=', '2' assert_evalutes_true 1, '<=', 2
assert_evalutes_true '1', '<=', '1' assert_evalutes_true 1, '<=', 1
# negative numbers # negative numbers
assert_evalutes_true '1', '>', '-1' assert_evalutes_true 1, '>', -1
assert_evalutes_true '-1', '<', '1' assert_evalutes_true -1, '<', 1
assert_evalutes_true '1.0', '>', '-1.0' assert_evalutes_true 1.0, '>', -1.0
assert_evalutes_true '-1.0', '<', '1.0' assert_evalutes_true -1.0, '<', 1.0
end end
def test_default_operators_evalute_false def test_default_operators_evalute_false
assert_evalutes_false '1', '==', '2' assert_evalutes_false 1, '==', 2
assert_evalutes_false '1', '!=', '1' assert_evalutes_false 1, '!=', 1
assert_evalutes_false '1', '<>', '1' assert_evalutes_false 1, '<>', 1
assert_evalutes_false '1', '<', '0' assert_evalutes_false 1, '<', 0
assert_evalutes_false '2', '>', '4' assert_evalutes_false 2, '>', 4
assert_evalutes_false '1', '>=', '3' assert_evalutes_false 1, '>=', 3
assert_evalutes_false '2', '>=', '4' assert_evalutes_false 2, '>=', 4
assert_evalutes_false '1', '<=', '0' assert_evalutes_false 1, '<=', 0
assert_evalutes_false '1', '<=', '0' assert_evalutes_false 1, '<=', 0
end end
def test_contains_works_on_strings def test_contains_works_on_strings
assert_evalutes_true "'bob'", 'contains', "'o'" assert_evalutes_true 'bob', 'contains', 'o'
assert_evalutes_true "'bob'", 'contains', "'b'" assert_evalutes_true 'bob', 'contains', 'b'
assert_evalutes_true "'bob'", 'contains', "'bo'" assert_evalutes_true 'bob', 'contains', 'bo'
assert_evalutes_true "'bob'", 'contains', "'ob'" assert_evalutes_true 'bob', 'contains', 'ob'
assert_evalutes_true "'bob'", 'contains', "'bob'" assert_evalutes_true 'bob', 'contains', 'bob'
assert_evalutes_false "'bob'", 'contains', "'bob2'" assert_evalutes_false 'bob', 'contains', 'bob2'
assert_evalutes_false "'bob'", 'contains', "'a'" assert_evalutes_false 'bob', 'contains', 'a'
assert_evalutes_false "'bob'", 'contains', "'---'" assert_evalutes_false 'bob', 'contains', '---'
end end
def test_invalid_comparation_operator def test_invalid_comparation_operator
assert_evaluates_argument_error "1", '~~', '0' assert_evaluates_argument_error 1, '~~', 0
end end
def test_comparation_of_int_and_str def test_comparation_of_int_and_str
assert_evaluates_argument_error "'1'", '>', '0' assert_evaluates_argument_error '1', '>', 0
assert_evaluates_argument_error "'1'", '<', '0' assert_evaluates_argument_error '1', '<', 0
assert_evaluates_argument_error "'1'", '>=', '0' assert_evaluates_argument_error '1', '>=', 0
assert_evaluates_argument_error "'1'", '<=', '0' assert_evaluates_argument_error '1', '<=', 0
end end
def test_contains_works_on_arrays def test_contains_works_on_arrays
@context = Liquid::Context.new @context = Liquid::Context.new
@context['array'] = [1,2,3,4,5] @context['array'] = [1,2,3,4,5]
array_expr = VariableLookup.new("array")
assert_evalutes_false "array", 'contains', '0' assert_evalutes_false array_expr, 'contains', 0
assert_evalutes_true "array", 'contains', '1' assert_evalutes_true array_expr, 'contains', 1
assert_evalutes_true "array", 'contains', '2' assert_evalutes_true array_expr, 'contains', 2
assert_evalutes_true "array", 'contains', '3' assert_evalutes_true array_expr, 'contains', 3
assert_evalutes_true "array", 'contains', '4' assert_evalutes_true array_expr, 'contains', 4
assert_evalutes_true "array", 'contains', '5' assert_evalutes_true array_expr, 'contains', 5
assert_evalutes_false "array", 'contains', '6' assert_evalutes_false array_expr, 'contains', 6
assert_evalutes_false "array", 'contains', '"1"' assert_evalutes_false array_expr, 'contains', "1"
end end
def test_contains_returns_false_for_nil_operands def test_contains_returns_false_for_nil_operands
@context = Liquid::Context.new @context = Liquid::Context.new
assert_evalutes_false "not_assigned", 'contains', '0' assert_evalutes_false VariableLookup.new('not_assigned'), 'contains', '0'
assert_evalutes_false "0", 'contains', 'not_assigned' assert_evalutes_false 0, 'contains', VariableLookup.new('not_assigned')
end end
def test_contains_return_false_on_wrong_data_type def test_contains_return_false_on_wrong_data_type
assert_evalutes_false "1", 'contains', '0' assert_evalutes_false 1, 'contains', 0
end end
def test_or_condition def test_or_condition
condition = Condition.new('1', '==', '2') condition = Condition.new(1, '==', 2)
assert_equal false, condition.evaluate assert_equal false, condition.evaluate
condition.or Condition.new('2', '==', '1') condition.or Condition.new(2, '==', 1)
assert_equal false, condition.evaluate assert_equal false, condition.evaluate
condition.or Condition.new('1', '==', '1') condition.or Condition.new(1, '==', 1)
assert_equal true, condition.evaluate assert_equal true, condition.evaluate
end end
def test_and_condition def test_and_condition
condition = Condition.new('1', '==', '1') condition = Condition.new(1, '==', 1)
assert_equal true, condition.evaluate assert_equal true, condition.evaluate
condition.and Condition.new('2', '==', '2') condition.and Condition.new(2, '==', 2)
assert_equal true, condition.evaluate assert_equal true, condition.evaluate
condition.and Condition.new('2', '==', '1') condition.and Condition.new(2, '==', 1)
assert_equal false, condition.evaluate assert_equal false, condition.evaluate
end end
@@ -115,9 +116,8 @@ class ConditionUnitTest < Minitest::Test
def test_should_allow_custom_proc_operator def test_should_allow_custom_proc_operator
Condition.operators['starts_with'] = Proc.new { |cond, left, right| left =~ %r{^#{right}} } Condition.operators['starts_with'] = Proc.new { |cond, left, right| left =~ %r{^#{right}} }
assert_evalutes_true "'bob'", 'starts_with', "'b'" assert_evalutes_true 'bob', 'starts_with', 'b'
assert_evalutes_false "'bob'", 'starts_with', "'o'" assert_evalutes_false 'bob', 'starts_with', 'o'
ensure ensure
Condition.operators.delete 'starts_with' Condition.operators.delete 'starts_with'
end end
@@ -126,7 +126,7 @@ class ConditionUnitTest < Minitest::Test
@context = Liquid::Context.new @context = Liquid::Context.new
@context['one'] = @context['another'] = "gnomeslab-and-or-liquid" @context['one'] = @context['another'] = "gnomeslab-and-or-liquid"
assert_evalutes_true "one", '==', "another" assert_evalutes_true VariableLookup.new("one"), '==', VariableLookup.new("another")
end end
private private

View File

@@ -465,22 +465,13 @@ class ContextUnitTest < Minitest::Test
mock_empty = Spy.on_instance_method(Array, :empty?) mock_empty = Spy.on_instance_method(Array, :empty?)
mock_has_interrupt = Spy.on(@context, :has_interrupt?).and_call_through mock_has_interrupt = Spy.on(@context, :has_interrupt?).and_call_through
@context.push_interrupt(StandardError.new)
@context.has_interrupt? @context.has_interrupt?
refute mock_any.has_been_called? refute mock_any.has_been_called?
assert mock_empty.has_been_called? assert mock_empty.has_been_called?
end end
def test_variable_lookup_caches_markup
mock_scan = Spy.on_instance_method(String, :scan).and_return(["string"])
@context['string'] = 'string'
@context['string']
@context['string']
assert_equal 1, mock_scan.calls.size
end
def test_context_initialization_with_a_proc_in_environment def test_context_initialization_with_a_proc_in_environment
contx = Context.new([:test => lambda { |c| c['poutine']}], {:test => :foo}) contx = Context.new([:test => lambda { |c| c['poutine']}], {:test => :foo})

View File

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

View File

@@ -31,8 +31,11 @@ class LexerUnitTest < Minitest::Test
end end
def test_fancy_identifiers def test_fancy_identifiers
tokens = Lexer.new('hi! five?').tokenize tokens = Lexer.new('hi five?').tokenize
assert_equal [[:id,'hi!'], [:id, 'five?'], [:end_of_string]], tokens assert_equal [[:id, 'hi'], [:id, 'five?'], [:end_of_string]], tokens
tokens = Lexer.new('2foo').tokenize
assert_equal [[:number, '2'], [:id, 'foo'], [:end_of_string]], tokens
end end
def test_whitespace def test_whitespace

View File

@@ -44,9 +44,9 @@ class ParserUnitTest < Minitest::Test
end end
def test_expressions def test_expressions
p = Parser.new("hi.there hi[5].! hi.there.bob") p = Parser.new("hi.there hi?[5].there? hi.there.bob")
assert_equal 'hi.there', p.expression assert_equal 'hi.there', p.expression
assert_equal 'hi[5].!', p.expression assert_equal 'hi?[5].there?', p.expression
assert_equal 'hi.there.bob', p.expression assert_equal 'hi.there.bob', p.expression
p = Parser.new("567 6.0 'lol' \"wut\"") p = Parser.new("567 6.0 'lol' \"wut\"")

View File

@@ -31,11 +31,11 @@ class StrainerUnitTest < Minitest::Test
def test_strainer_only_invokes_public_filter_methods def test_strainer_only_invokes_public_filter_methods
strainer = Strainer.create(nil) strainer = Strainer.create(nil)
assert_equal false, strainer.invokable?('__test__') assert_equal false, strainer.class.invokable?('__test__')
assert_equal false, strainer.invokable?('test') assert_equal false, strainer.class.invokable?('test')
assert_equal false, strainer.invokable?('instance_eval') assert_equal false, strainer.class.invokable?('instance_eval')
assert_equal false, strainer.invokable?('__send__') assert_equal false, strainer.class.invokable?('__send__')
assert_equal true, strainer.invokable?('size') # from the standard lib assert_equal true, strainer.class.invokable?('size') # from the standard lib
end end
def test_strainer_returns_nil_if_no_filter_method_found def test_strainer_returns_nil_if_no_filter_method_found
@@ -57,14 +57,13 @@ class StrainerUnitTest < Minitest::Test
end end
def test_strainer_uses_a_class_cache_to_avoid_method_cache_invalidation def test_strainer_uses_a_class_cache_to_avoid_method_cache_invalidation
a, b = Module.new, Module.new a = Module.new
b = Module.new
strainer = Strainer.create(nil, [a,b]) strainer = Strainer.create(nil, [a,b])
assert_kind_of Strainer, strainer assert_kind_of Strainer, strainer
assert_kind_of a, strainer assert_kind_of a, strainer
assert_kind_of b, strainer assert_kind_of b, strainer
Strainer.class_variable_get(:@@filters).each do |m| assert_kind_of Liquid::StandardFilters, strainer
assert_kind_of m, strainer
end
end end
end # StrainerTest end # StrainerTest

View File

@@ -5,6 +5,6 @@ class CaseTagUnitTest < Minitest::Test
def test_case_nodelist def test_case_nodelist
template = Liquid::Template.parse('{% case var %}{% when true %}WHEN{% else %}ELSE{% endcase %}') template = Liquid::Template.parse('{% case var %}{% when true %}WHEN{% else %}ELSE{% endcase %}')
assert_equal ['WHEN', 'ELSE'], template.root.nodelist[0].nodelist assert_equal ['WHEN', 'ELSE'], template.root.nodelist[0].nodelist.map(&:nodelist).flatten
end end
end end

View File

@@ -3,11 +3,11 @@ require 'test_helper'
class ForTagUnitTest < Minitest::Test class ForTagUnitTest < Minitest::Test
def test_for_nodelist def test_for_nodelist
template = Liquid::Template.parse('{% for item in items %}FOR{% endfor %}') template = Liquid::Template.parse('{% for item in items %}FOR{% endfor %}')
assert_equal ['FOR'], template.root.nodelist[0].nodelist assert_equal ['FOR'], template.root.nodelist[0].nodelist.map(&:nodelist).flatten
end end
def test_for_else_nodelist def test_for_else_nodelist
template = Liquid::Template.parse('{% for item in items %}FOR{% else %}ELSE{% endfor %}') template = Liquid::Template.parse('{% for item in items %}FOR{% else %}ELSE{% endfor %}')
assert_equal ['FOR', 'ELSE'], template.root.nodelist[0].nodelist assert_equal ['FOR', 'ELSE'], template.root.nodelist[0].nodelist.map(&:nodelist).flatten
end end
end end

View File

@@ -3,6 +3,6 @@ require 'test_helper'
class IfTagUnitTest < Minitest::Test class IfTagUnitTest < Minitest::Test
def test_if_nodelist def test_if_nodelist
template = Liquid::Template.parse('{% if true %}IF{% else %}ELSE{% endif %}') template = Liquid::Template.parse('{% if true %}IF{% else %}ELSE{% endif %}')
assert_equal ['IF', 'ELSE'], template.root.nodelist[0].nodelist assert_equal ['IF', 'ELSE'], template.root.nodelist[0].nodelist.map(&:nodelist).flatten
end end
end end

View File

@@ -5,16 +5,17 @@ class TemplateUnitTest < Minitest::Test
def test_sets_default_localization_in_document def test_sets_default_localization_in_document
t = Template.new t = Template.new
t.parse('') t.parse('{%comment%}{%endcomment%}')
assert_instance_of I18n, t.root.options[:locale] assert_instance_of I18n, t.root.nodelist[0].options[:locale]
end end
def test_sets_default_localization_in_context_with_quick_initialization def test_sets_default_localization_in_context_with_quick_initialization
t = Template.new t = Template.new
t.parse('{{foo}}', :locale => I18n.new(fixture("en_locale.yml"))) t.parse('{%comment%}{%endcomment%}', :locale => I18n.new(fixture("en_locale.yml")))
assert_instance_of I18n, t.root.options[:locale] locale = t.root.nodelist[0].options[:locale]
assert_equal fixture("en_locale.yml"), t.root.options[:locale].path assert_instance_of I18n, locale
assert_equal fixture("en_locale.yml"), locale.path
end end
def test_with_cache_classes_tags_returns_the_same_class def test_with_cache_classes_tags_returns_the_same_class

View File

@@ -5,125 +5,134 @@ class VariableUnitTest < Minitest::Test
def test_variable def test_variable
var = Variable.new('hello') var = Variable.new('hello')
assert_equal 'hello', var.name assert_equal VariableLookup.new('hello'), var.name
end end
def test_filters def test_filters
var = Variable.new('hello | textileze') var = Variable.new('hello | textileze')
assert_equal 'hello', var.name assert_equal VariableLookup.new('hello'), var.name
assert_equal [["textileze",[]]], var.filters assert_equal [['textileze',[]]], var.filters
var = Variable.new('hello | textileze | paragraph') var = Variable.new('hello | textileze | paragraph')
assert_equal 'hello', var.name assert_equal VariableLookup.new('hello'), var.name
assert_equal [["textileze",[]], ["paragraph",[]]], var.filters assert_equal [['textileze',[]], ['paragraph',[]]], var.filters
var = Variable.new(%! hello | strftime: '%Y'!) var = Variable.new(%! hello | strftime: '%Y'!)
assert_equal 'hello', var.name assert_equal VariableLookup.new('hello'), var.name
assert_equal [["strftime",["'%Y'"]]], var.filters assert_equal [['strftime',['%Y']]], var.filters
var = Variable.new(%! 'typo' | link_to: 'Typo', true !) var = Variable.new(%! 'typo' | link_to: 'Typo', true !)
assert_equal %!'typo'!, var.name assert_equal 'typo', var.name
assert_equal [["link_to",["'Typo'", "true"]]], var.filters assert_equal [['link_to',['Typo', true]]], var.filters
var = Variable.new(%! 'typo' | link_to: 'Typo', false !) var = Variable.new(%! 'typo' | link_to: 'Typo', false !)
assert_equal %!'typo'!, var.name assert_equal 'typo', var.name
assert_equal [["link_to",["'Typo'", "false"]]], var.filters assert_equal [['link_to',['Typo', false]]], var.filters
var = Variable.new(%! 'foo' | repeat: 3 !) var = Variable.new(%! 'foo' | repeat: 3 !)
assert_equal %!'foo'!, var.name assert_equal 'foo', var.name
assert_equal [["repeat",["3"]]], var.filters assert_equal [['repeat',[3]]], var.filters
var = Variable.new(%! 'foo' | repeat: 3, 3 !) var = Variable.new(%! 'foo' | repeat: 3, 3 !)
assert_equal %!'foo'!, var.name assert_equal 'foo', var.name
assert_equal [["repeat",["3","3"]]], var.filters assert_equal [['repeat',[3,3]]], var.filters
var = Variable.new(%! 'foo' | repeat: 3, 3, 3 !) var = Variable.new(%! 'foo' | repeat: 3, 3, 3 !)
assert_equal %!'foo'!, var.name assert_equal 'foo', var.name
assert_equal [["repeat",["3","3","3"]]], var.filters assert_equal [['repeat',[3,3,3]]], var.filters
var = Variable.new(%! hello | strftime: '%Y, okay?'!) var = Variable.new(%! hello | strftime: '%Y, okay?'!)
assert_equal 'hello', var.name assert_equal VariableLookup.new('hello'), var.name
assert_equal [["strftime",["'%Y, okay?'"]]], var.filters assert_equal [['strftime',['%Y, okay?']]], var.filters
var = Variable.new(%! hello | things: "%Y, okay?", 'the other one'!) var = Variable.new(%! hello | things: "%Y, okay?", 'the other one'!)
assert_equal 'hello', var.name assert_equal VariableLookup.new('hello'), var.name
assert_equal [["things",["\"%Y, okay?\"","'the other one'"]]], var.filters assert_equal [['things',['%Y, okay?','the other one']]], var.filters
end end
def test_filter_with_date_parameter def test_filter_with_date_parameter
var = Variable.new(%! '2006-06-06' | date: "%m/%d/%Y"!) var = Variable.new(%! '2006-06-06' | date: "%m/%d/%Y"!)
assert_equal "'2006-06-06'", var.name assert_equal '2006-06-06', var.name
assert_equal [["date",["\"%m/%d/%Y\""]]], var.filters assert_equal [['date',['%m/%d/%Y']]], var.filters
end end
def test_filters_without_whitespace def test_filters_without_whitespace
var = Variable.new('hello | textileze | paragraph') var = Variable.new('hello | textileze | paragraph')
assert_equal 'hello', var.name assert_equal VariableLookup.new('hello'), var.name
assert_equal [["textileze",[]], ["paragraph",[]]], var.filters assert_equal [['textileze',[]], ['paragraph',[]]], var.filters
var = Variable.new('hello|textileze|paragraph') var = Variable.new('hello|textileze|paragraph')
assert_equal 'hello', var.name assert_equal VariableLookup.new('hello'), var.name
assert_equal [["textileze",[]], ["paragraph",[]]], var.filters assert_equal [['textileze',[]], ['paragraph',[]]], var.filters
var = Variable.new("hello|replace:'foo','bar'|textileze") var = Variable.new("hello|replace:'foo','bar'|textileze")
assert_equal 'hello', var.name assert_equal VariableLookup.new('hello'), var.name
assert_equal [["replace", ["'foo'", "'bar'"]], ["textileze", []]], var.filters assert_equal [['replace', ['foo', 'bar']], ['textileze', []]], var.filters
end end
def test_symbol def test_symbol
var = Variable.new("http://disney.com/logo.gif | image: 'med' ", :error_mode => :lax) var = Variable.new("http://disney.com/logo.gif | image: 'med' ", :error_mode => :lax)
assert_equal "http://disney.com/logo.gif", var.name assert_equal VariableLookup.new('http://disney.com/logo.gif'), var.name
assert_equal [["image",["'med'"]]], var.filters assert_equal [['image',['med']]], var.filters
end end
def test_string_to_filter def test_string_to_filter
var = Variable.new("'http://disney.com/logo.gif' | image: 'med' ") var = Variable.new("'http://disney.com/logo.gif' | image: 'med' ")
assert_equal "'http://disney.com/logo.gif'", var.name assert_equal 'http://disney.com/logo.gif', var.name
assert_equal [["image",["'med'"]]], var.filters assert_equal [['image',['med']]], var.filters
end end
def test_string_single_quoted def test_string_single_quoted
var = Variable.new(%| "hello" |) var = Variable.new(%| "hello" |)
assert_equal '"hello"', var.name assert_equal 'hello', var.name
end end
def test_string_double_quoted def test_string_double_quoted
var = Variable.new(%| 'hello' |) var = Variable.new(%| 'hello' |)
assert_equal "'hello'", var.name assert_equal 'hello', var.name
end end
def test_integer def test_integer
var = Variable.new(%| 1000 |) var = Variable.new(%| 1000 |)
assert_equal "1000", var.name assert_equal 1000, var.name
end end
def test_float def test_float
var = Variable.new(%| 1000.01 |) var = Variable.new(%| 1000.01 |)
assert_equal "1000.01", var.name assert_equal 1000.01, var.name
end
def test_dashes
assert_equal VariableLookup.new('foo-bar'), Variable.new('foo-bar').name
assert_equal VariableLookup.new('foo-bar-2'), Variable.new('foo-bar-2').name
with_error_mode :strict do
assert_raises(Liquid::SyntaxError) { Variable.new('foo - bar') }
assert_raises(Liquid::SyntaxError) { Variable.new('-foo') }
assert_raises(Liquid::SyntaxError) { Variable.new('2foo') }
end
end end
def test_string_with_special_chars def test_string_with_special_chars
var = Variable.new(%| 'hello! $!@.;"ddasd" ' |) var = Variable.new(%| 'hello! $!@.;"ddasd" ' |)
assert_equal %|'hello! $!@.;"ddasd" '|, var.name assert_equal 'hello! $!@.;"ddasd" ', var.name
end end
def test_string_dot def test_string_dot
var = Variable.new(%| test.test |) var = Variable.new(%| test.test |)
assert_equal 'test.test', var.name assert_equal VariableLookup.new('test.test'), var.name
end end
def test_filter_with_keyword_arguments def test_filter_with_keyword_arguments
var = Variable.new(%! hello | things: greeting: "world", farewell: 'goodbye'!) var = Variable.new(%! hello | things: greeting: "world", farewell: 'goodbye'!)
assert_equal 'hello', var.name assert_equal VariableLookup.new('hello'), var.name
assert_equal [['things',["greeting: \"world\"","farewell: 'goodbye'"]]], var.filters assert_equal [['things', [], { 'greeting' => 'world', 'farewell' => 'goodbye' }]], var.filters
end end
def test_lax_filter_argument_parsing def test_lax_filter_argument_parsing
var = Variable.new(%! number_of_comments | pluralize: 'comment': 'comments' !, :error_mode => :lax) var = Variable.new(%! number_of_comments | pluralize: 'comment': 'comments' !, :error_mode => :lax)
assert_equal 'number_of_comments', var.name assert_equal VariableLookup.new('number_of_comments'), var.name
assert_equal [['pluralize',["'comment'","'comments'"]]], var.filters assert_equal [['pluralize',['comment','comments']]], var.filters
end end
def test_strict_filter_argument_parsing def test_strict_filter_argument_parsing
@@ -138,4 +147,10 @@ class VariableUnitTest < Minitest::Test
var = Variable.new(%! name_of_variable | upcase !) var = Variable.new(%! name_of_variable | upcase !)
assert_equal " name_of_variable | upcase ", var.raw assert_equal " name_of_variable | upcase ", var.raw
end end
def test_variable_lookup_interface
lookup = VariableLookup.new('a.b.c')
assert_equal 'a', lookup.name
assert_equal ['b', 'c'], lookup.lookups
end
end end