Compare commits

..

148 Commits

Author SHA1 Message Date
Tristan Hume
10173c3315 Add line numbers to warnings 2014-08-14 16:23:38 -04:00
Tristan Hume
1c1aa4094a Remove parser switching duplication 2014-08-14 15:06:54 -04:00
Florian Weingarten
627ef9e29d Optional line numbers for liquid errors 2014-08-14 16:11:21 +00:00
Florian Weingarten
c2663258be Merge pull request #364 from collectiveidea/instrument-rendering-with-hooks
Profiling the rendering of a liquid template
2014-08-13 23:04:29 +02:00
Tristan Hume
d4654d0062 Merge pull request #417 from Shopify/simplify-regex
Simplify Variable Parsing Regexes
2014-08-13 12:07:35 -04:00
Tristan Hume
ffd4f9d959 Simplify secondary filter regex 2014-08-13 09:36:02 -04:00
Tristan Hume
292161865d Simplify filter parse regex 2014-08-13 09:28:01 -04:00
Florian Weingarten
35808390ee Merge pull request #414 from Shopify/to_liquid_context
Call to_liquid in Context invoke
2014-08-12 22:05:52 +02:00
Florian Weingarten
1678c07548 Call to_liquid in Context invoke 2014-08-12 19:54:12 +00:00
Jason Roelofs
173a58d36a Profile liquid rendering
Add a simple profiling system to liquid rendering. Each
liquid tag ({{ }} and {% %}) is processed through this profiling,
keeping track of the partial name (in the case of {% include %}), line
number, and the time it took to render the tag. In the case of {%
include %}, the profiler keeps track of the name of the partial and
properly links back tag rendering to the partial and line number for
easy lookup and dive down. With this, it's now possible to track down
exactly how long each tag takes to render.

These hooks get installed and uninstalled on an as-need basis so by
default there is no impact on the overall liquid execution speed.
2014-08-12 15:37:21 -04:00
Tristan Hume
f31e309770 Merge pull request #416 from Shopify/filter-quirks
Make Filter Quirks Tests Actual Integration Tests
2014-08-12 10:08:05 -04:00
Tristan Hume
ffe1036e15 Make tests actual integration tests 2014-08-12 09:27:46 -04:00
Dylan Thacker-Smith
d3b113d2e1 Merge pull request #391 from Shopify/extract-context-parse
Separate expression parsing and rendering from Context#[]
2014-08-11 14:17:54 -07:00
Dylan Thacker-Smith
2aa9bbbac2 Separate expression parsing and rendering from Context#resolve. 2014-08-11 14:15:58 -07:00
Tristan Hume
d5e57a8ea4 Merge pull request #412 from Shopify/assign-strict
Pass through options on assign tag
2014-08-11 15:37:49 -04:00
Florian Weingarten
5c0e0be639 Merge pull request #402 from Shopify/benchmark-ips
benchmark/ips
2014-08-11 21:22:43 +02:00
Florian Weingarten
a74d40f1e5 benchmark/ips 2014-08-11 19:22:06 +00:00
Tristan Hume
79d4ec1a48 Merge pull request #413 from Shopify/filter-quirks
Add quirks test for unanchored filter args
2014-08-11 13:06:51 -04:00
Tristan Hume
4db22be8ba Add tests for assign tag fix 2014-08-11 13:06:01 -04:00
Tristan Hume
dc58a4d648 Add quirks test for unanchored filter args 2014-08-11 11:58:36 -04:00
Tristan Hume
2809ec780a Pass through options on assign tag 2014-08-11 10:38:36 -04:00
Jean Boussier
2d98392bf5 Merge pull request #411 from Shopify/to-s-before-split
Cast input to string before spliting
2014-08-08 00:13:35 -04:00
Jean Boussier
df6b442816 Cast input to string before spliting 2014-08-07 14:01:44 -04:00
Florian Weingarten
4b22fc8d1b Merge pull request #407 from Shopify/slice_arrays
Slice filter for arrays
2014-08-05 20:00:45 +02:00
Florian Weingarten
fb6f9c1c13 Slice filter for arrays. 2014-08-05 17:59:31 +00:00
Florian Weingarten
66ae7f3ec0 Merge pull request #406 from Shopify/slice_filter
slice filter
2014-08-05 17:14:01 +02:00
Florian Weingarten
0bea31d2ef Use Integer() instead of to_i 2014-08-05 15:13:15 +00:00
Florian Weingarten
e5b0487fef Merge pull request #312 from Shopify/uniq_filter
uniq filter
2014-08-05 16:22:48 +02:00
Florian Weingarten
9117722740 Use symbols in respond_to? 2014-08-05 14:22:11 +00:00
Florian Weingarten
baea0a6bf7 slice filter 2014-08-04 16:47:08 +00:00
Tom Burns
17347d43de Merge pull request #400 from Shopify/lazy_stack
lazily create stacks
2014-07-30 11:43:31 -04:00
Tom Burns
794ca9f604 make the conditions around stack creation easier to read 2014-07-30 15:42:24 +00:00
Tom Burns
15f6cabf83 avoid a hash comparison 2014-07-30 15:12:22 +00:00
Tom Burns
e53d102a2c use 'unless' instead of 'if !' for simple conditional 2014-07-30 14:59:56 +00:00
Florian Weingarten
33e7b8e373 uniq filter 2014-07-29 13:09:34 +00:00
Florian Weingarten
9b8e3d437e Merge pull request #401 from Shopify/ktdreyer-minitest
Minitest 5 (continuation of #358)
2014-07-29 15:05:23 +02:00
Florian Weingarten
a2f0f2547d with_global_filter test helper 2014-07-28 19:28:22 +00:00
Ken Dreyer
57d5426eed tests: reset Strainer's filters after modification
Three tests in the test suite use the Liquid::Template.register_filter
function to register custom filters with Liquid::Strainer. The problem
is that these register_filter calls leave the Liquid::Strainer object in
an altered state.

As an example, the FiltersTest's test_local_filter relies on the default
behavior of Liquid::Strainer operator, and the test was failing if
register_function had been called earlier. The same thing was happening
with FiltersInTemplate's test_local_global.

The problem was present when the Filters test classes were loaded inside
a single ruby process that also loaded HashOrderingTest. One example is
"rake test", which runs "require" on every test file. Another basic
example is the following command:

  ruby -Itest -e "require 'integration/hash_ordering_test';
  require 'integration/filter_test'"

Update the tests to always reset Liquid::Strainer's filters back to the
default list of filters.

With this change, FiltersTest and FiltersInTemplate now pass.
2014-07-28 16:36:43 +00:00
Ken Dreyer
3e3a415457 tests: fix whitespace in hash_ordering_test
Indent two spaces, not one.
2014-07-28 16:36:43 +00:00
Ken Dreyer
deba039d6d tests: reset "contains" op during IfElseTagTest
Two tests in IfElseTagTest each set a custom operator function for the
"contains" comparison operator.

The problem is that IfElseTagTest was clobbering the original operator
in Liquid and leaving it in an altered state.

As an example, ConditionUnitTest's test_contains_works_on_arrays relies
on the specific behavior of the "contains" operator, and its
test_contains_works_on_arrays was failing.

The problem was present when both test classes were require'd inside a
single ruby process. One example is "rake test", which runs "require" on
every test file. Another basic example is the following command:

  ruby -Itest -e "require 'integration/tags/if_else_tag_test.rb';
  require 'unit/condition_unit_test.rb'"

This would cause test_contains_works_on_arrays to fail.

Update IfElseTagTest to avoid clobbering the "contains" operator.

With this change, ConditionUnitTest's test_contains_works_on_arrays now
passes.
2014-07-28 16:36:43 +00:00
Ken Dreyer
ee4295c889 tests: switch to minitest
Ruby 1.9+ uses Minitest as the backend for Test::Unit. As of Minitest 5,
the shim has broken some compatibility with Test::Unit::TestCase in some
scenarios.

Adjusts the test suite to support Minitest 5's syntax.

Minitest versions 4 and below do not support the newer Minitest::Test
class that arrived in version 5. For that case, use the
MiniTest::Unit::TestCase class as a fallback

Conflicts:
	test/integration/tags/for_tag_test.rb
	test/test_helper.rb
2014-07-28 16:36:38 +00:00
Tom Burns
f5e67a12f9 remove added newline in liquid.rb 2014-07-28 14:24:29 +00:00
Tom Burns
6b56bdd74f remove variables used for counting empty stacks 2014-07-28 14:23:16 +00:00
Tom Burns
ba6e3e3da6 lazily create stacks 2014-07-28 14:12:11 +00:00
Jason Hiltz-Laforge
a8e63ff03d Merge pull request #398 from Shopify/fix_order_of_constructor_initialize
Reorder constructor to avoid referencing uninitialized variable when environment contains a self-referencing proc
2014-07-24 15:04:36 -04:00
Jason Hiltz-Laforge
052ef9fcb8 Reorder constructor to avoid referencing uninitialized variable when environment contains a self-referencing proc 2014-07-24 18:58:23 +00:00
Arthur Neves
d07b12dc7d Update History log
Bring latest History from 2-6-stable and 2-5-stable
2014-07-24 11:01:19 -04:00
Arthur Nogueira Neves
32e4f2d3b1 Merge pull request #240 from Shopify/remove_flatten
remove .flatten on standard filters
2014-07-24 10:54:28 -04:00
Arthur Nogueira Neves
2cb1483d54 Merge pull request #397 from Shopify/bogdan-excetion-handling-for-humans
Excetion handling for humans (2)
2014-07-24 10:51:02 -04:00
Florian Weingarten
6c6350f18b Exception handling for humans
Ability to pass exception_handler as a block to #render
and provide whatever behavior you want on handling exceptions

https://github.com/Shopify/liquid/pull/254
2014-07-24 14:44:02 +00:00
Florian Weingarten
eae24373e6 remove unnecessary flatten filter 2014-07-24 02:56:57 +00:00
Jason Hiltz-Laforge
034a47a6cf Merge pull request #395 from Shopify/fix_block_delimiter
Forgot an error message case
2014-07-23 22:35:13 -04:00
Jason Hiltz-Laforge
51c1165f26 Forgot an error message case 2014-07-24 02:27:26 +00:00
Florian Weingarten
0b45ffeada add more legacy tests 2014-07-24 00:33:39 +00:00
Arthur Neves
b7b243a13d Fix regression on map 2014-07-23 17:16:21 -04:00
Arthur Neves
18e8ce1eb0 add flatten filter 2014-07-23 17:16:20 -04:00
Florian Weingarten
994f309465 Fix broken standardfilter test 2014-07-23 17:15:39 -04:00
Arthur Neves
02d42a1475 Array is a Enumerable 2014-07-23 17:14:27 -04:00
Arthur Neves
d099878385 add a input iterator to standard filter 2014-07-23 17:14:27 -04:00
Arthur Neves
6a061cbe81 remove .flatten on standard filters 2014-07-23 17:14:26 -04:00
Arthur Nogueira Neves
c864a75903 Merge pull request #341 from curebit/comparation_argument_error
Raise Liquid::ArugmentError when condition has wrong usage
2014-07-23 17:03:31 -04:00
Jason Hiltz-Laforge
d6fdf86acd Merge pull request #393 from Shopify/fix_block_delimiter
Fixing regression from block delimiter enhancement
2014-07-23 16:24:24 -04:00
Jason Hiltz-Laforge
55597b8398 Fixing regression from block delimiter enhancement 2014-07-23 19:18:02 +00:00
Florian Weingarten
c75522026b Merge pull request #389 from Shopify/remove_unnecessary_blank_stuf
Remove unnecessary blank? code
2014-07-23 15:07:26 +02:00
Florian Weingarten
1e0e9f1f31 Remove unnecessary blank? code 2014-07-22 21:19:12 +00:00
Jason Hiltz-Laforge
5fc1929b73 Merge pull request #384 from Shopify/optimize_block_parsing
Optimize block parsing -- don't recreate delimiter, use strings instead of regex
2014-07-22 12:59:05 -04:00
Jason Hiltz-Laforge
746a800475 Merge pull request #386 from Shopify/optimize_variable_parsing
Reduce temporary objects during variable/filter parsing
2014-07-22 11:22:57 -04:00
Jason Hiltz-Laforge
85dc7ef610 Merge pull request #385 from Shopify/optimize_scope_variable_resolution
Remove block in favour of for loop to reduce temporary object allocation during variable context resolution
2014-07-22 11:22:33 -04:00
Jason Hiltz-Laforge
bc3b066ba8 Remove block in favour of for loop to reduce temporary object allocation during variable context resolution 2014-07-22 14:54:50 +00:00
Jason Hiltz-Laforge
3c2de7737d Optimize block parsing -- don't recreate delimiter, use strings instead of regex 2014-07-22 02:43:20 +00:00
Jason Hiltz-Laforge
adb7d2bbb8 Reduce temporary objects during variable/filter parsing 2014-07-21 21:13:59 +00:00
Jason Hiltz-Laforge
0e56cf99ab Merge pull request #383 from Shopify/optimize_variable_lookup
Cache parsed markup parts to avoid repeated calls during template render
2014-07-21 12:29:58 -04:00
Jason Hiltz-Laforge
0df3f1c372 Cache parsed markup parts to avoid repeated calls during template render 2014-07-21 15:55:06 +00:00
Jason Hiltz-Laforge
44b9ad604f Merge pull request #381 from Shopify/add_object_profiling
Add object profiling in addition to cpu profiling
2014-07-16 16:41:31 -04:00
Jason Hiltz-Laforge
535d549978 Merge pull request #380 from Shopify/optimize_interrupt_handling
Optimize checking for interrupts by replacing any? with NOT empty?
2014-07-16 11:36:32 -04:00
Jason Hiltz-Laforge
32349033a9 Add object profiling in addition to cpu profiling 2014-07-16 15:32:04 +00:00
Jason Hiltz-Laforge
fd8c30070a Adding tests, spy dependency 2014-07-16 15:05:45 +00:00
Jason Hiltz-Laforge
4cfc05e32a Optimize checking for interrupts by replacing any? with NOT empty? 2014-07-15 18:31:40 +00:00
Florian Weingarten
c4bc6cf3db Merge pull request #378 from Shopify/revert_352
Revert: Add error messages for missing variables when :strict
2014-07-08 17:31:19 +02:00
Florian Weingarten
0ac3ec7834 Revert "Merge pull request #352 from gaiottino/master"
This reverts commit 553b0926ae, reversing
changes made to 628ab3dc6a.
2014-07-08 14:48:19 +00:00
Florian Weingarten
8909c9f27a add regression tests for #377 2014-07-08 14:47:39 +00:00
David Cornu
51c708c8f8 Merge pull request #376 from Shopify/tag-class-reloading
Avoid holding on to stale tag classes
2014-07-03 12:01:50 -04:00
David Cornu
f57383af37 Allow tag classes to be reloaded when using Liquid.cache_classes is false
Because Liquid keeps a reference to tag classes, Rails class reloading may
cause problems with custom tags. This commit introduces a setting that
allows these classes to be resolved when required.
2014-07-02 20:03:18 +00:00
Jean Boussier
d007c50856 Merge pull request #374 from Shopify/remove-duplicate-ruby-version
Give some ❤️ to travis.yml
2014-07-02 11:04:38 -04:00
Tobias Lütke
101f125a69 Merge pull request #375 from Shopify/relative-link
Fixed relative link
2014-06-30 17:19:37 -04:00
Tony Zou
5110ca906c Fixed relative link 2014-06-30 16:43:37 -04:00
Jean Boussier
ac0f63eda9 rbx-19mode is not supported anymore 2014-06-28 16:21:08 -04:00
Jean Boussier
1372274fca Do not intall stackprof under jruby nor rubinius 2014-06-28 16:09:14 -04:00
Jean Boussier
69951be173 Canonicalize travis ruby versions 2014-06-28 16:02:37 -04:00
Arthur Nogueira Neves
c9863836cd Merge pull request #371 from Shopify/unfreeze-version-string
Unfreeze version string
2014-06-24 09:59:46 -05:00
Jean Boussier
14b8d824d7 Unfreeze version string
It breaks gem installation on ruby 1.9.x (older rubygem?)
2014-06-23 10:47:10 -04:00
Florian Weingarten
114a37d9ba add additional tests for https://github.com/jekyll/jekyll/pull/2505 2014-06-23 09:28:24 -04:00
Arthur Nogueira Neves
30bd9ad957 Merge pull request #368 from Shopify/add-round-ceil-and-floor
[Liquid] Add round, ceil and floor standard filters
2014-06-16 10:32:17 -05:00
Christian Blais
2239921804 [Liquid] Add round, ceil and floor standard filters 2014-06-16 11:15:53 -04:00
Florian Weingarten
1ea178e7a8 Merge pull request #363 from rrrene/patch-2
Update docs badge in README
2014-06-05 10:32:47 -04:00
René Föhring
5650c7eea1 Update docs badge in README
Update the URL of the docs badge to include it from inch-ci.org instead of inch-pages.github.io (the former being the successor of the Inch Pages project).

[ci skip]
2014-06-05 12:25:26 +02:00
Arthur Neves
553b0926ae Merge pull request #352 from gaiottino/master
Add error messages for missing variables when :strict

Conflicts:
	History.md
2014-05-06 10:16:45 -04:00
Daniel Gaiottino
2bac6267f9 Add error messages for missing variables when :strict 2014-05-06 16:12:46 +02:00
Florian Weingarten
628ab3dc6a add test for numerical sort 2014-05-04 19:50:38 -04:00
Florian Weingarten
2eb552c65d Merge pull request #354 from Dillon-Benson/patch-3
use attr_writer instead of error_mode= method
2014-05-02 18:15:33 -04:00
Dillon Benson
6e40746ce4 use attr_writer instead of error_mode= method 2014-05-02 17:18:56 -04:00
Thierry Joyal
75068e8fa4 Merge pull request #349 from Shopify/render_bang_allow_not_safe_contexts
render! will properly force rethrow of errors if context is passed as an argument
2014-05-01 12:52:06 -04:00
Thierry Joyal
ad1152853a render! will properly force rethrow of errors if context is passed as an argument 2014-05-01 16:44:00 +00:00
David Cornu
73098ac5bc Merge pull request #351 from Shopify/date-cleanup
Move date coercion to #to_date
2014-04-30 20:47:39 -04:00
David Cornu
8bc3792c0e Move date coercion to #to_date 2014-04-30 22:32:36 +00:00
Florian Weingarten
3ef29c624c Merge pull request #347 from Dillon-Benson/patch-1
remove template return
2014-04-30 08:23:36 -04:00
Dillon Benson
a85fb38769 remove template return 2014-04-30 03:00:16 -04:00
Florian Weingarten
6a1c3cff1a update history.md 2014-04-29 15:26:22 -04:00
Florian Weingarten
bde32018dd Merge pull request #346 from Shopify/false_rendering
Fix broken rendering of variables which are equal to false (closes #345)
2014-04-29 15:21:30 -04:00
Florian Weingarten
2a12f253bf Fix broken rendering of variables which are equal to false (closes #345) 2014-04-29 14:33:30 -04:00
Bogdan Gusiev
fa14fd02e7 Raise Liquid::ArugmentError when condition has wrong usage
Condition now raises ::ArgumentError when built wrongly.
This patch make it raise Liquid::ArgumentError instead
to indicate a liquid markup error instead of ruby error.
2014-04-21 16:42:37 +03:00
Arthur Nogueira Neves
f15d24509d Merge pull request #338 from here/docs-filter-date-strftime
Update standardfilters.rb add docs to date filter
2014-04-12 20:14:44 -04:00
mikey dubs
09e4378cfb Update standardfilters.rb add docs to date filter
Added '%s' - Number of seconds since 1970-01-01 00:00:00 UTC to included list of flags
Added link to Ruby docs on Time#strftime() to allow easier discovery of unlisted filter options.
2014-04-12 15:21:06 -07:00
Dylan Thacker-Smith
af0e26fb16 Merge pull request #337 from Shopify/remove-rails-dev-dependancy
Remove active_support from the development dependancies.
2014-04-11 15:08:34 -04:00
Dylan Thacker-Smith
f5502e8152 Remove active_support from the development dependancies. 2014-04-11 14:26:40 -04:00
Arthur Nogueira Neves
c098235baa Merge pull request #335 from rrrene/patch-1
Add docs badge to README
2014-04-05 11:46:32 -04:00
René Föhring
0e2bf768ba Add docs badge to README 2014-04-05 12:24:56 +02:00
Dylan Thacker-Smith
4c477c2087 Merge pull request #333 from Shopify/remove-liquid-view
Remove ActionView template handler
2014-03-31 10:16:37 -04:00
Dylan Thacker-Smith
cd7fc050b1 Remove ActionView template handler
Fixes #21
2014-03-29 15:59:42 -04:00
Dylan Thacker-Smith
8291c5e72c Merge pull request #329 from Shopify/seperate-integration-tests
Separate unit and integration tests.
2014-03-26 16:04:39 -04:00
Dylan Thacker-Smith
7e45155aa9 Seperate unit and integration tests.
This makes it easier to re-use the integration tests in a seperate gem that
optimizes parts of liquid with a native implementation.
2014-03-26 15:47:07 -04:00
Florian Weingarten
3dbb35d823 Merge branch 'freeze_all_the_things'
Conflicts:
	History.md
	lib/liquid/tags/assign.rb
	lib/liquid/tags/capture.rb
	lib/liquid/tags/decrement.rb
	lib/liquid/tags/if.rb
2014-03-24 12:39:34 -04:00
Florian Weingarten
44f29a87a8 Update history 2014-03-24 12:35:47 -04:00
Dylan Thacker-Smith
f0afbc27e0 Add regression test for raw tags with open variable tags. 2014-03-24 10:01:03 -04:00
Dylan Thacker-Smith
fdf03076e0 Revert "Merge pull request #325 from Shopify/remove-variable-incomplete-end"
That pull request broke raw tags with open variable tags. E.g.

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

This reverts commit fbaabf3b59, reversing
changes made to af24d2c2ab.
2014-03-24 09:59:07 -04:00
Dylan Thacker-Smith
d1bfda15e3 Add profile:strict rake task. 2014-03-21 21:54:53 -04:00
Dylan Thacker-Smith
d8d9984a7b Remove some unused regexes. 2014-03-21 15:50:14 -04:00
Dylan Thacker-Smith
fbaabf3b59 Merge pull request #325 from Shopify/remove-variable-incomplete-end
Allow quoted single curly braces in variables.
2014-03-21 13:48:47 -04:00
Dylan Thacker-Smith
7e0ef867d2 Make tag/variable termination error clearer. 2014-03-21 02:04:01 -04:00
Dylan Thacker-Smith
3682414cc4 Allow quoted single curly braces in variables. 2014-03-21 02:04:01 -04:00
Dylan Thacker-Smith
af24d2c2ab Add missing PR reference and author to a History.md entry. 2014-03-21 02:03:31 -04:00
Dylan Thacker-Smith
4ee43bc5d2 Merge pull request #324 from Shopify/multiline-tags-and-vars
Allow newlines in tags and variables.
2014-03-21 00:26:14 -04:00
Dylan Thacker-Smith
1320a69fca Merge pull request #323 from Shopify/render_bang_in_tests
Use render! in tests to make debugging test failures easier.
2014-03-20 18:33:40 -04:00
Dylan Thacker-Smith
3b14e27f55 Allow newlines in tags and variables. 2014-03-20 17:27:03 -04:00
Dylan Thacker-Smith
face33a137 Merge pull request #321 from Shopify/move-tag-parse-out-of-initialize
Refactor to create tags with a parse class method instead of new.
2014-03-20 16:15:58 -04:00
Dylan Thacker-Smith
d4ecaff8b8 Refactor to create tags with a parse class method instead of new.
By moving parse out of the initializer, we can call super at the start of
the initializers for subclasses, and avoid the nasty allocate hack.
2014-03-20 16:10:10 -04:00
Dylan Thacker-Smith
a5990042ff Use render! in tests to make debugging test failures easier. 2014-03-20 12:04:17 -04:00
Florian Weingarten
e190bbba9e move change to top 2014-03-19 18:15:31 -04:00
Dylan Thacker-Smith
4b5e41d04e Merge pull request #322 from Shopify/use-render-bang-in-benchmarks
Use render! in benchmarks to avoid making it faster by breaking things.
2014-03-19 18:06:15 -04:00
Dylan Thacker-Smith
4b69f6ae83 Use render! in benchmarks to avoid making it faster by breaking things. 2014-03-19 18:01:33 -04:00
Dylan Thacker-Smith
b9feb415f6 Merge pull request #320 from Shopify/move-table-row-tag
Move definition for TableRow to the tags folder.
2014-03-18 17:16:32 -04:00
Dylan Thacker-Smith
92781ec43b Move definition for TableRow to the tags folder. 2014-03-18 17:13:39 -04:00
Dylan Thacker-Smith
ff5c1f83f7 Merge pull request #318 from Shopify/stackprof
Use stackprof for profiling.
2014-03-14 10:27:46 -04:00
Dylan Thacker-Smith
e2b337af2f Merge pull request #317 from Shopify/string-anchors
Use start and end of string rather than line matching in regexes.
2014-03-14 10:17:33 -04:00
Dylan Thacker-Smith
f373b1003d Use stackprof for profiling. 2014-03-14 10:03:50 -04:00
Dylan Thacker-Smith
503d924274 Use start and end of string rather than line matching in regexes. 2014-03-13 17:56:42 -04:00
Florian Weingarten
0a7de51e2b Revert some freezes on non-literals 2014-01-27 14:56:07 -05:00
Florian Weingarten
43ac8d560b Freeze all the things 2014-01-07 12:35:16 -05:00
118 changed files with 2365 additions and 2065 deletions

2
.gitignore vendored
View File

@@ -5,6 +5,4 @@ pkg
*.rbc
.rvmrc
.ruby-version
*.bundle
/tmp
Gemfile.lock

View File

@@ -1,13 +1,13 @@
rvm:
- 1.9.3
- 2.0.0
- 2.1.0
- 1.9
- 2.0
- 2.1
- jruby-19mode
- jruby-head
- rbx-19mode
- rbx-2
matrix:
allow_failures:
- rvm: rbx-19mode
- rvm: rbx-2
- rvm: jruby-head
script: "rake test"

View File

@@ -1,3 +1,9 @@
source 'https://rubygems.org'
gemspec
gem 'stackprof', platforms: :mri_21
group :test do
gem 'spy', '0.4.1'
gem 'benchmark-ips'
end

View File

@@ -3,6 +3,17 @@
## 3.0.0 / not yet released / branch "master"
* ...
* Add uniq to standard filters [Florian Weingarten, fw42]
* Add exception_handler feature, see #397 and #254 [Bogdan Gusiev, bogdan and Florian Weingarten, fw42]
* Optimize variable parsing to avoid repeated regex evaluation during template rendering #383 [Jason Hiltz-Laforge, jasonhl]
* Optimize checking for block interrupts to reduce object allocation #380 [Jason Hiltz-Laforge, jasonhl]
* Properly set context rethrow_errors on render! #349 [Thierry Joyal, tjoyal]
* Fix broken rendering of variables which are equal to false, see #345 [Florian Weingarten, fw42]
* Remove ActionView template handler [Dylan Thacker-Smith, dylanahsmith]
* Freeze lots of string literals for new Ruby 2.1 optimization, see #297 [Florian Weingarten, fw42]
* Allow newlines in tags and variables, see #324 [Dylan Thacker-Smith, dylanahsmith]
* Tag#parse is called after initialize, which now takes options instead of tokens as the 3rd argument. See #321 [Dylan Thacker-Smith, dylanahsmith]
* Raise `Liquid::ArgumentError` instead of `::ArgumentError` when filter has wrong number of arguments #309 [Bogdan Gusiev, bogdan]
* Add a to_s default for liquid drops, see #306 [Adam Doeler, releod]
* Add strip, lstrip, and rstrip to standard filters [Florian Weingarten, fw42]
* Make if, for & case tags return complete and consistent nodelists, see #250 [Nick Jones, dntj]
@@ -21,9 +32,14 @@
* Fix clashing method names in enumerable drops, see #238 [Florian Weingarten, fw42]
* Make map filter work on enumerable drops, see #233 [Florian Weingarten, fw42]
* Improved whitespace stripping for blank blocks, related to #216 [Florian Weingarten, fw42]
* Raise `Liquid::ArgumentError` instead of `::ArgumentError` when filter has wrong number of arguments #309 [Bogdan Gusiev, bogdan]
## 2.6.0 / 2013-11-25 / branch "2.6-stable"
## 2.6.1 / 2014-01-10 / branch "2-6-stable"
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]
* Prevent arbitrary method invocation on condition objects, see #274 [Dylan Thacker-Smith, dylanahsmith]
## 2.6.0 / 2013-11-25
IMPORTANT: Liquid 2.6 is going to be the last version of Liquid which maintains explicit Ruby 1.8 compatability.
The following releases will only be tested against Ruby 1.9 and Ruby 2.0 and are likely to break on Ruby 1.8.
@@ -47,7 +63,13 @@ The following releases will only be tested against Ruby 1.9 and Ruby 2.0 and are
* Better documentation for 'include' tag (closes #163) [Peter Schröder, phoet]
* Use of BigDecimal on filters to have better precision (closes #155) [Arthur Nogueira Neves, arthurnn]
## 2.5.4 / 2013-11-11 / branch "2.5-stable"
## 2.5.5 / 2014-01-10 / branch "2-5-stable"
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]
* Prevent arbitrary method invocation on condition objects, see #274 [Dylan Thacker-Smith, dylanahsmith]
## 2.5.4 / 2013-11-11
* Fix "can't convert Fixnum into String" for "replace", see #173, [wǒ_is神仙, jsw0528]

View File

@@ -1,4 +1,6 @@
[![Build Status](https://secure.travis-ci.org/Shopify/liquid.png?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)
# Liquid template engine
* [Contributing guidelines](CONTRIBUTING.md)

View File

@@ -1,6 +1,5 @@
require 'rake'
require 'rake/testtask'
require 'rake/extensiontask'
$LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
require "liquid/version"
@@ -9,7 +8,7 @@ task :default => 'test'
desc 'run test suite with default parser'
Rake::TestTask.new(:base_test) do |t|
t.libs << '.' << 'lib' << 'test'
t.test_files = FileList['test/liquid/**/*_test.rb']
t.test_files = FileList['test/{integration,unit}/**/*_test.rb']
t.verbose = false
end
@@ -65,13 +64,9 @@ namespace :profile do
ruby "./performance/profile.rb"
end
task :stackprof do
ruby "./performance/stackprof.rb"
end
desc "Run KCacheGrind"
task :grind => :run do
system "qcachegrind /tmp/liquid.rubyprof_calltreeprinter.txt"
desc "Run the liquid profile/performance coverage with strict parsing"
task :strict do
ruby "./performance/profile.rb strict"
end
end
@@ -80,8 +75,3 @@ desc "Run example"
task :example do
ruby "-w -d -Ilib example/server/server.rb"
end
Rake::ExtensionTask.new "liquid" do |ext|
ext.lib_dir = "lib/liquid"
end
Rake::Task[:test].prerequisites << :compile

View File

@@ -3,4 +3,4 @@
<p>It is {{date}}</p>
<p>Check out the <a href="http://localhost:3000/products">Products</a> screen </p>
<p>Check out the <a href="/products">Products</a> screen </p>

View File

@@ -1,168 +0,0 @@
#include "liquid_ext.h"
VALUE cLiquidBlock;
ID intern_assert_missing_delimitation, intern_block_delimiter, intern_is_blank, intern_new,
intern_new_with_options, intern_tags, intern_unknown_tag, intern_unterminated_tag,
intern_unterminated_variable;
struct liquid_tag
{
char *name, *markup;
long name_length, markup_length;
};
static bool parse_tag(struct liquid_tag *tag, char *token, long token_length)
{
// Strip {{ and }} braces
token += 2;
token_length -= 4;
char *end = token + token_length;
while (token < end && isspace(*token))
token++;
tag->name = token;
char c = *token;
while (token < end && (isalnum(c) || c == '_'))
c = *(++token);
tag->name_length = token - tag->name;
if (!tag->name_length) {
memset(tag, 0, sizeof(*tag));
return false;
}
while (token < end && isspace(*token))
token++;
tag->markup = token;
char *last = end - 1;
while (token < last && isspace(*last))
last--;
end = last + 1;
tag->markup_length = end - token;
return true;
}
static VALUE rb_parse_body(VALUE self, VALUE tokenizerObj)
{
struct liquid_tokenizer *tokenizer = LIQUID_TOKENIZER_GET_STRUCT(tokenizerObj);
bool blank = true;
VALUE nodelist = rb_iv_get(self, "@nodelist");
if (nodelist == Qnil) {
nodelist = rb_ary_new();
rb_iv_set(self, "@nodelist", nodelist);
} else {
rb_ary_clear(nodelist);
}
struct token token;
while (true) {
liquid_tokenizer_next(tokenizer, &token);
switch (token.type) {
case TOKEN_NONE:
/*
* 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
*/
rb_funcall(self, intern_assert_missing_delimitation, 0);
goto done;
case TOKEN_INVALID:
{
VALUE token_obj = rb_str_new(token.str, token.length);
if (token.str[1] == '%')
rb_funcall(self, intern_unterminated_tag, 1, token_obj);
else
rb_funcall(self, intern_unterminated_variable, 1, token_obj);
break;
}
case TOKEN_TAG:
{
struct liquid_tag tag;
if (!parse_tag(&tag, token.str, token.length)) {
// FIXME: provide more appropriate error message
rb_funcall(self, intern_unterminated_tag, 1, rb_str_new(token.str, token.length));
} else {
if (tag.name_length >= 3 && !memcmp(tag.name, "end", 3)) {
VALUE block_delimiter = rb_funcall(self, intern_block_delimiter, 0);
if (TYPE(block_delimiter) == T_STRING &&
tag.name_length == RSTRING_LEN(block_delimiter) &&
!memcmp(tag.name, RSTRING_PTR(block_delimiter), tag.name_length))
{
goto done;
}
}
VALUE tags = rb_funcall(cLiquidTemplate, intern_tags, 0);
Check_Type(tags, T_HASH);
VALUE tag_name = rb_str_new(tag.name, tag.name_length);
VALUE tag_class = rb_hash_lookup(tags, tag_name);
VALUE markup = rb_str_new(tag.markup, tag.markup_length);
if (tag_class != Qnil) {
VALUE options = rb_iv_get(self, "@options");
if (options == Qnil)
options = rb_hash_new();
VALUE new_tag = rb_funcall(tag_class, intern_new_with_options, 4,
tag_name, markup, tokenizerObj, options);
if (blank) {
VALUE blank_block = rb_funcall(new_tag, intern_is_blank, 0);
if (blank_block == Qnil || blank_block == Qfalse)
blank = false;
}
rb_ary_push(nodelist, new_tag);
} else {
rb_funcall(self, intern_unknown_tag, 3, tag_name, markup, tokenizerObj);
/*
* multi-block tags may store the nodelist in a block array on unknown_tag
* then replace @nodelist with a new array. We need to use the new array
* for the block following the tag token.
*/
nodelist = rb_iv_get(self, "@nodelist");
}
}
break;
}
case TOKEN_VARIABLE:
{
VALUE markup = rb_str_new(token.str + 2, token.length - 4);
VALUE options = rb_iv_get(self, "@options");
VALUE new_var = rb_funcall(cLiquidVariable, intern_new, 2, markup, options);
rb_ary_push(nodelist, new_var);
blank = false;
break;
}
case TOKEN_STRING:
rb_ary_push(nodelist, rb_str_new(token.str, token.length));
if (blank) {
int i;
for (i = 0; i < token.length; i++) {
if (!isspace(token.str[i])) {
blank = false;
break;
}
}
}
break;
}
}
done:
rb_iv_set(self, "@blank", blank ? Qtrue : Qfalse);
return Qnil;
}
void init_liquid_block()
{
intern_assert_missing_delimitation = rb_intern("assert_missing_delimitation!");
intern_block_delimiter = rb_intern("block_delimiter");
intern_is_blank = rb_intern("blank?");
intern_new = rb_intern("new");
intern_new_with_options = rb_intern("new_with_options");
intern_tags = rb_intern("tags");
intern_unknown_tag = rb_intern("unknown_tag");
intern_unterminated_tag = rb_intern("unterminated_tag");
intern_unterminated_variable = rb_intern("unterminated_variable");
cLiquidBlock = rb_define_class_under(mLiquid, "Block", cLiquidTag);
rb_define_method(cLiquidBlock, "parse_body", rb_parse_body, 1);
}

View File

@@ -1,8 +0,0 @@
#ifndef LIQUID_BLOCK_H
#define LIQUID_BLOCK_H
void init_liquid_block();
extern VALUE cLiquidBlock;
#endif

View File

@@ -1,3 +0,0 @@
require 'mkmf'
$CFLAGS << ' -Wall'
create_makefile("liquid/liquid")

View File

@@ -1,16 +0,0 @@
#include "liquid_ext.h"
VALUE mLiquid;
VALUE cLiquidTemplate, cLiquidTag, cLiquidVariable;
void Init_liquid(void)
{
mLiquid = rb_define_module("Liquid");
cLiquidTemplate = rb_define_class_under(mLiquid, "Template", rb_cObject);
cLiquidTag = rb_define_class_under(mLiquid, "Tag", rb_cObject);
cLiquidVariable = rb_define_class_under(mLiquid, "Variable", rb_cObject);
init_liquid_tokenizer();
init_liquid_block();
init_liquid_variable();
}

View File

@@ -1,16 +0,0 @@
#ifndef LIQUID_EXT_H
#define LIQUID_EXT_H
#include <stdbool.h>
#include <ctype.h>
#include <ruby.h>
#include "tokenizer.h"
#include "block.h"
#include "utils.h"
#include "variable.h"
extern VALUE mLiquid;
extern VALUE cLiquidTemplate, cLiquidTag, cLiquidVariable;
#endif

View File

@@ -1,113 +0,0 @@
#include "liquid_ext.h"
VALUE cLiquidTokenizer;
static void free_tokenizer(void *ptr)
{
struct liquid_tokenizer *tokenizer = ptr;
xfree(tokenizer);
}
static VALUE rb_allocate(VALUE klass)
{
VALUE obj;
struct liquid_tokenizer *tokenizer;
obj = Data_Make_Struct(klass, struct liquid_tokenizer, NULL, free_tokenizer, tokenizer);
return obj;
}
static VALUE rb_initialize(VALUE self, VALUE source)
{
struct liquid_tokenizer *tokenizer;
Check_Type(source, T_STRING);
Data_Get_Struct(self, struct liquid_tokenizer, tokenizer);
tokenizer->cursor = RSTRING_PTR(source);
tokenizer->length = RSTRING_LEN(source);
return Qnil;
}
void liquid_tokenizer_next(struct liquid_tokenizer *tokenizer, struct token *token)
{
if (tokenizer->length <= 0) {
memset(token, 0, sizeof(*token));
return;
}
token->type = TOKEN_STRING;
char *cursor = tokenizer->cursor;
char *last = tokenizer->cursor + tokenizer->length - 1;
while (cursor < last) {
if (*cursor++ != '{')
continue;
char c = *cursor++;
if (c != '%' && c != '{')
continue;
if (cursor - tokenizer->cursor > 2) {
token->type = TOKEN_STRING;
cursor -= 2;
goto found;
}
char *incomplete_end = cursor;
token->type = TOKEN_INVALID;
if (c == '%') {
while (cursor < last) {
if (*cursor++ != '%')
continue;
c = *cursor++;
while (c == '%' && cursor <= last)
c = *cursor++;
if (c != '}')
continue;
token->type = TOKEN_TAG;
goto found;
}
cursor = incomplete_end;
goto found;
} else {
while (cursor < last) {
if (*cursor++ != '}')
continue;
if (*cursor++ != '}') {
incomplete_end = cursor - 1;
continue;
}
token->type = TOKEN_VARIABLE;
goto found;
}
cursor = incomplete_end;
goto found;
}
}
cursor = last + 1;
found:
token->str = tokenizer->cursor;
token->length = cursor - tokenizer->cursor;
tokenizer->cursor += token->length;
tokenizer->length -= token->length;
}
static VALUE rb_next(VALUE self)
{
struct liquid_tokenizer *tokenizer;
Data_Get_Struct(self, struct liquid_tokenizer, tokenizer);
struct token token;
liquid_tokenizer_next(tokenizer, &token);
if (token.type == TOKEN_NONE)
return Qnil;
return rb_str_new(token.str, token.length);
}
void init_liquid_tokenizer()
{
cLiquidTokenizer = rb_define_class_under(mLiquid, "Tokenizer", rb_cObject);
rb_define_alloc_func(cLiquidTokenizer, rb_allocate);
rb_define_method(cLiquidTokenizer, "initialize", rb_initialize, 1);
rb_define_method(cLiquidTokenizer, "next", rb_next, 0);
rb_define_alias(cLiquidTokenizer, "shift", "next");
}

View File

@@ -1,30 +0,0 @@
#ifndef LIQUID_TOKENIZER_H
#define LIQUID_TOKENIZER_H
extern VALUE cLiquidTokenizer;
enum token_type {
TOKEN_NONE,
TOKEN_INVALID,
TOKEN_STRING,
TOKEN_TAG,
TOKEN_VARIABLE
};
struct token {
enum token_type type;
char *str;
int length;
};
struct liquid_tokenizer {
char *cursor;
int length;
};
void init_liquid_tokenizer();
void liquid_tokenizer_next(struct liquid_tokenizer *tokenizer, struct token *token);
#define LIQUID_TOKENIZER_GET_STRUCT(obj) ((struct liquid_tokenizer *)obj_get_data_ptr(obj, cLiquidTokenizer))
#endif

View File

@@ -1,21 +0,0 @@
#include <ruby.h>
void raise_type_error(VALUE expected, VALUE got)
{
rb_raise(rb_eTypeError, "wrong argument type %s (expected %s)",
rb_class2name(got), rb_class2name(expected));
}
void check_class(VALUE obj, int type, VALUE klass)
{
Check_Type(obj, type);
VALUE obj_klass = RBASIC_CLASS(obj);
if (obj_klass != klass)
raise_type_error(klass, obj_klass);
}
void *obj_get_data_ptr(VALUE obj, VALUE klass)
{
check_class(obj, T_DATA, klass);
return DATA_PTR(obj);
}

View File

@@ -1,8 +0,0 @@
#ifndef LIQUID_UTILS_H
#define LIQUID_UTILS_H
void raise_type_error(VALUE expected, VALUE got);
void check_class(VALUE klass);
void *obj_get_data_ptr(VALUE obj, VALUE klass);
#endif

View File

@@ -1,179 +0,0 @@
#include "liquid_ext.h"
VALUE cLiquidVariable;
extern VALUE mLiquid;
static void free_variable(void *ptr)
{
struct liquid_variable *variable = ptr;
xfree(variable);
}
static VALUE rb_variable_allocate(VALUE klass)
{
VALUE obj;
struct liquid_variable *variable;
obj = Data_Make_Struct(klass, struct liquid_variable, NULL, free_variable, variable);
return obj;
}
static inline int skip_whitespace(char * str, int len)
{
int skipped = 0; char * ptr = str;
while (skipped < len && isspace(*ptr))
{skipped++; ptr++;}
return skipped;
}
static char * get_quoted_fragment(char * str, int len, int * ret_size, int * end_offset, bool colon)
{
int p = 0; /* Current position in string */
int start = -1, end = -1; /* Start and end indices for the found string */
char quoted_by = -1; /* Is the current part of string quoted by a single or double quote? If so
ignore any special chars */
while (p < len) {
switch (str[p]) {
case '"':
if (start == -1) {start = p; quoted_by = '"';}
else if (str[start] == '"') {end = p; goto quoted_fragment_found;}
else if (quoted_by == -1) quoted_by = '"';
else if (quoted_by == '"') quoted_by = -1;
break;
case '\'':
if (start == -1) {start = p; quoted_by = '\'';}
else if (str[start] == '\'') {end = p; goto quoted_fragment_found;}
else if (quoted_by == -1) quoted_by = '\'';
else if (quoted_by == '\'') quoted_by = -1;
break;
case ':':
if (colon)
if (start != -1 && quoted_by == -1) {end = p-1; goto quoted_fragment_found;}
else
if (start == -1) start = p;
break;
case '|':
case ',':
case '\n':
case '\r':
case '\f':
case '\t':
case '\v':
case ' ':
if (start != -1 && quoted_by == -1) {end = p-1; goto quoted_fragment_found;}
break;
default:
if (start == -1) start = p;
break;
}
p++;
}
if (p == len && start != -1 && end == -1) end = len-1;
quoted_fragment_found:
if (end >= start) {
*ret_size = end-start+1;
*end_offset = end+1;
return &str[start];
} else {
*ret_size = 0;
return NULL;
}
}
static VALUE get_filters(char * str, int len, VALUE self) {
VALUE filters_arr = rb_ary_new();
int p = 0;
int ret_size, end_offset;
char * f;
while(p<len) {
if (str[p] == '|') {
VALUE filter = rb_ary_new();
VALUE f_args = rb_ary_new();
p += skip_whitespace(&str[p+1], len-p-1);
f = get_quoted_fragment(&str[p], len-p, &ret_size, &end_offset, true);
p += end_offset;
if (f) {
if (f[ret_size-1] == ':') ret_size--;
rb_ary_push(filter, rb_str_new(f, ret_size));
}
/* Check for filter arguments */
do {
if (p<len) {
p += skip_whitespace(&str[p], len-p);
// printf("\n1. %.*s\n", len-p, &str[p]);
if (str[p] != '|') {
f = get_quoted_fragment(&str[p], len-p, &ret_size, &end_offset, false);
// printf("\n2. %.*s\n", ret_size, f);
p += end_offset;
p += skip_whitespace(&str[p], len-p);
if (str[p] == '|') p--;
if (f) rb_ary_push(f_args, rb_str_new(f, ret_size));
} else p--;
}
} while (str[p] == ',' || str[p] == ':');
rb_ary_push(filter, f_args);
/* Add to filters_arr array */
rb_ary_push(filters_arr, filter);
}
p++;
}
return filters_arr;
}
static VALUE rb_variable_lax_parse(VALUE self, VALUE m)
{
char * markup = RSTRING_PTR(m);
int markup_len = RSTRING_LEN(m);
char * cursor = markup; int cursor_pos = 0;
VALUE filters_arr;
int size, end_offset;
/* Extract name */
cursor_pos += skip_whitespace(markup, markup_len);
cursor = markup + cursor_pos;
cursor = get_quoted_fragment(cursor, markup_len - cursor_pos, &size, &end_offset, false);
if (cursor == NULL) {
rb_iv_set(self, "@name", Qnil);
filters_arr = rb_ary_new();
rb_iv_set(self, "@filters", filters_arr);
}
else
{
rb_iv_set(self, "@name", rb_str_new(cursor, size));
/* Extract filters */
if (end_offset < markup_len) {
cursor = &markup[end_offset];
filters_arr = get_filters(cursor, markup_len - end_offset, self);
rb_iv_set(self, "@filters", filters_arr);
}
}
return filters_arr;
}
void init_liquid_variable()
{
cLiquidVariable = rb_define_class_under(mLiquid, "Variable", rb_cObject);
rb_define_alloc_func(cLiquidVariable, rb_variable_allocate);
rb_define_method(cLiquidVariable, "lax_parse", rb_variable_lax_parse, 1);
}

View File

@@ -1,13 +0,0 @@
#ifndef LIQUID_VARIABLE_H
#define LIQUID_VARIABLE_H
#include <regex.h>
struct liquid_variable {
char *markup; long markup_len;
char *name; long name_len;
};
void init_liquid_variable();
#endif

View File

@@ -1,8 +0,0 @@
require 'liquid'
require 'extras/liquid_view'
if defined? ActionView::Template and ActionView::Template.respond_to? :register_template_handler
ActionView::Template
else
ActionView::Base
end.register_template_handler(:liquid, LiquidView)

View File

@@ -1,51 +0,0 @@
# LiquidView is a action view extension class. You can register it with rails
# and use liquid as an template system for .liquid files
#
# Example
#
# ActionView::Base::register_template_handler :liquid, LiquidView
class LiquidView
PROTECTED_ASSIGNS = %w( template_root response _session template_class action_name request_origin session template
_response url _request _cookies variables_added _flash params _headers request cookies
ignore_missing_templates flash _params logger before_filter_chain_aborted headers )
PROTECTED_INSTANCE_VARIABLES = %w( @_request @controller @_first_render @_memoized__pick_template @view_paths
@helpers @assigns_added @template @_render_stack @template_format @assigns )
def self.call(template)
"LiquidView.new(self).render(template, local_assigns)"
end
def initialize(view)
@view = view
end
def render(template, local_assigns = nil)
@view.controller.headers["Content-Type"] ||= 'text/html; charset=utf-8'
# Rails 2.2 Template has source, but not locals
if template.respond_to?(:source) && !template.respond_to?(:locals)
assigns = (@view.instance_variables - PROTECTED_INSTANCE_VARIABLES).inject({}) do |hash, ivar|
hash[ivar[1..-1]] = @view.instance_variable_get(ivar)
hash
end
else
assigns = @view.assigns.reject{ |k,v| PROTECTED_ASSIGNS.include?(k) }
end
source = template.respond_to?(:source) ? template.source : template
local_assigns = (template.respond_to?(:locals) ? template.locals : local_assigns) || {}
if content_for_layout = @view.instance_variable_get("@content_for_layout")
assigns['content_for_layout'] = content_for_layout
end
assigns.merge!(local_assigns.stringify_keys)
liquid = Liquid::Template.parse(source)
liquid.render(assigns, :filters => [@view.controller.master_helper_module], :registers => {:action_view => @view, :controller => @view.controller})
end
def compilable?
false
end
end

View File

@@ -21,27 +21,28 @@
module Liquid
FilterSeparator = /\|/
ArgumentSeparator = ','
FilterArgumentSeparator = ':'
VariableAttributeSeparator = '.'
ArgumentSeparator = ','.freeze
FilterArgumentSeparator = ':'.freeze
VariableAttributeSeparator = '.'.freeze
TagStart = /\{\%/
TagEnd = /\%\}/
VariableSignature = /\(?[\w\-\.\[\]]\)?/
VariableSegment = /[\w\-]/
VariableStart = /\{\{/
VariableEnd = /\}\}/
VariableIncompleteEnd = /\}\}?/
QuotedString = /"[^"]*"|'[^']*'/
QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o
StrictQuotedFragment = /"[^"]+"|'[^']+'|[^\s|:,]+/
FirstFilterArgument = /#{FilterArgumentSeparator}(?:#{StrictQuotedFragment})/o
OtherFilterArgument = /#{ArgumentSeparator}(?:#{StrictQuotedFragment})/o
SpacelessFilter = /\A(?:'[^']+'|"[^"]+"|[^'"])*#{FilterSeparator}(?:#{StrictQuotedFragment})(?:#{FirstFilterArgument}(?:#{OtherFilterArgument})*)?/o
Expression = /(?:#{QuotedFragment}(?:#{SpacelessFilter})*)/o
TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/o
AnyStartingTag = /\{\{|\{\%/
PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om
VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o
singleton_class.send(:attr_accessor, :cache_classes)
self.cache_classes = true
end
require 'liquid/liquid'
require "liquid/version"
require 'liquid/lexer'
require 'liquid/parser'
@@ -51,19 +52,26 @@ require 'liquid/extensions'
require 'liquid/errors'
require 'liquid/interrupts'
require 'liquid/strainer'
require 'liquid/expression'
require 'liquid/context'
require 'liquid/parser_switching'
require 'liquid/tag'
require 'liquid/block'
require 'liquid/document'
require 'liquid/variable'
require 'liquid/variable_lookup'
require 'liquid/range_lookup'
require 'liquid/file_system'
require 'liquid/template'
require 'liquid/htmltags'
require 'liquid/standardfilters'
require 'liquid/condition'
require 'liquid/module_ex'
require 'liquid/utils'
require 'liquid/token'
# Load all the tags of the standard library
#
Dir[File.dirname(__FILE__) + '/liquid/tags/*.rb'].each { |f| require f }
require 'liquid/profiler'
require 'liquid/profiler/hooks'

View File

@@ -1,12 +1,68 @@
module Liquid
class Block < Tag
def initialize(tag_name, markup, tokens)
super
parse_body(tokens)
end
FullToken = /\A#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
ContentOfVariable = /\A#{VariableStart}(.*)#{VariableEnd}\z/om
TAGSTART = "{%".freeze
VARSTART = "{{".freeze
def blank?
@blank || false
@blank
end
def parse(tokens)
@blank = true
@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
# warnings of this block and all sub-tags
@@ -14,51 +70,54 @@ module Liquid
all_warnings = []
all_warnings.concat(@warnings) if @warnings
(nodelist || []).each do |node|
all_warnings.concat(node.warnings || []) if node.respond_to?(:warnings)
(@children || []).each do |node|
all_warnings.concat(node.warnings || [])
end
all_warnings
end
def unknown_tag(tag, params, tokens)
case tag
when 'else'
raise SyntaxError.new(options[:locale].t("errors.syntax.unexpected_else",
:block_name => block_name))
when 'end'
raise SyntaxError.new(options[:locale].t("errors.syntax.invalid_delimiter",
:block_name => block_name,
:block_delimiter => block_delimiter))
else
raise SyntaxError.new(options[:locale].t("errors.syntax.unknown_tag", :tag => tag))
end
def end_tag
end
def block_delimiter
"end#{block_name}"
def unknown_tag(tag, params, tokens)
case tag
when 'else'.freeze
raise SyntaxError.new(options[:locale].t("errors.syntax.unexpected_else".freeze,
:block_name => block_name))
when 'end'.freeze
raise SyntaxError.new(options[:locale].t("errors.syntax.invalid_delimiter".freeze,
:block_name => block_name,
:block_delimiter => block_delimiter))
else
raise SyntaxError.new(options[:locale].t("errors.syntax.unknown_tag".freeze, :tag => tag))
end
end
def block_name
@tag_name
end
def block_delimiter
@block_delimiter ||= "end#{block_name}"
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
def unterminated_variable(token)
raise SyntaxError.new(options[:locale].t("errors.syntax.variable_termination", :token => token, :tag_end => VariableEnd.inspect))
end
def unterminated_tag(token)
raise SyntaxError.new(options[:locale].t("errors.syntax.tag_termination", :token => token, :tag_end => TagEnd.inspect))
end
def assert_missing_delimitation!
raise SyntaxError.new(options[:locale].t("errors.syntax.tag_never_closed", :block_name => block_name))
raise SyntaxError.new(options[:locale].t("errors.syntax.tag_never_closed".freeze, :block_name => block_name))
end
def render_all(list, context)
@@ -79,23 +138,29 @@ module Liquid
break
end
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")
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))
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

View File

@@ -8,14 +8,14 @@ module Liquid
#
class Condition #:nodoc:
@@operators = {
'==' => lambda { |cond, left, right| cond.send(:equal_variables, left, right) },
'!=' => lambda { |cond, left, right| !cond.send(:equal_variables, left, right) },
'<>' => lambda { |cond, left, right| !cond.send(:equal_variables, left, right) },
'<' => :<,
'>' => :>,
'>=' => :>=,
'<=' => :<=,
'contains' => lambda { |cond, left, right| left && right ? left.include?(right) : false }
'=='.freeze => lambda { |cond, left, right| cond.send(:equal_variables, left, right) },
'!='.freeze => lambda { |cond, left, right| !cond.send(:equal_variables, left, right) },
'<>'.freeze => lambda { |cond, left, right| !cond.send(:equal_variables, left, right) },
'<'.freeze => :<,
'>'.freeze => :>,
'>='.freeze => :>=,
'<='.freeze => :<=,
'contains'.freeze => lambda { |cond, left, right| left && right ? left.include?(right) : false }
}
def self.operators
@@ -61,7 +61,7 @@ module Liquid
end
def inspect
"#<Condition #{[@left, @operator, @right].compact.join(' ')}>"
"#<Condition #{[@left, @operator, @right].compact.join(' '.freeze)}>"
end
private
@@ -94,12 +94,16 @@ module Liquid
left, right = context[left], context[right]
operation = self.class.operators[op] || raise(ArgumentError.new("Unknown operator #{op}"))
operation = self.class.operators[op] || raise(Liquid::ArgumentError.new("Unknown operator #{op}"))
if operation.respond_to?(:call)
operation.call(self, left, right)
elsif left.respond_to?(operation) and right.respond_to?(operation)
left.send(operation, right)
begin
left.send(operation, right)
rescue ::ArgumentError => e
raise Liquid::ArgumentError.new(e.message)
end
else
nil
end

View File

@@ -14,16 +14,23 @@ module Liquid
# context['bob'] #=> nil class Context
class Context
attr_reader :scopes, :errors, :registers, :environments, :resource_limits
attr_accessor :exception_handler
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = {})
@environments = [environments].flatten
@scopes = [(outer_scope || {})]
@registers = registers
@errors = []
@rethrow_errors = rethrow_errors
@resource_limits = (resource_limits || {}).merge!({ :render_score_current => 0, :assign_score_current => 0 })
@environments = [environments].flatten
@scopes = [(outer_scope || {})]
@registers = registers
@errors = []
@resource_limits = (resource_limits || {}).merge!({ :render_score_current => 0, :assign_score_current => 0 })
@parsed_expression = Hash.new{ |cache, markup| cache[markup] = Expression.parse(markup) }
squash_instance_assigns_with_environments
@this_stack_used = false
if rethrow_errors
self.exception_handler = ->(e) { true }
end
@interrupts = []
@filters = []
end
@@ -71,7 +78,7 @@ module Liquid
# are there any not handled interrupts?
def has_interrupt?
@interrupts.any?
!@interrupts.empty?
end
# push an interrupt to the stack. this interrupt is considered not handled.
@@ -84,26 +91,22 @@ module Liquid
@interrupts.pop
end
def handle_error(e)
errors.push(e)
raise if @rethrow_errors
case e
when SyntaxError
"Liquid syntax error: #{e.message}"
else
"Liquid error: #{e.message}"
end
def handle_error(e, token)
e = Liquid::Error.error_with_line_number(e, token)
errors.push(e)
raise if exception_handler && exception_handler.call(e)
Liquid::Error.render(e)
end
def invoke(method, *args)
strainer.invoke(method, *args)
strainer.invoke(method, *args).to_liquid
end
# Push new local scope on the stack. use <tt>Context#stack</tt> instead
def push(new_scope={})
@scopes.unshift(new_scope)
raise StackLevelError, "Nesting too deep" if @scopes.length > 100
raise StackLevelError, "Nesting too deep".freeze if @scopes.length > 100
end
# Merge a hash of variables in the current local scope
@@ -125,11 +128,19 @@ module Liquid
# end
#
# context['var] #=> nil
def stack(new_scope={})
push(new_scope)
def stack(new_scope=nil)
old_stack_used = @this_stack_used
if new_scope
push(new_scope)
@this_stack_used = true
else
@this_stack_used = false
end
yield
ensure
pop
pop if @this_stack_used
@this_stack_used = old_stack_used
end
def clear_instance_assigns
@@ -138,138 +149,71 @@ module Liquid
# Only allow String, Numeric, Hash, Array, Proc, Boolean or <tt>Liquid::Drop</tt>
def []=(key, value)
unless @this_stack_used
@this_stack_used = true
push({})
end
@scopes[0][key] = value
end
def [](key)
resolve(key)
# Look up variable, either resolve directly after considering the name. We can directly handle
# Strings, digits, floats and booleans (true,false).
# If no match is made we lookup the variable in the current scope and
# later move up to the parent blocks to see if we can resolve the variable somewhere up the tree.
# Some special keywords return symbols. Those symbols are to be called on the rhs object in expressions
#
# Example:
# products == empty #=> products.empty?
def [](expression)
evaluate(@parsed_expression[expression])
end
def has_key?(key)
resolve(key) != nil
self[key] != nil
end
def evaluate(object)
object.respond_to?(:evaluate) ? object.evaluate(self) : object
end
# Fetches an object starting at the local scope and then moving up the hierachy
def find_variable(key)
# This was changed from find() to find_index() because this is a very hot
# path and find_index() is optimized in MRI to reduce object allocation
index = @scopes.find_index { |s| s.has_key?(key) }
scope = @scopes[index] if index
variable = nil
if scope.nil?
@environments.each do |e|
variable = lookup_and_evaluate(e, key)
unless variable.nil?
scope = e
break
end
end
end
scope ||= @environments.last || @scopes.last
variable ||= lookup_and_evaluate(scope, key)
variable = variable.to_liquid
variable.context = self if variable.respond_to?(:context=)
return variable
end
def lookup_and_evaluate(obj, key)
if (value = obj[key]).is_a?(Proc) && obj.respond_to?(:[]=)
obj[key] = (value.arity == 0) ? value.call : value.call(self)
else
value
end
end
private
LITERALS = {
nil => nil, 'nil' => nil, 'null' => nil, '' => nil,
'true' => true,
'false' => false,
'blank' => :blank?,
'empty' => :empty?
}
# Look up variable, either resolve directly after considering the name. We can directly handle
# Strings, digits, floats and booleans (true,false).
# If no match is made we lookup the variable in the current scope and
# later move up to the parent blocks to see if we can resolve the variable somewhere up the tree.
# Some special keywords return symbols. Those symbols are to be called on the rhs object in expressions
#
# Example:
# products == empty #=> products.empty?
def resolve(key)
if LITERALS.key?(key)
LITERALS[key]
else
case key
when /\A'(.*)'\z/ # Single quoted strings
$1
when /\A"(.*)"\z/ # Double quoted strings
$1
when /\A(-?\d+)\z/ # Integer and floats
$1.to_i
when /\A\((\S+)\.\.(\S+)\)\z/ # Ranges
(resolve($1).to_i..resolve($2).to_i)
when /\A(-?\d[\d\.]+)\z/ # Floats
$1.to_f
else
variable(key)
end
end
end
# Fetches an object starting at the local scope and then moving up the hierachy
def find_variable(key)
scope = @scopes.find { |s| s.has_key?(key) }
variable = nil
if scope.nil?
@environments.each do |e|
if variable = lookup_and_evaluate(e, key)
scope = e
break
end
end
end
scope ||= @environments.last || @scopes.last
variable ||= lookup_and_evaluate(scope, key)
variable = variable.to_liquid
variable.context = self if variable.respond_to?(:context=)
return variable
end
# Resolves namespaced queries gracefully.
#
# Example
# @context['hash'] = {"name" => 'tobi'}
# assert_equal 'tobi', @context['hash.name']
# assert_equal 'tobi', @context['hash["name"]']
def variable(markup)
parts = markup.scan(VariableParser)
square_bracketed = /\A\[(.*)\]\z/
first_part = parts.shift
if first_part =~ square_bracketed
first_part = resolve($1)
end
if object = find_variable(first_part)
parts.each do |part|
part = resolve($1) if part_resolved = (part =~ square_bracketed)
# If object is a hash- or array-like object we look for the
# presence of the key and if its available we return it
if object.respond_to?(:[]) and
((object.respond_to?(:has_key?) and object.has_key?(part)) or
(object.respond_to?(:fetch) and part.is_a?(Integer)))
# if its a proc we will replace the entry with the proc
res = lookup_and_evaluate(object, part)
object = res.to_liquid
# Some special cases. If the part wasn't in square brackets and
# no key with the same name was found we interpret following calls
# as commands and call them on the current object
elsif !part_resolved and object.respond_to?(part) and ['size', 'first', 'last'].include?(part)
object = object.send(part.intern).to_liquid
# No key was present with the desired value and it wasn't one of the directly supported
# keywords either. The only thing we got left is to return nil
else
return nil
end
# If we are dealing with a drop here we have to
object.context = self if object.respond_to?(:context=)
end
end
object
end # variable
def lookup_and_evaluate(obj, key)
if (value = obj[key]).is_a?(Proc) && obj.respond_to?(:[]=)
obj[key] = (value.arity == 0) ? value.call : value.call(self)
else
value
end
end # lookup_and_evaluate
def squash_instance_assigns_with_environments
@scopes.last.each_key do |k|
@environments.each do |env|
@@ -281,5 +225,4 @@ module Liquid
end
end # squash_instance_assigns_with_environments
end # Context
end # Liquid

View File

@@ -1,14 +1,13 @@
module Liquid
class Document < Block
# we don't need markup to open this block
def initialize(tokens, options = {})
@options = options
parse_body(tokens)
def self.parse(tokens, options={})
# we don't need markup to open this block
super(nil, nil, tokens, options)
end
# There isn't a real delimiter
def block_delimiter
nil
[]
end
# Document blocks don't need to be terminated since they are not actually opened

View File

@@ -1,5 +1,35 @@
module Liquid
class Error < ::StandardError; end
class Error < ::StandardError
attr_accessor :line_number
def self.render(e)
msg = if e.is_a?(Liquid::Error) && e.line_number
"#{e.line_number}: #{e.message}"
else
e.message
end
case e
when SyntaxError
"Liquid syntax error: #{msg}"
else
"Liquid error: #{msg}"
end
end
def self.error_with_line_number(e, token)
if e.is_a?(Liquid::Error)
e.set_line_number_from_token(token)
end
e
end
def set_line_number_from_token(token)
return unless token.respond_to?(:line_number)
self.line_number = token.line_number
end
end
class ArgumentError < Error; end
class ContextError < Error; end

33
lib/liquid/expression.rb Normal file
View File

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

View File

@@ -44,7 +44,7 @@ module Liquid
class LocalFileSystem
attr_accessor :root
def initialize(root, pattern = "_%s.liquid")
def initialize(root, pattern = "_%s.liquid".freeze)
@root = root
@pattern = pattern
end
@@ -59,7 +59,7 @@ module Liquid
def full_path(template_path)
raise FileSystemError, "Illegal template name '#{template_path}'" unless template_path =~ /\A[^.\/][a-zA-Z0-9_\/]+\z/
full_path = if template_path.include?('/')
full_path = if template_path.include?('/'.freeze)
File.join(root, File.dirname(template_path), @pattern % File.basename(template_path))
else
File.join(root, @pattern % template_path)

View File

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

View File

@@ -6,7 +6,7 @@ module Liquid
class TranslationError < StandardError
end
attr_reader :path
def initialize(path = DEFAULT_LOCALE)
@@ -31,7 +31,7 @@ module Liquid
end
def deep_fetch_translation(name)
name.split('.').reduce(locale) do |level, cur|
name.split('.'.freeze).reduce(locale) do |level, cur|
level[cur] or raise TranslationError, "Translation for #{name} does not exist in locale #{path}"
end
end

View File

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

View File

@@ -2,14 +2,14 @@ require "strscan"
module Liquid
class Lexer
SPECIALS = {
'|' => :pipe,
'.' => :dot,
':' => :colon,
',' => :comma,
'[' => :open_square,
']' => :close_square,
'(' => :open_round,
')' => :close_round
'|'.freeze => :pipe,
'.'.freeze => :dot,
':'.freeze => :colon,
','.freeze => :comma,
'['.freeze => :open_square,
']'.freeze => :close_square,
'('.freeze => :open_round,
')'.freeze => :close_round
}
IDENTIFIER = /[\w\-?!]+/
SINGLE_STRING_LITERAL = /'[^\']*'/

View File

@@ -66,10 +66,11 @@ module Liquid
str = ""
# might be a keyword argument (identifier: expression)
if look(:id) && look(:colon, 1)
str << consume << consume << ' '
str << consume << consume << ' '.freeze
end
str << expression
str
end
def variable_signature

View File

@@ -0,0 +1,31 @@
module Liquid
module ParserSwitching
def parse_with_selected_parser(markup)
case @options[:error_mode] || Template.error_mode
when :strict then strict_parse_with_error_context(markup)
when :lax then lax_parse(markup)
when :warn
begin
return strict_parse_with_error_context(markup)
rescue SyntaxError => e
e.line_number = markup.line_number if markup.is_a?(Token)
@warnings ||= []
@warnings << e
return lax_parse(markup)
end
end
end
private
def strict_parse_with_error_context(markup)
strict_parse(markup)
rescue SyntaxError => e
e.message << markup_context(markup)
raise e
end
def markup_context(markup)
" in \"#{markup.strip}\""
end
end
end

159
lib/liquid/profiler.rb Normal file
View File

@@ -0,0 +1,159 @@
module Liquid
# 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
# <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.
#
# template = Liquid::Template.parse(template_content, profile: true)
# output = template.render
# profile = template.profiler
#
# This object contains all profiling information, containing information on what tags were rendered,
# where in the templates these tags live, and how long each tag took to render.
#
# This is a tree structure that is Enumerable all the way down, and keeps track of tags and rendering times
# inside of <tt>{% include %}</tt> tags.
#
# profile.each do |node|
# # Access to the token itself
# node.code
#
# # Which template and line number of this node.
# # If top level, this will be "<root>".
# node.partial
# node.line_number
#
# # Render time in seconds of this node
# node.render_time
#
# # If the template used {% include %}, this node will also have children.
# node.children.each do |child2|
# # ...
# end
# end
#
# Profiler also exposes the total time of the template's render in <tt>Liquid::Profiler#total_render_time</tt>.
#
# All render times are in seconds. There is a small performance hit when profiling is enabled.
#
class Profiler
include Enumerable
class Timing
attr_reader :code, :partial, :line_number, :children
def initialize(token, partial)
@code = token.respond_to?(:raw) ? token.raw : token
@partial = partial
@line_number = token.respond_to?(:line_number) ? token.line_number : nil
@children = []
end
def self.start(token, partial)
new(token, partial).tap do |t|
t.start
end
end
def start
@start_time = Time.now
end
def finish
@end_time = Time.now
end
def render_time
@end_time - @start_time
end
end
def self.profile_token_render(token)
if Profiler.current_profile && token.respond_to?(:render)
Profiler.current_profile.start_token(token)
output = yield
Profiler.current_profile.end_token(token)
output
else
yield
end
end
def self.profile_children(template_name)
if Profiler.current_profile
Profiler.current_profile.push_partial(template_name)
output = yield
Profiler.current_profile.pop_partial
output
else
yield
end
end
def self.current_profile
Thread.current[:liquid_profiler]
end
def initialize
@partial_stack = ["<root>"]
@root_timing = Timing.new("", current_partial)
@timing_stack = [@root_timing]
@render_start_at = Time.now
@render_end_at = @render_start_at
end
def start
Thread.current[:liquid_profiler] = self
@render_start_at = Time.now
end
def stop
Thread.current[:liquid_profiler] = nil
@render_end_at = Time.now
end
def total_render_time
@render_end_at - @render_start_at
end
def each(&block)
@root_timing.children.each(&block)
end
def [](idx)
@root_timing.children[idx]
end
def length
@root_timing.children.length
end
def start_token(token)
@timing_stack.push(Timing.start(token, current_partial))
end
def end_token(token)
timing = @timing_stack.pop
timing.finish
@timing_stack.last.children << timing
end
def current_partial
@partial_stack.last
end
def push_partial(partial_name)
@partial_stack.push(partial_name)
end
def pop_partial
@partial_stack.pop
end
end
end

View File

@@ -0,0 +1,23 @@
module Liquid
class Block < Tag
def render_token_with_profiling(token, context)
Profiler.profile_token_render(token) do
render_token_without_profiling(token, context)
end
end
alias_method :render_token_without_profiling, :render_token
alias_method :render_token, :render_token_with_profiling
end
class Include < Tag
def render_with_profiling(context)
Profiler.profile_children(@template_name) do
render_without_profiling(context)
end
end
alias_method :render_without_profiling, :render
alias_method :render, :render_with_profiling
end
end

View File

@@ -0,0 +1,22 @@
module Liquid
class RangeLookup
def self.parse(start_markup, end_markup)
start_obj = Expression.parse(start_markup)
end_obj = Expression.parse(end_markup)
if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate)
new(start_obj, end_obj)
else
start_obj.to_i..end_obj.to_i
end
end
def initialize(start_obj, end_obj)
@start_obj = start_obj
@end_obj = end_obj
end
def evaluate(context)
context.evaluate(@start_obj).to_i..context.evaluate(@end_obj).to_i
end
end
end

View File

@@ -4,12 +4,17 @@ require 'bigdecimal'
module Liquid
module StandardFilters
HTML_ESCAPE = { '&' => '&amp;', '>' => '&gt;', '<' => '&lt;', '"' => '&quot;', "'" => '&#39;' }
HTML_ESCAPE = {
'&'.freeze => '&amp;'.freeze,
'>'.freeze => '&gt;'.freeze,
'<'.freeze => '&lt;'.freeze,
'"'.freeze => '&quot;'.freeze,
"'".freeze => '&#39;'.freeze
}
HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/
# Return the size of an array or of an string
def size(input)
input.respond_to?(:size) ? input.size : 0
end
@@ -31,27 +36,37 @@ module Liquid
def escape(input)
CGI.escapeHTML(input) rescue input
end
alias_method :h, :escape
def escape_once(input)
input.to_s.gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE)
end
alias_method :h, :escape
def slice(input, offset, length=nil)
offset = Integer(offset)
length = length ? Integer(length) : 1
if input.is_a?(Array)
input.slice(offset, length) || []
else
input.to_s.slice(offset, length) || ''
end
end
# Truncate a string down to x characters
def truncate(input, length = 50, truncate_string = "...")
def truncate(input, length = 50, truncate_string = "...".freeze)
if input.nil? then return end
l = length.to_i - truncate_string.length
l = 0 if l < 0
input.length > length.to_i ? input[0...l] + truncate_string : input
end
def truncatewords(input, words = 15, truncate_string = "...")
def truncatewords(input, words = 15, truncate_string = "...".freeze)
if input.nil? then return end
wordlist = input.to_s.split
l = words.to_i - 1
l = 0 if l < 0
wordlist.length > l ? wordlist[0..l].join(" ") + truncate_string : input
wordlist.length > l ? wordlist[0..l].join(" ".freeze) + truncate_string : input
end
# Split input string into an array of substrings separated by given pattern.
@@ -60,7 +75,7 @@ module Liquid
# <div class="summary">{{ post | split '//' | first }}</div>
#
def split(input, pattern)
input.split(pattern)
input.to_s.split(pattern)
end
def strip(input)
@@ -76,44 +91,56 @@ module Liquid
end
def strip_html(input)
input.to_s.gsub(/<script.*?<\/script>/m, '').gsub(/<!--.*?-->/m, '').gsub(/<style.*?<\/style>/m, '').gsub(/<.*?>/m, '')
empty = ''.freeze
input.to_s.gsub(/<script.*?<\/script>/m, empty).gsub(/<!--.*?-->/m, empty).gsub(/<style.*?<\/style>/m, empty).gsub(/<.*?>/m, empty)
end
# Remove all newlines from the string
def strip_newlines(input)
input.to_s.gsub(/\r?\n/, '')
input.to_s.gsub(/\r?\n/, ''.freeze)
end
# Join elements of the array with certain character between them
def join(input, glue = ' ')
[input].flatten.join(glue)
def join(input, glue = ' '.freeze)
InputIterator.new(input).join(glue)
end
# Sort elements of the array
# provide optional property with which to sort an array of hashes or drops
def sort(input, property = nil)
ary = flatten_if_necessary(input)
ary = InputIterator.new(input)
if property.nil?
ary.sort
elsif ary.first.respond_to?('[]') and !ary.first[property].nil?
elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
ary.sort {|a,b| a[property] <=> b[property] }
elsif ary.first.respond_to?(property)
ary.sort {|a,b| a.send(property) <=> b.send(property) }
end
end
# Remove duplicate elements from an array
# provide optional property with which to determine uniqueness
def uniq(input, property = nil)
ary = InputIterator.new(input)
if property.nil?
input.uniq
elsif input.first.respond_to?(:[])
input.uniq{ |a| a[property] }
end
end
# Reverse the elements of an array
def reverse(input)
ary = [input].flatten
ary = InputIterator.new(input)
ary.reverse
end
# map/collect on a given property
def map(input, property)
flatten_if_necessary(input).map do |e|
InputIterator.new(input).map do |e|
e = e.call if e.is_a?(Proc)
if property == "to_liquid"
if property == "to_liquid".freeze
e
elsif e.respond_to?(:[])
e[property]
@@ -122,23 +149,23 @@ module Liquid
end
# Replace occurrences of a string with another
def replace(input, string, replacement = '')
def replace(input, string, replacement = ''.freeze)
input.to_s.gsub(string, replacement.to_s)
end
# Replace the first occurrences of a string with another
def replace_first(input, string, replacement = '')
def replace_first(input, string, replacement = ''.freeze)
input.to_s.sub(string, replacement.to_s)
end
# remove a substring
def remove(input, string)
input.to_s.gsub(string, '')
input.to_s.gsub(string, ''.freeze)
end
# remove the first occurrences of a substring
def remove_first(input, string)
input.to_s.sub(string, '')
input.to_s.sub(string, ''.freeze)
end
# add one string to another
@@ -153,10 +180,10 @@ module Liquid
# Add <br /> tags in front of all newlines in input string
def newline_to_br(input)
input.to_s.gsub(/\n/, "<br />\n")
input.to_s.gsub(/\n/, "<br />\n".freeze)
end
# Reformat a date
# Reformat a date using Ruby's core Time#strftime( string ) -> string
#
# %a - The abbreviated weekday name (``Sun'')
# %A - The full weekday name (``Sunday'')
@@ -170,6 +197,7 @@ module Liquid
# %m - Month of the year (01..12)
# %M - Minute of the hour (00..59)
# %p - Meridian indicator (``AM'' or ``PM'')
# %s - Number of seconds since 1970-01-01 00:00:00 UTC.
# %S - Second of the minute (00..60)
# %U - Week number of the current year,
# starting with the first Sunday as the first
@@ -184,34 +212,14 @@ module Liquid
# %Y - Year with century
# %Z - Time zone name
# %% - Literal ``%'' character
#
# See also: http://www.ruby-doc.org/core/Time.html#method-i-strftime
def date(input, format)
return input if format.to_s.empty?
if format.to_s.empty?
return input.to_s
end
return input unless date = to_date(input)
if ((input.is_a?(String) && !/\A\d+\z/.match(input.to_s).nil?) || input.is_a?(Integer)) && input.to_i > 0
input = Time.at(input.to_i)
end
date = if input.is_a?(String)
case input.downcase
when 'now', 'today'
Time.now
else
Time.parse(input)
end
else
input
end
if date.respond_to?(:strftime)
date.strftime(format.to_s)
else
input
end
rescue
input
date.strftime(format.to_s)
end
# Get the first element of the passed in array
@@ -256,24 +264,28 @@ module Liquid
apply_operation(input, operand, :%)
end
def default(input, default_value = "")
def round(input, n = 0)
result = to_number(input).round(to_number(n))
result = result.to_f if result.is_a?(BigDecimal)
result = result.to_i if n == 0
result
end
def ceil(input)
to_number(input).ceil.to_i
end
def floor(input)
to_number(input).floor.to_i
end
def default(input, default_value = "".freeze)
is_blank = input.respond_to?(:empty?) ? input.empty? : !input
is_blank ? default_value : input
end
private
def flatten_if_necessary(input)
ary = if input.is_a?(Array)
input.flatten
elsif input.is_a?(Enumerable) && !input.is_a?(Hash)
input
else
[input].flatten
end
ary.map{ |e| e.respond_to?(:to_liquid) ? e.to_liquid : e }
end
def to_number(obj)
case obj
when Float
@@ -287,10 +299,57 @@ module Liquid
end
end
def to_date(obj)
return obj if obj.respond_to?(:strftime)
case obj
when 'now'.freeze, 'today'.freeze
Time.now
when /\A\d+\z/, Integer
Time.at(obj.to_i)
when String
Time.parse(obj)
else
nil
end
rescue ArgumentError
nil
end
def apply_operation(input, operand, operation)
result = to_number(input).send(operation, to_number(operand))
result.is_a?(BigDecimal) ? result.to_f : result
end
class InputIterator
include Enumerable
def initialize(input)
@input = if input.is_a?(Array)
input.flatten
elsif input.is_a?(Hash)
[input]
elsif input.is_a?(Enumerable)
input
else
Array(input)
end
end
def join(glue)
to_a.join(glue)
end
def reverse
reverse_each.to_a
end
def each
@input.each do |e|
yield(e.respond_to?(:to_liquid) ? e.to_liquid : e)
end
end
end
end
Template.register_filter(StandardFilters)

View File

@@ -1,21 +1,30 @@
module Liquid
class Tag
attr_accessor :options
attr_accessor :options, :line_number
attr_reader :nodelist, :warnings
include ParserSwitching
def self.new_with_options(tag_name, markup, tokens, options)
# Forgive me Matz for I have sinned. I know this code is weird
# but it was necessary to maintain API compatibility.
new_tag = self.allocate
new_tag.options = options
new_tag.send(:initialize, tag_name, markup, tokens)
new_tag
class << self
def parse(tag_name, markup, tokens, options)
tag = new(tag_name, markup, options)
tag.parse(tokens)
tag
end
private :new
end
def initialize(tag_name, markup, tokens)
def initialize(tag_name, markup, options)
@tag_name = tag_name
@markup = markup
@options ||= {} # needs || because might be set before initialize
@options = options
end
def parse(tokens)
end
def raw
"#{@tag_name} #{@markup}"
end
def name
@@ -23,34 +32,11 @@ module Liquid
end
def render(context)
''
''.freeze
end
def blank?
@blank || false
false
end
def parse_with_selected_parser(markup)
case @options[:error_mode] || Template.error_mode
when :strict then strict_parse_with_error_context(markup)
when :lax then lax_parse(markup)
when :warn
begin
return strict_parse_with_error_context(markup)
rescue SyntaxError => e
@warnings ||= []
@warnings << e
return lax_parse(markup)
end
end
end
private
def strict_parse_with_error_context(markup)
strict_parse(markup)
rescue SyntaxError => e
e.message << " in \"#{markup.strip}\""
raise e
end
end # Tag
end # Liquid
end
end

View File

@@ -9,24 +9,24 @@ module Liquid
# {{ foo }}
#
class Assign < Tag
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/o
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
def initialize(tag_name, markup, tokens)
def initialize(tag_name, markup, options)
super
if markup =~ Syntax
@to = $1
@from = Variable.new($2)
@from = Variable.new($2,options)
@from.line_number = line_number
else
raise SyntaxError.new options[:locale].t("errors.syntax.assign")
raise SyntaxError.new options[:locale].t("errors.syntax.assign".freeze)
end
super
end
def render(context)
val = @from.render(context)
context.scopes.last[@to] = val
context.increment_used_resources(:assign_score_current, val)
''
''.freeze
end
def blank?
@@ -34,5 +34,5 @@ module Liquid
end
end
Template.register_tag('assign', Assign)
Template.register_tag('assign'.freeze, Assign)
end

View File

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

View File

@@ -1,5 +1,4 @@
module Liquid
# Capture stores the result of a block into a variable without rendering it inplace.
#
# {% capture heading %}
@@ -14,21 +13,20 @@ module Liquid
class Capture < Block
Syntax = /(\w+)/
def initialize(tag_name, markup, tokens)
def initialize(tag_name, markup, options)
super
if markup =~ Syntax
@to = $1
else
raise SyntaxError.new(options[:locale].t("errors.syntax.capture"))
end
super
end
def render(context)
output = super
context.scopes.last[@to] = output
context.increment_used_resources(:assign_score_current, output)
''
''.freeze
end
def blank?
@@ -36,5 +34,5 @@ module Liquid
end
end
Template.register_tag('capture', Capture)
Template.register_tag('capture'.freeze, Capture)
end

View File

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

View File

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

View File

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

View File

@@ -13,9 +13,10 @@ module Liquid
#
class Cycle < Tag
SimpleSyntax = /\A#{QuotedFragment}+/o
NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/o
NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om
def initialize(tag_name, markup, tokens)
def initialize(tag_name, markup, options)
super
case markup
when NamedSyntax
@variables = variables_from_string($2)
@@ -24,9 +25,8 @@ module Liquid
@variables = variables_from_string(markup)
@name = "'#{@variables.to_s}'"
else
raise SyntaxError.new(options[:locale].t("errors.syntax.cycle"))
raise SyntaxError.new(options[:locale].t("errors.syntax.cycle".freeze))
end
super
end
def render(context)
@@ -43,11 +43,8 @@ module Liquid
end
end
def blank?
false
end
private
def variables_from_string(markup)
markup.split(',').collect do |var|
var =~ /\s*(#{QuotedFragment})\s*/o

View File

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

View File

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

View File

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

View File

@@ -4,17 +4,17 @@ module Liquid
def render(context)
context.stack do
output = super
output = render_all(@nodelist, context)
if output != context.registers[:ifchanged]
context.registers[:ifchanged] = output
output
else
''
''.freeze
end
end
end
end
Template.register_tag('ifchanged', Ifchanged)
Template.register_tag('ifchanged'.freeze, Ifchanged)
end

View File

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

View File

@@ -15,9 +15,9 @@ module Liquid
# Hello: 2
#
class Increment < Tag
def initialize(tag_name, markup, tokens)
@variable = markup.strip
def initialize(tag_name, markup, options)
super
@variable = markup.strip
end
def render(context)
@@ -25,11 +25,7 @@ module Liquid
context.environments.first[@variable] = value + 1
value.to_s
end
def blank?
false
end
end
Template.register_tag('increment', Increment)
Template.register_tag('increment'.freeze, Increment)
end

View File

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

View File

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

View File

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

View File

@@ -21,7 +21,45 @@ module Liquid
attr_accessor :root, :resource_limits
@@file_system = BlankFileSystem.new
class TagRegistry
def initialize
@tags = {}
@cache = {}
end
def [](tag_name)
return nil unless @tags.has_key?(tag_name)
return @cache[tag_name] if Liquid.cache_classes
lookup_class(@tags[tag_name]).tap { |o| @cache[tag_name] = o }
end
def []=(tag_name, klass)
@tags[tag_name] = klass.name
@cache[tag_name] = klass
end
def delete(tag_name)
@tags.delete(tag_name)
@cache.delete(tag_name)
end
private
def lookup_class(name)
name.split("::").reject(&:empty?).reduce(Object) { |scope, const| scope.const_get(const) }
end
end
attr_reader :profiler
class << self
# Sets how strict the parser should be.
# :lax acts like liquid 2.5 and silently ignores malformed tags in most cases.
# :warn is the default and will give deprecation warnings when invalid syntax is used.
# :strict will enforce correct syntax.
attr_writer :error_mode
def file_system
@@file_system
end
@@ -35,15 +73,7 @@ module Liquid
end
def tags
@tags ||= {}
end
# Sets how strict the parser should be.
# :lax acts like liquid 2.5 and silently ignores malformed tags in most cases.
# :warn is the default and will give deprecation warnings when invalid syntax is used.
# :strict will enforce correct syntax.
def error_mode=(mode)
@error_mode = mode
@tags ||= TagRegistry.new
end
def error_mode
@@ -57,10 +87,11 @@ module Liquid
end
# creates a new <tt>Template</tt> object from liquid source code
# To enable profiling, pass in <tt>profile: true</tt> as an option.
# See Liquid::Profiler for more information
def parse(source, options = {})
template = Template.new
template.parse(source, options)
template
end
end
@@ -72,7 +103,9 @@ module Liquid
# Parse source code.
# Returns self for easy chaining
def parse(source, options = {})
@root = Document.new(tokenize(source), DEFAULT_OPTIONS.merge(options))
@profiling = options.delete(:profile)
@line_numbers = options.delete(:line_numbers) || @profiling
@root = Document.parse(tokenize(source), DEFAULT_OPTIONS.merge(options))
@warnings = nil
self
end
@@ -103,6 +136,9 @@ module Liquid
# if you use the same filters over and over again consider registering them globally
# with <tt>Template.register_filter</tt>
#
# if profiling was enabled in <tt>Template#parse</tt> then the resulting profiling information
# will be available via <tt>Template#profiler</tt>
#
# Following options can be passed:
#
# * <tt>filters</tt> : array with local filters
@@ -110,11 +146,17 @@ module Liquid
# filters and tags and might be useful to integrate liquid more with its host application
#
def render(*args)
return '' if @root.nil?
return ''.freeze if @root.nil?
context = case args.first
when Liquid::Context
args.shift
c = args.shift
if @rethrow_errors
c.exception_handler = ->(e) { true }
end
c
when Liquid::Drop
drop = args.shift
drop.context = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits)
@@ -138,6 +180,9 @@ module Liquid
context.add_filters(options[:filters])
end
if options[:exception_handler]
context.exception_handler = options[:exception_handler]
end
when Module
context.add_filters(args.pop)
when Array
@@ -147,25 +192,61 @@ module Liquid
begin
# render the nodelist.
# for performance reasons we get an array back here. join will make a string out of it.
result = @root.render(context)
result = with_profiling do
@root.render(context)
end
result.respond_to?(:join) ? result.join : result
rescue Liquid::MemoryError => e
context.handle_error(e)
context.handle_error(e, nil)
ensure
@errors = context.errors
end
end
def render!(*args)
@rethrow_errors = true; render(*args)
@rethrow_errors = true
render(*args)
end
private
# Uses the <tt>Liquid::TemplateParser</tt> regexp to tokenize the passed source
def tokenize(source)
source = source.source if source.respond_to?(:source)
Tokenizer.new(source.to_s)
return [] if source.to_s.empty?
tokens = calculate_line_numbers(source.split(TemplateParser))
# removes the rogue empty element at the beginning of the array
tokens.shift if tokens[0] and tokens[0].empty?
tokens
end
def calculate_line_numbers(raw_tokens)
return raw_tokens unless @line_numbers
current_line = 1
raw_tokens.map do |token|
Token.new(token, current_line).tap do
current_line += token.count("\n")
end
end
end
def with_profiling
if @profiling
@profiler = Profiler.new
@profiler.start
begin
yield
ensure
@profiler.stop
end
else
yield
end
end
end
end

18
lib/liquid/token.rb Normal file
View File

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

View File

@@ -10,7 +10,7 @@ module Liquid
end
def self.non_blank_string?(collection)
collection.is_a?(String) && collection != ''
collection.is_a?(String) && collection != ''.freeze
end
def self.slice_collection_using_each(collection, from, to)

View File

@@ -11,46 +11,45 @@ module Liquid
# {{ user | link }}
#
class Variable
FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o
EasyParse = /\A *(\w+(?:\.\w+)*) *\z/
attr_accessor :filters, :name, :warnings
attr_accessor :line_number
include ParserSwitching
def initialize(markup, options = {})
@markup = markup
@name = nil
@options = options || {}
case @options[:error_mode] || Template.error_mode
when :strict then strict_parse(markup)
when :lax then lax_parse(markup)
when :warn
begin
strict_parse(markup)
rescue SyntaxError => e
@warnings ||= []
@warnings << e
lax_parse(markup)
parse_with_selected_parser(markup)
end
def raw
@markup
end
def markup_context(markup)
" in \"{{#{markup}}}\""
end
def lax_parse(markup)
@filters = []
if markup =~ /\s*(#{QuotedFragment})(.*)/om
@name = Regexp.last_match(1)
if Regexp.last_match(2) =~ /#{FilterSeparator}\s*(.*)/om
filters = Regexp.last_match(1).scan(FilterParser)
filters.each do |f|
if f =~ /\w+/
filtername = Regexp.last_match(0)
filterargs = f.scan(/(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o).flatten
@filters << [filtername, filterargs]
end
end
end
end
end
# def lax_parse(markup)
# @filters = []
# if match = markup.match(/\s*(#{QuotedFragment})(.*)/o)
# @name = match[1]
# if match[2].match(/#{FilterSeparator}\s*(.*)/o)
# filters = Regexp.last_match(1).scan(FilterParser)
# filters.each do |f|
# if matches = f.match(/\s*(\w+)/)
# filtername = matches[1]
# filterargs = f.scan(/(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o).flatten
# @filters << [filtername, filterargs]
# end
# end
# end
# end
# end
def strict_parse(markup)
# Very simple valid cases
if markup =~ EasyParse
@@ -62,16 +61,13 @@ module Liquid
@filters = []
p = Parser.new(markup)
# Could be just filters with no input
@name = p.look(:pipe) ? '' : p.expression
@name = p.look(:pipe) ? ''.freeze : p.expression
while p.consume?(:pipe)
filtername = p.consume(:id)
filterargs = p.consume?(:colon) ? parse_filterargs(p) : []
@filters << [filtername, filterargs]
end
p.consume(:end_of_string)
rescue SyntaxError => e
e.message << " in \"{{#{markup}}}\""
raise e
end
def parse_filterargs(p)
@@ -85,7 +81,7 @@ module Liquid
end
def render(context)
return '' if @name.nil?
return ''.freeze if @name.nil?
@filters.inject(context[@name]) do |output, filter|
filterargs = []
keyword_args = {}

View File

@@ -0,0 +1,68 @@
module Liquid
class VariableLookup
SQUARE_BRACKETED = /\A\[(.*)\]\z/m
COMMAND_METHODS = ['size'.freeze, 'first'.freeze, 'last'.freeze]
def self.parse(markup)
new(markup)
end
def initialize(markup)
lookups = markup.scan(VariableParser)
name = lookups.shift
if name =~ SQUARE_BRACKETED
name = Expression.parse($1)
end
@name = name
@lookups = lookups
@command_flags = 0
@lookups.each_index do |i|
lookup = lookups[i]
if lookup =~ SQUARE_BRACKETED
lookups[i] = Expression.parse($1)
elsif COMMAND_METHODS.include?(lookup)
@command_flags |= 1 << i
end
end
end
def evaluate(context)
name = context.evaluate(@name)
object = context.find_variable(name)
@lookups.each_index do |i|
key = context.evaluate(@lookups[i])
# If object is a hash- or array-like object we look for the
# presence of the key and if its available we return it
if object.respond_to?(:[]) &&
((object.respond_to?(:has_key?) && object.has_key?(key)) ||
(object.respond_to?(:fetch) && key.is_a?(Integer)))
# if its a proc we will replace the entry with the proc
res = context.lookup_and_evaluate(object, key)
object = res.to_liquid
# Some special cases. If the part wasn't in square brackets and
# no key with the same name was found we interpret following calls
# as commands and call them on the current object
elsif @command_flags & (1 << i) != 0 && object.respond_to?(key)
object = object.send(key).to_liquid
# No key was present with the desired value and it wasn't one of the directly supported
# keywords either. The only thing we got left is to return nil
else
return nil
end
# If we are dealing with a drop here we have to
object.context = context if object.respond_to?(:context=)
end
object
end
end
end

View File

@@ -18,15 +18,12 @@ Gem::Specification.new do |s|
s.required_rubygems_version = ">= 1.3.7"
s.test_files = Dir.glob("{test}/**/*")
s.files = Dir.glob("{lib,ext}/**/*") + %w(MIT-LICENSE README.md)
s.extensions = ['ext/liquid/extconf.rb']
s.files = Dir.glob("{lib}/**/*") + %w(MIT-LICENSE README.md)
s.extra_rdoc_files = ["History.md", "README.md"]
s.require_path = "lib"
s.add_development_dependency 'rake-compiler'
s.add_development_dependency 'stackprof'
s.add_development_dependency 'rake'
s.add_development_dependency 'activesupport'
s.add_development_dependency 'minitest'
end

View File

@@ -1,12 +1,17 @@
require 'rubygems'
require 'benchmark'
require 'benchmark/ips'
require File.dirname(__FILE__) + '/theme_runner'
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new
Benchmark.bmbm do |x|
x.report("parse:") { 100.times { profiler.compile } }
x.report("parse & run:") { 100.times { profiler.run } }
end
Benchmark.ips do |x|
x.time = 60
x.warmup = 5
puts
puts "Running benchmark for #{x.time} seconds (with #{x.warmup} seconds warmup)."
puts
x.report("parse:") { profiler.compile }
x.report("parse & run:") { profiler.run }
end

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
require 'yaml'
module Database
module Database
# Load the standard vision toolkit database and re-arrage it to be simply exportable
# to liquid as assigns. All this is based on Shopify
def self.tables

View File

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

View File

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

View File

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

View File

@@ -1,15 +0,0 @@
require 'stackprof' rescue fail("install stackprof extension/gem")
require File.dirname(__FILE__) + '/theme_runner'
profiler = ThemeRunner.new
profiler.run
results = StackProf.run(mode: :cpu, out: ENV['FILENAME']) do
100.times do
profiler.run
end
end
if results.kind_of?(File)
puts "wrote stackprof dump to #{results.path}"
else
StackProf::Report.new(results).print_text(false, 20)
end

View File

@@ -6,10 +6,6 @@
# Shopify which is likely the biggest user of liquid in the world which something to the tune of several
# million Template#render calls a day.
require 'rubygems'
require 'active_support'
require 'yaml'
require 'digest/md5'
require File.dirname(__FILE__) + '/shopify/liquid'
require File.dirname(__FILE__) + '/shopify/database.rb'
@@ -64,57 +60,19 @@ class ThemeRunner
end
def run_profile
RubyProf.measure_mode = RubyProf::WALL_TIME
# Dup assigns because will make some changes to them
assigns = Database.tables.dup
@tests.each do |liquid, layout, template_name|
# Compute page_tempalte outside of profiler run, uninteresting to profiler
html = nil
page_template = File.basename(template_name, File.extname(template_name))
unless @started
RubyProf.start
RubyProf.pause
@started = true
end
html = nil
RubyProf.resume
html = compile_and_render(liquid, layout, assigns, page_template, template_name)
RubyProf.pause
# return the result and the MD5 of the content, this can be used to detect regressions between liquid version
$stdout.puts "* rendered template %s, content: %s" % [template_name, Digest::MD5.hexdigest(html)]
# Uncomment to dump html files to /tmp so that you can inspect for errors
# File.open("/tmp/#{File.basename(template_name)}.html", "w+") { |fp| fp <<html}
end
RubyProf.stop
end
def compile_and_render(template, layout, assigns, page_template, template_file)
tmpl = Liquid::Template.new
tmpl.assigns['page_title'] = 'Page title'
tmpl.assigns['template'] = page_template
tmpl.registers[:file_system] = ThemeRunner::FileSystem.new(File.dirname(template_file))
content_for_layout = tmpl.parse(template).render(assigns)
content_for_layout = tmpl.parse(template).render!(assigns)
if layout
assigns['content_for_layout'] = content_for_layout
tmpl.parse(layout).render(assigns)
tmpl.parse(layout).render!(assigns)
else
content_for_layout
end
end
end

View File

@@ -1,6 +1,6 @@
require 'test_helper'
class AssignTest < Test::Unit::TestCase
class AssignTest < Minitest::Test
include Liquid
def test_assigned_variable
@@ -24,4 +24,15 @@ class AssignTest < Test::Unit::TestCase
'{% assign foo not values %}.',
'values' => "foo,bar,baz")
end
def test_assign_uses_error_mode
with_error_mode(:strict) do
assert_raises(SyntaxError) do
Template.parse("{% assign foo = ('X' | downcase) %}")
end
end
with_error_mode(:lax) do
assert Template.parse("{% assign foo = ('X' | downcase) %}")
end
end
end # AssignTest

View File

@@ -14,7 +14,7 @@ class BlankTestFileSystem
end
end
class BlankTest < Test::Unit::TestCase
class BlankTest < Minitest::Test
include Liquid
N = 10
@@ -93,9 +93,9 @@ class BlankTest < Test::Unit::TestCase
def test_include_is_blank
Liquid::Template.file_system = BlankTestFileSystem.new
assert_equal "foobar"*(N+1), Template.parse(wrap("{% include 'foobar' %}")).render()
assert_equal " foobar "*(N+1), Template.parse(wrap("{% include ' foobar ' %}")).render()
assert_equal " ", Template.parse(" {% include ' ' %} ").render()
assert_template_result "foobar"*(N+1), wrap("{% include 'foobar' %}")
assert_template_result " foobar "*(N+1), wrap("{% include ' foobar ' %}")
assert_template_result " "*(N+1), wrap(" {% include ' ' %} ")
end
def test_case_is_blank

View File

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

View File

@@ -0,0 +1,34 @@
require 'test_helper'
class ContextTest < Minitest::Test
include Liquid
def test_override_global_filter
global = Module.new do
def notice(output)
"Global #{output}"
end
end
local = Module.new do
def notice(output)
"Local #{output}"
end
end
with_global_filter(global) do
assert_equal 'Global test', Template.parse("{{'test' | notice }}").render!
assert_equal 'Local test', Template.parse("{{'test' | notice }}").render!({}, :filters => [local])
end
end
def test_has_key_will_not_add_an_error_for_missing_keys
Template.error_mode = :strict
context = Context.new
context.has_key?('unknown')
assert_empty context.errors
end
end

View File

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

View File

@@ -0,0 +1,137 @@
require 'test_helper'
class ErrorDrop < Liquid::Drop
def standard_error
raise Liquid::StandardError, 'standard error'
end
def argument_error
raise Liquid::ArgumentError, 'argument error'
end
def syntax_error
raise Liquid::SyntaxError, 'syntax error'
end
def exception
raise Exception, 'exception'
end
end
class ErrorHandlingTest < Minitest::Test
include Liquid
def test_templates_parsed_with_line_numbers_renders_them_in_errors
template = <<-LIQUID
Hello,
{{ errors.standard_error }} will raise a standard error.
Bla bla test.
{{ errors.syntax_error }} will raise a syntax error.
This is an argument error: {{ errors.argument_error }}
Bla.
LIQUID
expected = <<-TEXT
Hello,
Liquid error: 3: standard error will raise a standard error.
Bla bla test.
Liquid syntax error: 7: syntax error will raise a syntax error.
This is an argument error: Liquid error: 9: argument error
Bla.
TEXT
output = Liquid::Template.parse(template, line_numbers: true).render('errors' => ErrorDrop.new)
assert_equal expected, output
end
def test_standard_error
template = Liquid::Template.parse( ' {{ errors.standard_error }} ' )
assert_equal ' Liquid error: standard error ', template.render('errors' => ErrorDrop.new)
assert_equal 1, template.errors.size
assert_equal StandardError, template.errors.first.class
end
def test_syntax
template = Liquid::Template.parse( ' {{ errors.syntax_error }} ' )
assert_equal ' Liquid syntax error: syntax error ', template.render('errors' => ErrorDrop.new)
assert_equal 1, template.errors.size
assert_equal SyntaxError, template.errors.first.class
end
def test_argument
template = Liquid::Template.parse( ' {{ errors.argument_error }} ' )
assert_equal ' Liquid error: argument error ', template.render('errors' => ErrorDrop.new)
assert_equal 1, template.errors.size
assert_equal ArgumentError, template.errors.first.class
end
def test_missing_endtag_parse_time_error
assert_raises(Liquid::SyntaxError) do
Liquid::Template.parse(' {% for a in b %} ... ')
end
end
def test_unrecognized_operator
with_error_mode(:strict) do
assert_raises(SyntaxError) do
Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ')
end
end
end
def test_lax_unrecognized_operator
template = Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', :error_mode => :lax)
assert_equal ' Liquid error: Unknown operator =! ', template.render
assert_equal 1, template.errors.size
assert_equal Liquid::ArgumentError, template.errors.first.class
end
def test_strict_error_messages
err = assert_raises(SyntaxError) do
Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', :error_mode => :strict)
end
assert_equal 'Unexpected character = in "1 =! 2"', err.message
err = assert_raises(SyntaxError) do
Liquid::Template.parse('{{%%%}}', :error_mode => :strict)
end
assert_equal 'Unexpected character % in "{{%%%}}"', err.message
end
def test_warnings
template = Liquid::Template.parse('{% if ~~~ %}{{%%%}}{% else %}{{ hello. }}{% endif %}', :error_mode => :warn)
assert_equal 3, template.warnings.size
assert_equal 'Unexpected character ~ in "~~~"', template.warnings[0].message
assert_equal 'Unexpected character % in "{{%%%}}"', template.warnings[1].message
assert_equal 'Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].message
assert_equal '', template.render
end
def test_warning_line_numbers
template = Liquid::Template.parse("{% if ~~~ %}\n{{%%%}}{% else %}\n{{ hello. }}{% endif %}", :error_mode => :warn, :line_numbers => true)
assert_equal 3, template.warnings.size
assert_equal [1,2,3], template.warnings.map(&:line_number)
end
# Liquid should not catch Exceptions that are not subclasses of StandardError, like Interrupt and NoMemoryError
def test_exceptions_propagate
assert_raises Exception do
template = Liquid::Template.parse('{{ errors.exception }}')
template.render('errors' => ErrorDrop.new)
end
end
end

View File

@@ -22,7 +22,7 @@ module SubstituteFilter
end
end
class FiltersTest < Test::Unit::TestCase
class FiltersTest < Minitest::Test
include Liquid
def setup
@@ -67,12 +67,12 @@ class FiltersTest < Test::Unit::TestCase
@context['value'] = 3
@context['numbers'] = [2,1,4,3]
@context['words'] = ['expected', 'as', 'alphabetic']
@context['arrays'] = [['flattened'], ['are']]
@context['arrays'] = ['flower', 'are']
assert_equal [1,2,3,4], Variable.new("numbers | sort").render(@context)
assert_equal ['alphabetic', 'as', 'expected'], Variable.new("words | sort").render(@context)
assert_equal [3], Variable.new("value | sort").render(@context)
assert_equal ['are', 'flattened'], Variable.new("arrays | sort").render(@context)
assert_equal ['are', 'flower'], Variable.new("arrays | sort").render(@context)
end
def test_strip_html
@@ -107,19 +107,19 @@ class FiltersTest < Test::Unit::TestCase
end
end
class FiltersInTemplate < Test::Unit::TestCase
class FiltersInTemplate < Minitest::Test
include Liquid
def test_local_global
Template.register_filter(MoneyFilter)
assert_equal " 1000$ ", Template.parse("{{1000 | money}}").render(nil, nil)
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render(nil, :filters => CanadianMoneyFilter)
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render(nil, :filters => [CanadianMoneyFilter])
with_global_filter(MoneyFilter) do
assert_equal " 1000$ ", Template.parse("{{1000 | money}}").render!(nil, nil)
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, :filters => CanadianMoneyFilter)
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, :filters => [CanadianMoneyFilter])
end
end
def test_local_filter_with_deprecated_syntax
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render(nil, CanadianMoneyFilter)
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render(nil, [CanadianMoneyFilter])
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, CanadianMoneyFilter)
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, [CanadianMoneyFilter])
end
end # FiltersTest

View File

@@ -0,0 +1,23 @@
require 'test_helper'
module MoneyFilter
def money(input)
sprintf(' %d$ ', input)
end
end
module CanadianMoneyFilter
def money(input)
sprintf(' %d$ CAD ', input)
end
end
class HashOrderingTest < Minitest::Test
include Liquid
def test_global_register_order
with_global_filter(MoneyFilter, CanadianMoneyFilter) do
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render(nil, nil)
end
end
end

View File

@@ -27,7 +27,7 @@ module FunnyFilter
end
class OutputTest < Test::Unit::TestCase
class OutputTest < Minitest::Test
include Liquid
def setup
@@ -41,76 +41,76 @@ class OutputTest < Test::Unit::TestCase
text = %| {{best_cars}} |
expected = %| bmw |
assert_equal expected, Template.parse(text).render(@assigns)
assert_equal expected, Template.parse(text).render!(@assigns)
end
def test_variable_traversing
text = %| {{car.bmw}} {{car.gm}} {{car.bmw}} |
expected = %| good bad good |
assert_equal expected, Template.parse(text).render(@assigns)
assert_equal expected, Template.parse(text).render!(@assigns)
end
def test_variable_piping
text = %( {{ car.gm | make_funny }} )
expected = %| LOL |
assert_equal expected, Template.parse(text).render(@assigns, :filters => [FunnyFilter])
assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter])
end
def test_variable_piping_with_input
text = %( {{ car.gm | cite_funny }} )
expected = %| LOL: bad |
assert_equal expected, Template.parse(text).render(@assigns, :filters => [FunnyFilter])
assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter])
end
def test_variable_piping_with_args
text = %! {{ car.gm | add_smiley : ':-(' }} !
expected = %| bad :-( |
assert_equal expected, Template.parse(text).render(@assigns, :filters => [FunnyFilter])
assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter])
end
def test_variable_piping_with_no_args
text = %! {{ car.gm | add_smiley }} !
expected = %| bad :-) |
assert_equal expected, Template.parse(text).render(@assigns, :filters => [FunnyFilter])
assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter])
end
def test_multiple_variable_piping_with_args
text = %! {{ car.gm | add_smiley : ':-(' | add_smiley : ':-('}} !
expected = %| bad :-( :-( |
assert_equal expected, Template.parse(text).render(@assigns, :filters => [FunnyFilter])
assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter])
end
def test_variable_piping_with_multiple_args
text = %! {{ car.gm | add_tag : 'span', 'bar'}} !
expected = %| <span id="bar">bad</span> |
assert_equal expected, Template.parse(text).render(@assigns, :filters => [FunnyFilter])
assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter])
end
def test_variable_piping_with_variable_args
text = %! {{ car.gm | add_tag : 'span', car.bmw}} !
expected = %| <span id="good">bad</span> |
assert_equal expected, Template.parse(text).render(@assigns, :filters => [FunnyFilter])
assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter])
end
def test_multiple_pipings
text = %( {{ best_cars | cite_funny | paragraph }} )
expected = %| <p>LOL: bmw</p> |
assert_equal expected, Template.parse(text).render(@assigns, :filters => [FunnyFilter])
assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter])
end
def test_link_to
text = %( {{ 'Typo' | link_to: 'http://typo.leetsoft.com' }} )
expected = %| <a href="http://typo.leetsoft.com">Typo</a> |
assert_equal expected, Template.parse(text).render(@assigns, :filters => [FunnyFilter])
assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter])
end
end # OutputTest

View File

@@ -1,41 +1,36 @@
require 'test_helper'
class ParsingQuirksTest < Test::Unit::TestCase
class ParsingQuirksTest < Minitest::Test
include Liquid
def test_error_with_css
text = %| div { font-weight: bold; } |
template = Template.parse(text)
assert_equal text, template.render
assert_equal [String], template.root.nodelist.collect {|i| i.class}
def test_parsing_css
text = " div { font-weight: bold; } "
assert_equal text, Template.parse(text).render!
end
def test_raise_on_single_close_bracet
assert_raise(SyntaxError) do
assert_raises(SyntaxError) do
Template.parse("text {{method} oh nos!")
end
end
def test_raise_on_label_and_no_close_bracets
assert_raise(SyntaxError) do
assert_raises(SyntaxError) do
Template.parse("TEST {{ ")
end
end
def test_raise_on_label_and_no_close_bracets_percent
assert_raise(SyntaxError) do
assert_raises(SyntaxError) do
Template.parse("TEST {% ")
end
end
def test_error_on_empty_filter
assert_nothing_raised do
Template.parse("{{test}}")
Template.parse("{{|test}}")
end
assert Template.parse("{{test}}")
assert Template.parse("{{|test}}")
with_error_mode(:strict) do
assert_raise(SyntaxError) do
assert_raises(SyntaxError) do
Template.parse("{{test |a|b|}}")
end
end
@@ -43,7 +38,7 @@ class ParsingQuirksTest < Test::Unit::TestCase
def test_meaningless_parens_error
with_error_mode(:strict) do
assert_raise(SyntaxError) do
assert_raises(SyntaxError) do
markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false"
Template.parse("{% if #{markup} %} YES {% endif %}")
end
@@ -52,11 +47,11 @@ class ParsingQuirksTest < Test::Unit::TestCase
def test_unexpected_characters_syntax_error
with_error_mode(:strict) do
assert_raise(SyntaxError) do
assert_raises(SyntaxError) do
markup = "true && false"
Template.parse("{% if #{markup} %} YES {% endif %}")
end
assert_raise(SyntaxError) do
assert_raises(SyntaxError) do
markup = "false || true"
Template.parse("{% if #{markup} %} YES {% endif %}")
end
@@ -64,11 +59,9 @@ class ParsingQuirksTest < Test::Unit::TestCase
end
def test_no_error_on_lax_empty_filter
assert_nothing_raised do
Template.parse("{{test |a|b|}}", :error_mode => :lax)
Template.parse("{{test}}", :error_mode => :lax)
Template.parse("{{|test|}}", :error_mode => :lax)
end
assert Template.parse("{{test |a|b|}}", :error_mode => :lax)
assert Template.parse("{{test}}", :error_mode => :lax)
assert Template.parse("{{|test|}}", :error_mode => :lax)
end
def test_meaningless_parens_lax
@@ -87,4 +80,24 @@ class ParsingQuirksTest < Test::Unit::TestCase
assert_template_result('',"{% if #{markup} %} YES {% endif %}")
end
end
def test_raise_on_invalid_tag_delimiter
assert_raises(Liquid::SyntaxError) do
Template.new.parse('{% end %}')
end
end
def test_unanchored_filter_arguments
with_error_mode(:lax) do
assert_template_result('hi',"{{ 'hi there' | split$$$:' ' | first }}")
assert_template_result('x', "{{ 'X' | downcase) }}")
# After the messed up quotes a filter without parameters (reverse) should work
# but one with parameters (remove) shouldn't be detected.
assert_template_result('here', "{{ 'hi there' | split:\"t\"\" | reverse | first}}")
assert_template_result('hi ', "{{ 'hi there' | split:\"t\"\" | remove:\"i\" | first}}")
end
end
end # ParsingQuirksTest

View File

@@ -0,0 +1,142 @@
require 'test_helper'
class RenderProfilingTest < Minitest::Test
include Liquid
class ProfilingFileSystem
def read_template_file(template_path, context)
"Rendering template {% assign template_name = '#{template_path}'%}\n{{ template_name }}"
end
end
def setup
Liquid::Template.file_system = ProfilingFileSystem.new
end
def test_template_allows_flagging_profiling
t = Template.parse("{{ 'a string' | upcase }}")
t.render!
assert_nil t.profiler
end
def test_parse_makes_available_simple_profiling
t = Template.parse("{{ 'a string' | upcase }}", :profile => true)
t.render!
assert_equal 1, t.profiler.length
node = t.profiler[0]
assert_equal " 'a string' | upcase ", node.code
end
def test_render_ignores_raw_strings_when_profiling
t = Template.parse("This is raw string\nstuff\nNewline", :profile => true)
t.render!
assert_equal 0, t.profiler.length
end
def test_profiling_includes_line_numbers_of_liquid_nodes
t = Template.parse("{{ 'a string' | upcase }}\n{% increment test %}", :profile => true)
t.render!
assert_equal 2, t.profiler.length
# {{ 'a string' | upcase }}
assert_equal 1, t.profiler[0].line_number
# {{ increment test }}
assert_equal 2, t.profiler[1].line_number
end
def test_profiling_times_the_rendering_of_tokens
t = Template.parse("{% include 'a_template' %}", :profile => true)
t.render!
node = t.profiler[0]
refute_nil node.render_time
end
def test_profiling_times_the_entire_render
t = Template.parse("{% include 'a_template' %}", :profile => true)
t.render!
assert t.profiler.total_render_time > 0, "Total render time was not calculated"
end
def test_profiling_uses_include_to_mark_children
t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}", :profile => true)
t.render!
include_node = t.profiler[1]
assert_equal 2, include_node.children.length
end
def test_profiling_marks_children_with_the_name_of_included_partial
t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}", :profile => true)
t.render!
include_node = t.profiler[1]
include_node.children.each do |child|
assert_equal "'a_template'", child.partial
end
end
def test_profiling_supports_multiple_templates
t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}\n{% include 'b_template' %}", :profile => true)
t.render!
a_template = t.profiler[1]
a_template.children.each do |child|
assert_equal "'a_template'", child.partial
end
b_template = t.profiler[2]
b_template.children.each do |child|
assert_equal "'b_template'", child.partial
end
end
def test_profiling_supports_rendering_the_same_partial_multiple_times
t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}\n{% include 'a_template' %}", :profile => true)
t.render!
a_template1 = t.profiler[1]
a_template1.children.each do |child|
assert_equal "'a_template'", child.partial
end
a_template2 = t.profiler[2]
a_template2.children.each do |child|
assert_equal "'a_template'", child.partial
end
end
def test_can_iterate_over_each_profiling_entry
t = Template.parse("{{ 'a string' | upcase }}\n{% increment test %}", :profile => true)
t.render!
timing_count = 0
t.profiler.each do |timing|
timing_count += 1
end
assert_equal 2, timing_count
end
def test_profiling_marks_children_of_if_blocks
t = Template.parse("{% if true %} {% increment test %} {{ test }} {% endif %}", :profile => true)
t.render!
assert_equal 1, t.profiler.length
assert_equal 2, t.profiler[0].children.length
end
def test_profiling_marks_children_of_for_blocks
t = Template.parse("{% for item in collection %} {{ item }} {% endfor %}", :profile => true)
t.render!({"collection" => ["one", "two"]})
assert_equal 1, t.profiler.length
# Will profile each invocation of the for block
assert_equal 2, t.profiler[0].children.length
end
end

View File

@@ -6,21 +6,21 @@ module SecurityFilter
end
end
class SecurityTest < Test::Unit::TestCase
class SecurityTest < Minitest::Test
include Liquid
def test_no_instance_eval
text = %( {{ '1+1' | instance_eval }} )
expected = %| 1+1 |
assert_equal expected, Template.parse(text).render(@assigns)
assert_equal expected, Template.parse(text).render!(@assigns)
end
def test_no_existing_instance_eval
text = %( {{ '1+1' | __instance_eval__ }} )
expected = %| 1+1 |
assert_equal expected, Template.parse(text).render(@assigns)
assert_equal expected, Template.parse(text).render!(@assigns)
end
@@ -28,7 +28,7 @@ class SecurityTest < Test::Unit::TestCase
text = %( {{ '1+1' | instance_eval }} )
expected = %| 1+1 |
assert_equal expected, Template.parse(text).render(@assigns)
assert_equal expected, Template.parse(text).render!(@assigns)
end
@@ -36,7 +36,7 @@ class SecurityTest < Test::Unit::TestCase
text = %( {{ '1+1' | add_one | instance_eval }} )
expected = %| 1+1 + 1 |
assert_equal expected, Template.parse(text).render(@assigns, :filters => SecurityFilter)
assert_equal expected, Template.parse(text).render!(@assigns, :filters => SecurityFilter)
end
def test_does_not_add_filters_to_symbol_table
@@ -47,17 +47,17 @@ class SecurityTest < Test::Unit::TestCase
template = Template.parse(test)
assert_equal [], (Symbol.all_symbols - current_symbols)
template.render
template.render!
assert_equal [], (Symbol.all_symbols - current_symbols)
end
def test_does_not_add_drop_methods_to_symbol_table
current_symbols = Symbol.all_symbols
drop = Drop.new
drop.invoke_drop("custom_method_1")
drop.invoke_drop("custom_method_2")
drop.invoke_drop("custom_method_3")
assigns = { 'drop' => Drop.new }
assert_equal "", Template.parse("{{ drop.custom_method_1 }}", assigns).render!
assert_equal "", Template.parse("{{ drop.custom_method_2 }}", assigns).render!
assert_equal "", Template.parse("{{ drop.custom_method_3 }}", assigns).render!
assert_equal [], (Symbol.all_symbols - current_symbols)
end

View File

@@ -7,6 +7,8 @@ class Filters
end
class TestThing
attr_reader :foo
def initialize
@foo = 0
end
@@ -39,7 +41,7 @@ class TestEnumerable < Liquid::Drop
end
end
class StandardFiltersTest < Test::Unit::TestCase
class StandardFiltersTest < Minitest::Test
include Liquid
def setup
@@ -62,6 +64,34 @@ class StandardFiltersTest < Test::Unit::TestCase
assert_equal '', @filters.upcase(nil)
end
def test_slice
assert_equal 'oob', @filters.slice('foobar', 1, 3)
assert_equal 'oobar', @filters.slice('foobar', 1, 1000)
assert_equal '', @filters.slice('foobar', 1, 0)
assert_equal 'o', @filters.slice('foobar', 1, 1)
assert_equal 'bar', @filters.slice('foobar', 3, 3)
assert_equal 'ar', @filters.slice('foobar', -2, 2)
assert_equal 'ar', @filters.slice('foobar', -2, 1000)
assert_equal 'r', @filters.slice('foobar', -1)
assert_equal '', @filters.slice(nil, 0)
assert_equal '', @filters.slice('foobar', 100, 10)
assert_equal '', @filters.slice('foobar', -100, 10)
end
def test_slice_on_arrays
input = 'foobar'.split(//)
assert_equal %w{o o b}, @filters.slice(input, 1, 3)
assert_equal %w{o o b a r}, @filters.slice(input, 1, 1000)
assert_equal %w{}, @filters.slice(input, 1, 0)
assert_equal %w{o}, @filters.slice(input, 1, 1)
assert_equal %w{b a r}, @filters.slice(input, 3, 3)
assert_equal %w{a r}, @filters.slice(input, -2, 2)
assert_equal %w{a r}, @filters.slice(input, -2, 1000)
assert_equal %w{r}, @filters.slice(input, -1)
assert_equal %w{}, @filters.slice(input, 100, 10)
assert_equal %w{}, @filters.slice(input, -100, 10)
end
def test_truncate
assert_equal '1234...', @filters.truncate('1234567890', 7)
assert_equal '1234567890', @filters.truncate('1234567890', 20)
@@ -76,6 +106,7 @@ class StandardFiltersTest < Test::Unit::TestCase
assert_equal ['A?Z'], @filters.split('A?Z', '~')
# Regexp works although Liquid does not support.
assert_equal ['A','Z'], @filters.split('AxZ', /x/)
assert_equal [], @filters.split(nil, ' ')
end
def test_escape
@@ -115,10 +146,32 @@ class StandardFiltersTest < Test::Unit::TestCase
assert_equal [{"a" => 1}, {"a" => 2}, {"a" => 3}, {"a" => 4}], @filters.sort([{"a" => 4}, {"a" => 3}, {"a" => 1}, {"a" => 2}], "a")
end
def test_legacy_sort_hash
assert_equal [{a:1, b:2}], @filters.sort({a:1, b:2})
end
def test_numerical_vs_lexicographical_sort
assert_equal [2, 10], @filters.sort([10, 2])
assert_equal [{"a" => 2}, {"a" => 10}], @filters.sort([{"a" => 10}, {"a" => 2}], "a")
assert_equal ["10", "2"], @filters.sort(["10", "2"])
assert_equal [{"a" => "10"}, {"a" => "2"}], @filters.sort([{"a" => "10"}, {"a" => "2"}], "a")
end
def test_uniq
assert_equal [1,3,2,4], @filters.uniq([1,1,3,2,3,1,4,3,2,1])
assert_equal [{"a" => 1}, {"a" => 3}, {"a" => 2}], @filters.uniq([{"a" => 1}, {"a" => 3}, {"a" => 1}, {"a" => 2}], "a")
testdrop = TestDrop.new
assert_equal [testdrop], @filters.uniq([testdrop, TestDrop.new], 'test')
end
def test_reverse
assert_equal [4,3,2,1], @filters.reverse([1,2,3,4])
end
def test_legacy_reverse_hash
assert_equal [{a:1, b:2}], @filters.reverse(a:1, b:2)
end
def test_map
assert_equal [1,2,3,4], @filters.map([{"a" => 1}, {"a" => 2}, {"a" => 3}, {"a" => 4}], 'a')
assert_template_result 'abc', "{{ ary | map:'foo' | map:'bar' }}",
@@ -126,37 +179,50 @@ class StandardFiltersTest < Test::Unit::TestCase
end
def test_map_doesnt_call_arbitrary_stuff
assert_equal "", Liquid::Template.parse('{{ "foo" | map: "__id__" }}').render
assert_equal "", Liquid::Template.parse('{{ "foo" | map: "inspect" }}').render
assert_template_result "", '{{ "foo" | map: "__id__" }}'
assert_template_result "", '{{ "foo" | map: "inspect" }}'
end
def test_map_calls_to_liquid
t = TestThing.new
assert_equal "woot: 1", Liquid::Template.parse('{{ foo | map: "whatever" }}').render("foo" => [t])
assert_template_result "woot: 1", '{{ foo | map: "whatever" }}', "foo" => [t]
end
def test_map_on_hashes
assert_equal "4217", Liquid::Template.parse('{{ thing | map: "foo" | map: "bar" }}').render("thing" => { "foo" => [ { "bar" => 42 }, { "bar" => 17 } ] })
assert_template_result "4217", '{{ thing | map: "foo" | map: "bar" }}',
"thing" => { "foo" => [ { "bar" => 42 }, { "bar" => 17 } ] }
end
def test_legacy_map_on_hashes_with_dynamic_key
template = "{% assign key = 'foo' %}{{ thing | map: key | map: 'bar' }}"
hash = { "foo" => { "bar" => 42 } }
assert_template_result "42", template, "thing" => hash
end
def test_sort_calls_to_liquid
t = TestThing.new
assert_equal "woot: 1", Liquid::Template.parse('{{ foo | sort: "whatever" }}').render("foo" => [t])
Liquid::Template.parse('{{ foo | sort: "whatever" }}').render("foo" => [t])
assert t.foo > 0
end
def test_map_over_proc
drop = TestDrop.new
p = Proc.new{ drop }
templ = '{{ procs | map: "test" }}'
assert_equal "testfoo", Liquid::Template.parse(templ).render("procs" => [p])
assert_template_result "testfoo", templ, "procs" => [p]
end
def test_map_works_on_enumerables
assert_equal "123", Liquid::Template.parse('{{ foo | map: "foo" }}').render!("foo" => TestEnumerable.new)
assert_template_result "123", '{{ foo | map: "foo" }}', "foo" => TestEnumerable.new
end
def test_sort_works_on_enumerables
assert_equal "213", Liquid::Template.parse('{{ foo | sort: "bar" | map: "foo" }}').render!("foo" => TestEnumerable.new)
assert_template_result "213", '{{ foo | sort: "bar" | map: "foo" }}', "foo" => TestEnumerable.new
end
def test_first_and_last_call_to_liquid
assert_template_result 'foobar', '{{ foo | first }}', 'foo' => [ThingWithToLiquid.new]
assert_template_result 'foobar', '{{ foo | last }}', 'foo' => [ThingWithToLiquid.new]
end
def test_date
@@ -185,7 +251,6 @@ class StandardFiltersTest < Test::Unit::TestCase
assert_equal "07/05/2006", @filters.date("1152098955", "%m/%d/%Y")
end
def test_first_last
assert_equal 1, @filters.first([1,2,3])
assert_equal 3, @filters.last([1,2,3])
@@ -257,7 +322,7 @@ class StandardFiltersTest < Test::Unit::TestCase
assert_template_result "4", "{{ 14 | divided_by:3 }}"
assert_template_result "5", "{{ 15 | divided_by:3 }}"
assert_template_result "Liquid error: divided by 0", "{{ 5 | divided_by:0 }}"
assert_equal "Liquid error: divided by 0", Template.parse("{{ 5 | divided_by:0 }}").render
assert_template_result "0.5", "{{ 2.0 | divided_by:4 }}"
end
@@ -266,6 +331,22 @@ class StandardFiltersTest < Test::Unit::TestCase
assert_template_result "1", "{{ 3 | modulo:2 }}"
end
def test_round
assert_template_result "5", "{{ input | round }}", 'input' => 4.6
assert_template_result "4", "{{ '4.3' | round }}"
assert_template_result "4.56", "{{ input | round: 2 }}", 'input' => 4.5612
end
def test_ceil
assert_template_result "5", "{{ input | ceil }}", 'input' => 4.6
assert_template_result "5", "{{ '4.3' | ceil }}"
end
def test_floor
assert_template_result "4", "{{ input | floor }}", 'input' => 4.6
assert_template_result "4", "{{ '4.3' | floor }}"
end
def test_append
assigns = {'a' => 'bc', 'b' => 'd' }
assert_template_result('bcd',"{{ a | append: 'd'}}",assigns)

View File

@@ -1,6 +1,6 @@
require 'test_helper'
class BreakTagTest < Test::Unit::TestCase
class BreakTagTest < Minitest::Test
include Liquid
# tests that no weird errors are raised if break is called outside of a

View File

@@ -1,6 +1,6 @@
require 'test_helper'
class ContinueTagTest < Test::Unit::TestCase
class ContinueTagTest < Minitest::Test
include Liquid
# tests that no weird errors are raised if continue is called outside of a

View File

@@ -1,6 +1,12 @@
require 'test_helper'
class ForTagTest < Test::Unit::TestCase
class ThingWithValue < Liquid::Drop
def value
3
end
end
class ForTagTest < Minitest::Test
include Liquid
def test_for
@@ -34,6 +40,20 @@ HERE
assert_template_result(' 1 2 3 ','{%for item in (1..3) %} {{item}} {%endfor%}')
end
def test_for_with_variable_range
assert_template_result(' 1 2 3 ','{%for item in (1..foobar) %} {{item}} {%endfor%}', "foobar" => 3)
end
def test_for_with_hash_value_range
foobar = { "value" => 3 }
assert_template_result(' 1 2 3 ','{%for item in (1..foobar.value) %} {{item}} {%endfor%}', "foobar" => foobar)
end
def test_for_with_drop_value_range
foobar = ThingWithValue.new
assert_template_result(' 1 2 3 ','{%for item in (1..foobar.value) %} {{item}} {%endfor%}', "foobar" => foobar)
end
def test_for_with_variable
assert_template_result(' 1 2 3 ','{%for item in array%} {{item}} {%endfor%}','array' => [1,2,3])
assert_template_result('123','{%for item in array%}{{item}}{%endfor%}','array' => [1,2,3])
@@ -283,7 +303,7 @@ HERE
end
def test_bad_variable_naming_in_for_loop
assert_raise(Liquid::SyntaxError) do
assert_raises(Liquid::SyntaxError) do
Liquid::Template.parse('{% for a/b in x %}{% endfor %}')
end
end
@@ -295,16 +315,6 @@ HERE
assert_template_result(expected, template, assigns)
end
def test_for_nodelist
template = Liquid::Template.parse('{% for item in items %}FOR{% endfor %}')
assert_equal ['FOR'], template.root.nodelist[0].nodelist
end
def test_for_else_nodelist
template = Liquid::Template.parse('{% for item in items %}FOR{% else %}ELSE{% endfor %}')
assert_equal ['FOR', 'ELSE'], template.root.nodelist[0].nodelist
end
class LoaderDrop < Liquid::Drop
attr_accessor :each_called, :load_slice_called

View File

@@ -1,6 +1,6 @@
require 'test_helper'
class IfElseTagTest < Test::Unit::TestCase
class IfElseTagTest < Minitest::Test
include Liquid
def test_if
@@ -37,25 +37,19 @@ class IfElseTagTest < Test::Unit::TestCase
end
def test_comparison_of_strings_containing_and_or_or
assert_nothing_raised do
awful_markup = "a == 'and' and b == 'or' and c == 'foo and bar' and d == 'bar or baz' and e == 'foo' and foo and bar"
assigns = {'a' => 'and', 'b' => 'or', 'c' => 'foo and bar', 'd' => 'bar or baz', 'e' => 'foo', 'foo' => true, 'bar' => true}
assert_template_result(' YES ',"{% if #{awful_markup} %} YES {% endif %}", assigns)
end
awful_markup = "a == 'and' and b == 'or' and c == 'foo and bar' and d == 'bar or baz' and e == 'foo' and foo and bar"
assigns = {'a' => 'and', 'b' => 'or', 'c' => 'foo and bar', 'd' => 'bar or baz', 'e' => 'foo', 'foo' => true, 'bar' => true}
assert_template_result(' YES ',"{% if #{awful_markup} %} YES {% endif %}", assigns)
end
def test_comparison_of_expressions_starting_with_and_or_or
assigns = {'order' => {'items_count' => 0}, 'android' => {'name' => 'Roy'}}
assert_nothing_raised do
assert_template_result( "YES",
"{% if android.name == 'Roy' %}YES{% endif %}",
assigns)
end
assert_nothing_raised do
assert_template_result( "YES",
"{% if order.items_count == 0 %}YES{% endif %}",
assigns)
end
assert_template_result( "YES",
"{% if android.name == 'Roy' %}YES{% endif %}",
assigns)
assert_template_result( "YES",
"{% if order.items_count == 0 %}YES{% endif %}",
assigns)
end
def test_if_and
@@ -135,37 +129,36 @@ class IfElseTagTest < Test::Unit::TestCase
end
def test_syntax_error_no_variable
assert_raise(SyntaxError){ assert_template_result('', '{% if jerry == 1 %}')}
assert_raises(SyntaxError){ assert_template_result('', '{% if jerry == 1 %}')}
end
def test_syntax_error_no_expression
assert_raise(SyntaxError) { assert_template_result('', '{% if %}') }
assert_raises(SyntaxError) { assert_template_result('', '{% if %}') }
end
def test_if_with_custom_condition
original_op = Condition.operators['contains']
Condition.operators['contains'] = :[]
assert_template_result('yes', %({% if 'bob' contains 'o' %}yes{% endif %}))
assert_template_result('no', %({% if 'bob' contains 'f' %}yes{% else %}no{% endif %}))
ensure
Condition.operators.delete 'contains'
Condition.operators['contains'] = original_op
end
def test_operators_are_ignored_unless_isolated
original_op = Condition.operators['contains']
Condition.operators['contains'] = :[]
assert_template_result('yes',
%({% if 'gnomeslab-and-or-liquid' contains 'gnomeslab-and-or-liquid' %}yes{% endif %}))
end
def test_if_nodelist
template = Liquid::Template.parse('{% if true %}IF{% else %}ELSE{% endif %}')
assert_equal ['IF', 'ELSE'], template.root.nodelist[0].nodelist
ensure
Condition.operators['contains'] = original_op
end
def test_operators_are_whitelisted
assert_raise(SyntaxError) do
assert_raises(SyntaxError) do
assert_template_result('', %({% if 1 or throw or or 1 %}yes{% endif %}))
end
end
end # IfElseTest
end

View File

@@ -60,16 +60,12 @@ class CustomInclude < Liquid::Tag
def parse(tokens)
end
def blank?
false
end
def render(context)
@template_name[1..-2]
end
end
class IncludeTagTest < Test::Unit::TestCase
class IncludeTagTest < Minitest::Test
include Liquid
def setup
@@ -78,57 +74,52 @@ class IncludeTagTest < Test::Unit::TestCase
def test_include_tag_looks_for_file_system_in_registers_first
assert_equal 'from OtherFileSystem',
Template.parse("{% include 'pick_a_source' %}").render({}, :registers => {:file_system => OtherFileSystem.new})
Template.parse("{% include 'pick_a_source' %}").render!({}, :registers => {:file_system => OtherFileSystem.new})
end
def test_include_tag_with
assert_equal "Product: Draft 151cm ",
Template.parse("{% include 'product' with products[0] %}").render( "products" => [ {'title' => 'Draft 151cm'}, {'title' => 'Element 155cm'} ] )
assert_template_result "Product: Draft 151cm ",
"{% include 'product' with products[0] %}", "products" => [ {'title' => 'Draft 151cm'}, {'title' => 'Element 155cm'} ]
end
def test_include_tag_with_default_name
assert_equal "Product: Draft 151cm ",
Template.parse("{% include 'product' %}").render( "product" => {'title' => 'Draft 151cm'} )
assert_template_result "Product: Draft 151cm ",
"{% include 'product' %}", "product" => {'title' => 'Draft 151cm'}
end
def test_include_tag_for
assert_equal "Product: Draft 151cm Product: Element 155cm ",
Template.parse("{% include 'product' for products %}").render( "products" => [ {'title' => 'Draft 151cm'}, {'title' => 'Element 155cm'} ] )
assert_template_result "Product: Draft 151cm Product: Element 155cm ",
"{% include 'product' for products %}", "products" => [ {'title' => 'Draft 151cm'}, {'title' => 'Element 155cm'} ]
end
def test_include_tag_with_local_variables
assert_equal "Locale: test123 ",
Template.parse("{% include 'locale_variables' echo1: 'test123' %}").render
assert_template_result "Locale: test123 ", "{% include 'locale_variables' echo1: 'test123' %}"
end
def test_include_tag_with_multiple_local_variables
assert_equal "Locale: test123 test321",
Template.parse("{% include 'locale_variables' echo1: 'test123', echo2: 'test321' %}").render
assert_template_result "Locale: test123 test321",
"{% include 'locale_variables' echo1: 'test123', echo2: 'test321' %}"
end
def test_include_tag_with_multiple_local_variables_from_context
assert_equal "Locale: test123 test321",
Template.parse("{% include 'locale_variables' echo1: echo1, echo2: more_echos.echo2 %}").render('echo1' => 'test123', 'more_echos' => { "echo2" => 'test321'})
assert_template_result "Locale: test123 test321",
"{% include 'locale_variables' echo1: echo1, echo2: more_echos.echo2 %}",
'echo1' => 'test123', 'more_echos' => { "echo2" => 'test321'}
end
def test_nested_include_tag
assert_equal "body body_detail",
Template.parse("{% include 'body' %}").render
assert_template_result "body body_detail", "{% include 'body' %}"
assert_equal "header body body_detail footer",
Template.parse("{% include 'nested_template' %}").render
assert_template_result "header body body_detail footer", "{% include 'nested_template' %}"
end
def test_nested_include_with_variable
assert_template_result "Product: Draft 151cm details ",
"{% include 'nested_product_template' with product %}", "product" => {"title" => 'Draft 151cm'}
assert_equal "Product: Draft 151cm details ",
Template.parse("{% include 'nested_product_template' with product %}").render("product" => {"title" => 'Draft 151cm'})
assert_equal "Product: Draft 151cm details Product: Element 155cm details ",
Template.parse("{% include 'nested_product_template' for products %}").render("products" => [{"title" => 'Draft 151cm'}, {"title" => 'Element 155cm'}])
assert_template_result "Product: Draft 151cm details Product: Element 155cm details ",
"{% include 'nested_product_template' for products %}", "products" => [{"title" => 'Draft 151cm'}, {"title" => 'Element 155cm'}]
end
def test_recursively_included_template_does_not_produce_endless_loop
@@ -141,7 +132,7 @@ class IncludeTagTest < Test::Unit::TestCase
Liquid::Template.file_system = infinite_file_system.new
assert_raise(Liquid::StackLevelError) do
assert_raises(Liquid::StackLevelError) do
Template.parse("{% include 'loop' %}").render!
end
@@ -160,34 +151,33 @@ class IncludeTagTest < Test::Unit::TestCase
end
def test_dynamically_choosen_template
assert_template_result "Test123", "{% include template %}", "template" => 'Test123'
assert_template_result "Test321", "{% include template %}", "template" => 'Test321'
assert_equal "Test123", Template.parse("{% include template %}").render("template" => 'Test123')
assert_equal "Test321", Template.parse("{% include template %}").render("template" => 'Test321')
assert_equal "Product: Draft 151cm ", Template.parse("{% include template for product %}").render("template" => 'product', 'product' => { 'title' => 'Draft 151cm'})
assert_template_result "Product: Draft 151cm ", "{% include template for product %}",
"template" => 'product', 'product' => { 'title' => 'Draft 151cm'}
end
def test_include_tag_caches_second_read_of_same_partial
file_system = CountingFileSystem.new
assert_equal 'from CountingFileSystemfrom CountingFileSystem',
Template.parse("{% include 'pick_a_source' %}{% include 'pick_a_source' %}").render({}, :registers => {:file_system => file_system})
Template.parse("{% include 'pick_a_source' %}{% include 'pick_a_source' %}").render!({}, :registers => {:file_system => file_system})
assert_equal 1, file_system.count
end
def test_include_tag_doesnt_cache_partials_across_renders
file_system = CountingFileSystem.new
assert_equal 'from CountingFileSystem',
Template.parse("{% include 'pick_a_source' %}").render({}, :registers => {:file_system => file_system})
Template.parse("{% include 'pick_a_source' %}").render!({}, :registers => {:file_system => file_system})
assert_equal 1, file_system.count
assert_equal 'from CountingFileSystem',
Template.parse("{% include 'pick_a_source' %}").render({}, :registers => {:file_system => file_system})
Template.parse("{% include 'pick_a_source' %}").render!({}, :registers => {:file_system => file_system})
assert_equal 2, file_system.count
end
def test_include_tag_within_if_statement
assert_equal "foo_if_true",
Template.parse("{% if true %}{% include 'foo_if_true' %}{% endif %}").render
assert_template_result "foo_if_true", "{% if true %}{% include 'foo_if_true' %}{% endif %}"
end
def test_custom_include_tag
@@ -195,7 +185,7 @@ class IncludeTagTest < Test::Unit::TestCase
Liquid::Template.tags['include'] = CustomInclude
begin
assert_equal "custom_foo",
Template.parse("{% include 'custom_foo' %}").render
Template.parse("{% include 'custom_foo' %}").render!
ensure
Liquid::Template.tags['include'] = original_tag
end
@@ -206,9 +196,17 @@ class IncludeTagTest < Test::Unit::TestCase
Liquid::Template.tags['include'] = CustomInclude
begin
assert_equal "custom_foo_if_true",
Template.parse("{% if true %}{% include 'custom_foo_if_true' %}{% endif %}").render
Template.parse("{% if true %}{% include 'custom_foo_if_true' %}{% endif %}").render!
ensure
Liquid::Template.tags['include'] = original_tag
end
end
def test_does_not_add_error_in_strict_mode_for_missing_variable
Liquid::Template.file_system = TestFileSystem.new
a = Liquid::Template.parse(' {% include "nested_template" %}')
a.render!
assert_empty a.errors
end
end # IncludeTagTest

View File

@@ -1,6 +1,6 @@
require 'test_helper'
class IncrementTagTest < Test::Unit::TestCase
class IncrementTagTest < Minitest::Test
include Liquid
def test_inc

View File

@@ -1,6 +1,6 @@
require 'test_helper'
class RawTagTest < Test::Unit::TestCase
class RawTagTest < Minitest::Test
include Liquid
def test_tag_in_raw
@@ -20,5 +20,6 @@ class RawTagTest < Test::Unit::TestCase
assert_template_result ' Foobar {% invalid {% {% endraw ', '{% raw %} Foobar {% invalid {% {% endraw {% endraw %}'
assert_template_result ' Foobar {% {% {% ', '{% raw %} Foobar {% {% {% {% endraw %}'
assert_template_result ' test {% raw %} {% endraw %}', '{% raw %} test {% raw %} {% {% endraw %}endraw %}'
assert_template_result ' Foobar {{ invalid 1', '{% raw %} Foobar {{ invalid {% endraw %}{{ 1 }}'
end
end

View File

@@ -1,14 +1,8 @@
require 'test_helper'
class StandardTagTest < Test::Unit::TestCase
class StandardTagTest < Minitest::Test
include Liquid
def test_tag
tag = Tag.new('tag', [], [])
assert_equal 'liquid::tag', tag.name
assert_equal '', tag.render(Context.new)
end
def test_no_transform
assert_template_result('this text should come out of the template without change...',
'this text should come out of the template without change...')
@@ -72,7 +66,7 @@ class StandardTagTest < Test::Unit::TestCase
end
def test_capture_detects_bad_syntax
assert_raise(SyntaxError) do
assert_raises(SyntaxError) do
assert_template_result('content foo content foo ',
'{{ var2 }}{% capture %}{{ var }} foo {% endcapture %}{{ var2 }}{{ var2 }}',
{'var' => 'content' })
@@ -180,11 +174,11 @@ class StandardTagTest < Test::Unit::TestCase
# Example from the shopify forums
code = %q({% case collection.handle %}{% when 'menswear-jackets' %}{% assign ptitle = 'menswear' %}{% when 'menswear-t-shirts' %}{% assign ptitle = 'menswear' %}{% else %}{% assign ptitle = 'womenswear' %}{% endcase %}{{ ptitle }})
template = Liquid::Template.parse(code)
assert_equal "menswear", template.render("collection" => {'handle' => 'menswear-jackets'})
assert_equal "menswear", template.render("collection" => {'handle' => 'menswear-t-shirts'})
assert_equal "womenswear", template.render("collection" => {'handle' => 'x'})
assert_equal "womenswear", template.render("collection" => {'handle' => 'y'})
assert_equal "womenswear", template.render("collection" => {'handle' => 'z'})
assert_equal "menswear", template.render!("collection" => {'handle' => 'menswear-jackets'})
assert_equal "menswear", template.render!("collection" => {'handle' => 'menswear-t-shirts'})
assert_equal "womenswear", template.render!("collection" => {'handle' => 'x'})
assert_equal "womenswear", template.render!("collection" => {'handle' => 'y'})
assert_equal "womenswear", template.render!("collection" => {'handle' => 'z'})
end
def test_case_when_or
@@ -218,7 +212,7 @@ class StandardTagTest < Test::Unit::TestCase
end
def test_assign
assert_equal 'variable', Liquid::Template.parse( '{% assign a = "variable"%}{{a}}').render
assert_template_result 'variable', '{% assign a = "variable"%}{{a}}'
end
def test_assign_unassigned
@@ -227,20 +221,19 @@ class StandardTagTest < Test::Unit::TestCase
end
def test_assign_an_empty_string
assert_equal '', Liquid::Template.parse( '{% assign a = ""%}{{a}}' ).render
assert_template_result '', '{% assign a = ""%}{{a}}'
end
def test_assign_is_global
assert_equal 'variable',
Liquid::Template.parse( '{%for i in (1..2) %}{% assign a = "variable"%}{% endfor %}{{a}}' ).render
assert_template_result 'variable', '{%for i in (1..2) %}{% assign a = "variable"%}{% endfor %}{{a}}'
end
def test_case_detects_bad_syntax
assert_raise(SyntaxError) do
assert_raises(SyntaxError) do
assert_template_result('', '{% case false %}{% when %}true{% endcase %}', {})
end
assert_raise(SyntaxError) do
assert_raises(SyntaxError) do
assert_template_result('', '{% case false %}{% huh %}true{% endcase %}', {})
end
@@ -297,4 +290,8 @@ class StandardTagTest < Test::Unit::TestCase
assigns = {'array' => [ 1, 1, 1, 1] }
assert_template_result('1','{%for item in array%}{%ifchanged%}{{item}}{% endifchanged %}{%endfor%}',assigns)
end
def test_multiline_tag
assert_template_result '0 1 2 3', "0{%\nfor i in (1..3)\n%} {{\ni\n}}{%\nendfor\n%}"
end
end # StandardTagTest

View File

@@ -0,0 +1,113 @@
require 'test_helper'
class StatementsTest < Minitest::Test
include Liquid
def test_true_eql_true
text = ' {% if true == true %} true {% else %} false {% endif %} '
assert_template_result ' true ', text
end
def test_true_not_eql_true
text = ' {% if true != true %} true {% else %} false {% endif %} '
assert_template_result ' false ', text
end
def test_true_lq_true
text = ' {% if 0 > 0 %} true {% else %} false {% endif %} '
assert_template_result ' false ', text
end
def test_one_lq_zero
text = ' {% if 1 > 0 %} true {% else %} false {% endif %} '
assert_template_result ' true ', text
end
def test_zero_lq_one
text = ' {% if 0 < 1 %} true {% else %} false {% endif %} '
assert_template_result ' true ', text
end
def test_zero_lq_or_equal_one
text = ' {% if 0 <= 0 %} true {% else %} false {% endif %} '
assert_template_result ' true ', text
end
def test_zero_lq_or_equal_one_involving_nil
text = ' {% if null <= 0 %} true {% else %} false {% endif %} '
assert_template_result ' false ', text
text = ' {% if 0 <= null %} true {% else %} false {% endif %} '
assert_template_result ' false ', text
end
def test_zero_lqq_or_equal_one
text = ' {% if 0 >= 0 %} true {% else %} false {% endif %} '
assert_template_result ' true ', text
end
def test_strings
text = " {% if 'test' == 'test' %} true {% else %} false {% endif %} "
assert_template_result ' true ', text
end
def test_strings_not_equal
text = " {% if 'test' != 'test' %} true {% else %} false {% endif %} "
assert_template_result ' false ', text
end
def test_var_strings_equal
text = ' {% if var == "hello there!" %} true {% else %} false {% endif %} '
assert_template_result ' true ', text, 'var' => 'hello there!'
end
def test_var_strings_are_not_equal
text = ' {% if "hello there!" == var %} true {% else %} false {% endif %} '
assert_template_result ' true ', text, 'var' => 'hello there!'
end
def test_var_and_long_string_are_equal
text = " {% if var == 'hello there!' %} true {% else %} false {% endif %} "
assert_template_result ' true ', text, 'var' => 'hello there!'
end
def test_var_and_long_string_are_equal_backwards
text = " {% if 'hello there!' == var %} true {% else %} false {% endif %} "
assert_template_result ' true ', text, 'var' => 'hello there!'
end
#def test_is_nil
# text = %| {% if var != nil %} true {% else %} false {% end %} |
# @template.assigns = { 'var' => 'hello there!'}
# expected = %| true |
# assert_equal expected, @template.parse(text)
#end
def test_is_collection_empty
text = ' {% if array == empty %} true {% else %} false {% endif %} '
assert_template_result ' true ', text, 'array' => []
end
def test_is_not_collection_empty
text = ' {% if array == empty %} true {% else %} false {% endif %} '
assert_template_result ' false ', text, 'array' => [1,2,3]
end
def test_nil
text = ' {% if var == nil %} true {% else %} false {% endif %} '
assert_template_result ' true ', text, 'var' => nil
text = ' {% if var == null %} true {% else %} false {% endif %} '
assert_template_result ' true ', text, 'var' => nil
end
def test_not_nil
text = ' {% if var != nil %} true {% else %} false {% endif %} '
assert_template_result ' true ', text, 'var' => 1
text = ' {% if var != null %} true {% else %} false {% endif %} '
assert_template_result ' true ', text, 'var' => 1
end
end # StatementsTest

View File

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

View File

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

View File

@@ -22,62 +22,68 @@ class SomethingWithLength
liquid_methods :length
end
class TemplateTest < Test::Unit::TestCase
class ErroneousDrop < Liquid::Drop
def bad_method
raise 'ruby error in drop'
end
end
class TemplateTest < Minitest::Test
include Liquid
def test_instance_assigns_persist_on_same_template_object_between_parses
t = Template.new
assert_equal 'from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render
assert_equal 'from instance assigns', t.parse("{{ foo }}").render
assert_equal 'from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render!
assert_equal 'from instance assigns', t.parse("{{ foo }}").render!
end
def test_instance_assigns_persist_on_same_template_parsing_between_renders
t = Template.new.parse("{{ foo }}{% assign foo = 'foo' %}{{ foo }}")
assert_equal 'foo', t.render
assert_equal 'foofoo', t.render
assert_equal 'foo', t.render!
assert_equal 'foofoo', t.render!
end
def test_custom_assigns_do_not_persist_on_same_template
t = Template.new
assert_equal 'from custom assigns', t.parse("{{ foo }}").render('foo' => 'from custom assigns')
assert_equal '', t.parse("{{ foo }}").render
assert_equal 'from custom assigns', t.parse("{{ foo }}").render!('foo' => 'from custom assigns')
assert_equal '', t.parse("{{ foo }}").render!
end
def test_custom_assigns_squash_instance_assigns
t = Template.new
assert_equal 'from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render
assert_equal 'from custom assigns', t.parse("{{ foo }}").render('foo' => 'from custom assigns')
assert_equal 'from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render!
assert_equal 'from custom assigns', t.parse("{{ foo }}").render!('foo' => 'from custom assigns')
end
def test_persistent_assigns_squash_instance_assigns
t = Template.new
assert_equal 'from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render
assert_equal 'from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render!
t.assigns['foo'] = 'from persistent assigns'
assert_equal 'from persistent assigns', t.parse("{{ foo }}").render
assert_equal 'from persistent assigns', t.parse("{{ foo }}").render!
end
def test_lambda_is_called_once_from_persistent_assigns_over_multiple_parses_and_renders
t = Template.new
t.assigns['number'] = lambda { @global ||= 0; @global += 1 }
assert_equal '1', t.parse("{{number}}").render
assert_equal '1', t.parse("{{number}}").render
assert_equal '1', t.render
assert_equal '1', t.parse("{{number}}").render!
assert_equal '1', t.parse("{{number}}").render!
assert_equal '1', t.render!
@global = nil
end
def test_lambda_is_called_once_from_custom_assigns_over_multiple_parses_and_renders
t = Template.new
assigns = {'number' => lambda { @global ||= 0; @global += 1 }}
assert_equal '1', t.parse("{{number}}").render(assigns)
assert_equal '1', t.parse("{{number}}").render(assigns)
assert_equal '1', t.render(assigns)
assert_equal '1', t.parse("{{number}}").render!(assigns)
assert_equal '1', t.parse("{{number}}").render!(assigns)
assert_equal '1', t.render!(assigns)
@global = nil
end
def test_resource_limits_works_with_custom_length_method
t = Template.parse("{% assign foo = bar %}")
t.resource_limits = { :render_length_limit => 42 }
assert_equal "", t.render("bar" => SomethingWithLength.new)
assert_equal "", t.render!("bar" => SomethingWithLength.new)
end
def test_resource_limits_render_length
@@ -86,8 +92,8 @@ class TemplateTest < Test::Unit::TestCase
assert_equal "Liquid error: Memory limits exceeded", t.render()
assert t.resource_limits[:reached]
t.resource_limits = { :render_length_limit => 10 }
assert_equal "0123456789", t.render()
assert_not_nil t.resource_limits[:render_length_current]
assert_equal "0123456789", t.render!()
refute_nil t.resource_limits[:render_length_current]
end
def test_resource_limits_render_score
@@ -100,8 +106,8 @@ class TemplateTest < Test::Unit::TestCase
assert_equal "Liquid error: Memory limits exceeded", t.render()
assert t.resource_limits[:reached]
t.resource_limits = { :render_score_limit => 200 }
assert_equal (" foo " * 100), t.render()
assert_not_nil t.resource_limits[:render_score_current]
assert_equal (" foo " * 100), t.render!()
refute_nil t.resource_limits[:render_score_current]
end
def test_resource_limits_assign_score
@@ -110,8 +116,8 @@ class TemplateTest < Test::Unit::TestCase
assert_equal "Liquid error: Memory limits exceeded", t.render()
assert t.resource_limits[:reached]
t.resource_limits = { :assign_score_limit => 2 }
assert_equal "", t.render()
assert_not_nil t.resource_limits[:assign_score_current]
assert_equal "", t.render!()
refute_nil t.resource_limits[:assign_score_current]
end
def test_resource_limits_aborts_rendering_after_first_error
@@ -123,7 +129,7 @@ class TemplateTest < Test::Unit::TestCase
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.render()
t.render!()
assert t.resource_limits[:assign_score_current] > 0
assert t.resource_limits[:render_score_current] > 0
assert t.resource_limits[:render_length_current] > 0
@@ -133,22 +139,32 @@ class TemplateTest < Test::Unit::TestCase
t = Template.new
t.registers['lulz'] = 'haha'
drop = TemplateContextDrop.new
assert_equal 'fizzbuzz', t.parse('{{foo}}').render(drop)
assert_equal 'bar', t.parse('{{bar}}').render(drop)
assert_equal 'haha', t.parse("{{baz}}").render(drop)
assert_equal 'fizzbuzz', t.parse('{{foo}}').render!(drop)
assert_equal 'bar', t.parse('{{bar}}').render!(drop)
assert_equal 'haha', t.parse("{{baz}}").render!(drop)
end
def test_sets_default_localization_in_document
t = Template.new
t.parse('')
assert_instance_of I18n, t.root.options[:locale]
def test_render_bang_force_rethrow_errors_on_passed_context
context = Context.new({'drop' => ErroneousDrop.new})
t = Template.new.parse('{{ drop.bad_method }}')
e = assert_raises RuntimeError do
t.render!(context)
end
assert_equal 'ruby error in drop', e.message
end
def test_sets_default_localization_in_context_with_quick_initialization
t = Template.new
t.parse('{{foo}}', :locale => I18n.new(fixture("en_locale.yml")))
def test_exception_handler_doesnt_reraise_if_it_returns_false
exception = nil
Template.parse("{{ 1 | divided_by: 0 }}").render({}, exception_handler: ->(e) { exception = e; false })
assert exception.is_a?(ZeroDivisionError)
end
assert_instance_of I18n, t.root.options[:locale]
assert_equal fixture("en_locale.yml"), t.root.options[:locale].path
def test_exception_handler_does_reraise_if_it_returns_true
exception = nil
assert_raises(ZeroDivisionError) do
Template.parse("{{ 1 | divided_by: 0 }}").render({}, exception_handler: ->(e) { exception = e; true })
end
assert exception.is_a?(ZeroDivisionError)
end
end

View File

@@ -0,0 +1,76 @@
require 'test_helper'
class VariableTest < Minitest::Test
include Liquid
def test_simple_variable
template = Template.parse(%|{{test}}|)
assert_equal 'worked', template.render!('test' => 'worked')
assert_equal 'worked wonderfully', template.render!('test' => 'worked wonderfully')
end
def test_variable_render_calls_to_liquid
assert_template_result 'foobar', '{{ foo }}', 'foo' => ThingWithToLiquid.new
end
def test_simple_with_whitespaces
template = Template.parse(%| {{ test }} |)
assert_equal ' worked ', template.render!('test' => 'worked')
assert_equal ' worked wonderfully ', template.render!('test' => 'worked wonderfully')
end
def test_ignore_unknown
template = Template.parse(%|{{ test }}|)
assert_equal '', template.render!
end
def test_hash_scoping
template = Template.parse(%|{{ test.test }}|)
assert_equal 'worked', template.render!('test' => {'test' => 'worked'})
end
def test_false_renders_as_false
assert_equal 'false', Template.parse("{{ foo }}").render!('foo' => false)
end
def test_preset_assigns
template = Template.parse(%|{{ test }}|)
template.assigns['test'] = 'worked'
assert_equal 'worked', template.render!
end
def test_reuse_parsed_template
template = Template.parse(%|{{ greeting }} {{ name }}|)
template.assigns['greeting'] = 'Goodbye'
assert_equal 'Hello Tobi', template.render!('greeting' => 'Hello', 'name' => 'Tobi')
assert_equal 'Hello ', template.render!('greeting' => 'Hello', 'unknown' => 'Tobi')
assert_equal 'Hello Brian', template.render!('greeting' => 'Hello', 'name' => 'Brian')
assert_equal 'Goodbye Brian', template.render!('name' => 'Brian')
assert_equal({'greeting'=>'Goodbye'}, template.assigns)
end
def test_assigns_not_polluted_from_template
template = Template.parse(%|{{ test }}{% assign test = 'bar' %}{{ test }}|)
template.assigns['test'] = 'baz'
assert_equal 'bazbar', template.render!
assert_equal 'bazbar', template.render!
assert_equal 'foobar', template.render!('test' => 'foo')
assert_equal 'bazbar', template.render!
end
def test_hash_with_default_proc
template = Template.parse(%|Hello {{ test }}|)
assigns = Hash.new { |h,k| raise "Unknown variable '#{k}'" }
assigns['test'] = 'Tobi'
assert_equal 'Hello Tobi', template.render!(assigns)
assigns.delete('test')
e = assert_raises(RuntimeError) {
template.render!(assigns)
}
assert_equal "Unknown variable 'test'", e.message
end
def test_multiline_variable
assert_equal 'worked', Template.parse("{{\ntest\n}}").render!('test' => 'worked')
end
end

View File

@@ -1,110 +0,0 @@
require 'test_helper'
class ErrorDrop < Liquid::Drop
def standard_error
raise Liquid::StandardError, 'standard error'
end
def argument_error
raise Liquid::ArgumentError, 'argument error'
end
def syntax_error
raise Liquid::SyntaxError, 'syntax error'
end
def exception
raise Exception, 'exception'
end
end
class ErrorHandlingTest < Test::Unit::TestCase
include Liquid
def test_standard_error
assert_nothing_raised do
template = Liquid::Template.parse( ' {{ errors.standard_error }} ' )
assert_equal ' Liquid error: standard error ', template.render('errors' => ErrorDrop.new)
assert_equal 1, template.errors.size
assert_equal StandardError, template.errors.first.class
end
end
def test_syntax
assert_nothing_raised do
template = Liquid::Template.parse( ' {{ errors.syntax_error }} ' )
assert_equal ' Liquid syntax error: syntax error ', template.render('errors' => ErrorDrop.new)
assert_equal 1, template.errors.size
assert_equal SyntaxError, template.errors.first.class
end
end
def test_argument
assert_nothing_raised do
template = Liquid::Template.parse( ' {{ errors.argument_error }} ' )
assert_equal ' Liquid error: argument error ', template.render('errors' => ErrorDrop.new)
assert_equal 1, template.errors.size
assert_equal ArgumentError, template.errors.first.class
end
end
def test_missing_endtag_parse_time_error
assert_raise(Liquid::SyntaxError) do
Liquid::Template.parse(' {% for a in b %} ... ')
end
end
def test_unrecognized_operator
with_error_mode(:strict) do
assert_raise(SyntaxError) do
Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ')
end
end
end
def test_lax_unrecognized_operator
assert_nothing_raised do
template = Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', :error_mode => :lax)
assert_equal ' Liquid error: Unknown operator =! ', template.render
assert_equal 1, template.errors.size
assert_equal Liquid::ArgumentError, template.errors.first.class
end
end
def test_strict_error_messages
err = assert_raise(SyntaxError) do
Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', :error_mode => :strict)
end
assert_equal 'Unexpected character = in "1 =! 2"', err.message
err = assert_raise(SyntaxError) do
Liquid::Template.parse('{{%%%}}', :error_mode => :strict)
end
assert_equal 'Unexpected character % in "{{%%%}}"', err.message
end
def test_warnings
template = Liquid::Template.parse('{% if ~~~ %}{{%%%}}{% else %}{{ hello. }}{% endif %}', :error_mode => :warn)
assert_equal 3, template.warnings.size
assert_equal 'Unexpected character ~ in "~~~"', template.warnings[0].message
assert_equal 'Unexpected character % in "{{%%%}}"', template.warnings[1].message
assert_equal 'Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].message
assert_equal '', template.render
end
# Liquid should not catch Exceptions that are not subclasses of StandardError, like Interrupt and NoMemoryError
def test_exceptions_propagate
assert_raise Exception do
template = Liquid::Template.parse( ' {{ errors.exception }} ' )
template.render('errors' => ErrorDrop.new)
end
end
end # ErrorHandlingTest

View File

@@ -1,25 +0,0 @@
require 'test_helper'
module MoneyFilter
def money(input)
sprintf(' %d$ ', input)
end
end
module CanadianMoneyFilter
def money(input)
sprintf(' %d$ CAD ', input)
end
end
class HashOrderingTest < Test::Unit::TestCase
include Liquid
def test_global_register_order
Template.register_filter(MoneyFilter)
Template.register_filter(CanadianMoneyFilter)
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render(nil, nil)
end
end

View File

@@ -1,134 +0,0 @@
require 'test_helper'
class StatementsTest < Test::Unit::TestCase
include Liquid
def test_true_eql_true
text = %| {% if true == true %} true {% else %} false {% endif %} |
expected = %| true |
assert_equal expected, Template.parse(text).render
end
def test_true_not_eql_true
text = %| {% if true != true %} true {% else %} false {% endif %} |
expected = %| false |
assert_equal expected, Template.parse(text).render
end
def test_true_lq_true
text = %| {% if 0 > 0 %} true {% else %} false {% endif %} |
expected = %| false |
assert_equal expected, Template.parse(text).render
end
def test_one_lq_zero
text = %| {% if 1 > 0 %} true {% else %} false {% endif %} |
expected = %| true |
assert_equal expected, Template.parse(text).render
end
def test_zero_lq_one
text = %| {% if 0 < 1 %} true {% else %} false {% endif %} |
expected = %| true |
assert_equal expected, Template.parse(text).render
end
def test_zero_lq_or_equal_one
text = %| {% if 0 <= 0 %} true {% else %} false {% endif %} |
expected = %| true |
assert_equal expected, Template.parse(text).render
end
def test_zero_lq_or_equal_one_involving_nil
text = %| {% if null <= 0 %} true {% else %} false {% endif %} |
expected = %| false |
assert_equal expected, Template.parse(text).render
text = %| {% if 0 <= null %} true {% else %} false {% endif %} |
expected = %| false |
assert_equal expected, Template.parse(text).render
end
def test_zero_lqq_or_equal_one
text = %| {% if 0 >= 0 %} true {% else %} false {% endif %} |
expected = %| true |
assert_equal expected, Template.parse(text).render
end
def test_strings
text = %| {% if 'test' == 'test' %} true {% else %} false {% endif %} |
expected = %| true |
assert_equal expected, Template.parse(text).render
end
def test_strings_not_equal
text = %| {% if 'test' != 'test' %} true {% else %} false {% endif %} |
expected = %| false |
assert_equal expected, Template.parse(text).render
end
def test_var_strings_equal
text = %| {% if var == "hello there!" %} true {% else %} false {% endif %} |
expected = %| true |
assert_equal expected, Template.parse(text).render('var' => 'hello there!')
end
def test_var_strings_are_not_equal
text = %| {% if "hello there!" == var %} true {% else %} false {% endif %} |
expected = %| true |
assert_equal expected, Template.parse(text).render('var' => 'hello there!')
end
def test_var_and_long_string_are_equal
text = %| {% if var == 'hello there!' %} true {% else %} false {% endif %} |
expected = %| true |
assert_equal expected, Template.parse(text).render('var' => 'hello there!')
end
def test_var_and_long_string_are_equal_backwards
text = %| {% if 'hello there!' == var %} true {% else %} false {% endif %} |
expected = %| true |
assert_equal expected, Template.parse(text).render('var' => 'hello there!')
end
#def test_is_nil
# text = %| {% if var != nil %} true {% else %} false {% end %} |
# @template.assigns = { 'var' => 'hello there!'}
# expected = %| true |
# assert_equal expected, @template.parse(text)
#end
def test_is_collection_empty
text = %| {% if array == empty %} true {% else %} false {% endif %} |
expected = %| true |
assert_equal expected, Template.parse(text).render('array' => [])
end
def test_is_not_collection_empty
text = %| {% if array == empty %} true {% else %} false {% endif %} |
expected = %| false |
assert_equal expected, Template.parse(text).render('array' => [1,2,3])
end
def test_nil
text = %| {% if var == nil %} true {% else %} false {% endif %} |
expected = %| true |
assert_equal expected, Template.parse(text).render('var' => nil)
text = %| {% if var == null %} true {% else %} false {% endif %} |
expected = %| true |
assert_equal expected, Template.parse(text).render('var' => nil)
end
def test_not_nil
text = %| {% if var != nil %} true {% else %} false {% endif %} |
expected = %| true |
assert_equal expected, Template.parse(text).render('var' => 1 )
text = %| {% if var != null %} true {% else %} false {% endif %} |
expected = %| true |
assert_equal expected, Template.parse(text).render('var' => 1 )
end
end # StatementsTest

View File

@@ -1,64 +0,0 @@
require 'test_helper'
class TokenizerTest < Test::Unit::TestCase
def test_tokenize_strings
assert_equal [' '], tokenize(' ')
assert_equal ['hello world'], tokenize('hello world')
end
def test_tokenize_variables
assert_equal ['{{funk}}'], tokenize('{{funk}}')
assert_equal [' ', '{{funk}}', ' '], tokenize(' {{funk}} ')
assert_equal [' ', '{{funk}}', ' ', '{{so}}', ' ', '{{brother}}', ' '], tokenize(' {{funk}} {{so}} {{brother}} ')
assert_equal [' ', '{{ funk }}', ' '], tokenize(' {{ funk }} ')
end
def test_tokenize_blocks
assert_equal ['{%comment%}'], tokenize('{%comment%}')
assert_equal [' ', '{%comment%}', ' '], tokenize(' {%comment%} ')
assert_equal [' ', '{%comment%}', ' ', '{%endcomment%}', ' '], tokenize(' {%comment%} {%endcomment%} ')
assert_equal [' ', '{% comment %}', ' ', '{% endcomment %}', ' '], tokenize(" {% comment %} {% endcomment %} ")
end
def test_tokenize_incomplete_end
assert_tokens 'before{{ incomplete }after', ['before', '{{ incomplete }', 'after']
assert_tokens 'before{% incomplete %after', ['before', '{%', ' incomplete %after']
end
def test_tokenize_no_end
assert_tokens 'before{{ unterminated ', ['before', '{{', ' unterminated ']
assert_tokens 'before{% unterminated ', ['before', '{%', ' unterminated ']
end
private
def assert_tokens(source, expected)
assert_equal expected, tokenize(source)
assert_equal expected, old_tokenize(source)
end
def tokenize(source)
tokenizer = Liquid::Tokenizer.new(source)
tokens = []
while token = tokenizer.next
tokens << token
end
tokens
end
AnyStartingTag = /\{\{|\{\%/
VariableIncompleteEnd = /\}\}?/
PartialTemplateParser = /#{Liquid::TagStart}.*?#{Liquid::TagEnd}|#{Liquid::VariableStart}.*?#{VariableIncompleteEnd}/o
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/o
def old_tokenize(source)
return [] if source.to_s.empty?
tokens = source.split(TemplateParser)
# removes the rogue empty element at the beginning of the array
tokens.shift if tokens[0] and tokens[0].empty?
tokens
end
end

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