Compare commits

..

67 Commits

Author SHA1 Message Date
Lourens Naudé
e853bf5b84 Also dump object distribution 2015-05-06 10:45:20 -04:00
Lourens Naudé
27248f1eb1 Lazy init context errors and interrupts to prevent excess allocation 2015-05-05 23:40:52 -04:00
Lourens Naudé
174839fbef Reuse filters array during variable parsing 2015-05-05 23:30:05 -04:00
Lourens Naudé
01a86728f2 Remove needless intermediate local var in VariableLookup#initialize 2015-05-05 23:21:28 -04:00
Lourens Naudé
0e38f88b58 Recycle the array buffer in BlockBody#render 2015-05-05 23:02:08 -04:00
Lourens Naudé
5a48edae6a Introduce support for memory usage profiles 2015-05-05 23:00:28 -04:00
Justin Li
76c24db039 Remove ruby-head from allowed failures 2015-05-05 12:49:04 -04:00
James Reid-Smith
251ce7483c Merge pull request #441 from Shopify/remove_context_from_read_template_file
Removed context from read_template_file, fixed tests to match new arity
2015-04-27 12:13:36 -04:00
James Reid-Smith
4592afcc8b Updated History.md and removed a couple remaining methods using the old signature 2015-04-27 15:45:44 +00:00
James Reid-Smith
448766b0c4 Removed context from read_template_file, fixed tests to match new arity 2015-04-27 15:27:03 +00:00
Justin Li
6390652c3f Update changelog with backported patches 2015-04-24 16:09:37 -04:00
Justin Li
f266aee2e5 Slightly more concise issue# reference in changelog 2015-04-21 23:40:42 -04:00
Justin Li
df0649a031 Update changelog 2015-04-21 23:36:54 -04:00
Justin Li
78a5972487 Merge pull request #541 from Shopify/history-sync
Sync History.md for Liquid 4
2015-04-21 23:34:29 -04:00
Justin Li
298ae3357c Merge pull request #551 from Shopify/expose-variable-name
Merge pull request 551
2015-04-21 23:33:13 -04:00
Justin Li
f1f3f57647 Remove command_lookups reader 2015-04-21 00:25:51 -04:00
Justin Li
e5dd63e1fc Expose name, lookups, and command flags from VariableLookup 2015-04-20 17:36:04 -04:00
Justin Li
881f86d698 Merge pull request #550 from Shopify/minitest-fail-workaround
Disable minitest expectation interface due to reckless modification of Object
2015-04-20 10:22:19 -04:00
Justin Li
a1b209d212 Disable minitest expectation interface due to reckless modification of Object 2015-04-20 10:15:19 -04:00
Thierry Joyal
8e5926669b Merge pull request #545 from Shopify/explode-invokable_methods-on-drop
Explode invokable_methods method on Liquid::Drop
2015-04-02 09:02:48 -07:00
Thierry Joyal
8736b602ea Explode invokable_methods method on Liquid::Drop 2015-04-02 13:16:07 +00:00
Justin Li
b8365af07d Add changes for 4.0.0 2015-03-25 14:53:43 -04:00
Justin Li
53842a471e Create history section for 4.0.0 2015-03-25 14:40:19 -04:00
Justin Li
86a82d3039 Merge pull request #540 from Shopify/array-concat
Add array concat filter
2015-03-25 01:42:22 -04:00
Justin Li
2b78e74b4e Add test for concat filter with non-array input 2015-03-25 01:34:47 -04:00
divecch
db396dd739 adding concat filter to append arrays 2015-03-25 01:31:22 -04:00
Justin Li
3213db54d6 Merge pull request #520 from Shopify/forloop-parentloop
Add forloop.parentloop as a reference to the parent loop
2015-03-25 01:22:35 -04:00
Justin Li
97a3f145a1 Merge pull request #499 from kreynolds/to_date_downcase_regression
Fix case sensitivity regression in date standard filter
2015-03-25 01:22:04 -04:00
Florian Weingarten
2fbe813770 Merge pull request #539 from Dorian/patch-1
Update module_ex.rb code documentation and code style
2015-03-24 15:21:22 +01:00
Dorian Marié
23a23c6419 Update module_ex.rb code documentation and code style
Didn't look good on rubydoc.info: http://i.imgur.com/469N92P.png
2015-03-24 14:09:08 +01:00
Dylan Thacker-Smith
63eb1aac69 Merge pull request #519 from Shopify/remove-filter-method-blacklist
Allow filters to redefine Object methods to make them invokable.
2015-02-04 18:07:51 -05:00
Justin Li
205bd19d3f Add forloop.parentloop as a reference to the parent loop 2015-02-04 12:43:09 -05:00
Dylan Thacker-Smith
950f062041 Allow filters to redefine Object methods to make them invokable. 2015-02-03 13:51:33 -05:00
Tobias Lütke
3476a556dd Merge pull request #512 from Shopify/fix_tobi_name
Fix Tobi last name on gemspec
2015-01-23 21:24:04 -05:00
Arthur Neves
d2ef9cef10 master is 4.0.0 2015-01-23 10:49:07 -05:00
Arthur Neves
0021c93fef Add ruby 2.2 to travis
and allow failure on ruby head
2015-01-23 10:42:26 -05:00
Arthur Neves
dcf7064460 Fix Tobi last name on gemspec 2015-01-23 10:21:40 -05:00
Florian Weingarten
bebd3570ee Merge pull request #506 from Shopify/fix_capture_with_hyphen
Use VariableSignature as Syntax for Capture tag to allow hyphens in variable names
2015-01-10 23:27:00 -05:00
Florian Weingarten
7cfee1616a Use VariableSignature as Syntax for Capture tag to allow hyphens in variable names 2015-01-09 14:15:26 +00:00
Arthur Nogueira Neves
4b0a7c5d1d Merge pull request #504 from alfredxing/duplicate-keys
Remove duplicate `index0` key in TableRow tag
2014-12-30 13:15:10 -05:00
Alfred Xing
5df1a262ad Remove duplicate key in hash 2014-12-25 12:12:42 -08:00
Kelley Reynolds
84fddba2e1 Remove regex for downcase and is_a?(String) 2014-12-18 13:01:23 -05:00
Kelley Reynolds
8b0774b519 Fix case sensitivity regression in date standard filter 2014-12-16 10:37:05 -05:00
Justin Li
e2f8b28f56 Merge pull request #492 from Shopify/resource-counting-perf
Resource counting perf
2014-12-11 16:05:41 -05:00
Justin Li
3080f95a4f Make render_length tests stricter 2014-12-11 10:41:47 -05:00
Justin Li
cc57908c03 Add test for render_length persisting between block bodies 2014-12-11 10:38:47 -05:00
Justin Li
4df4f218cf Use same template instance 2014-12-09 17:25:15 -05:00
Justin Li
c2f71ee86b Reset resource consumption before each render 2014-12-09 17:23:07 -05:00
Justin Li
9f7e601110 Convert render output to strings in BlockBody 2014-12-05 15:17:09 -05:00
Justin Li
3755031c18 Merge pull request #485 from Shopify/lazy-load-profiler-hooks
Defer loading profiler hooks
2014-12-05 15:10:16 -05:00
Justin Li
b628477af1 Disambiguate checking if Liquid::Profiler is defined 2014-12-04 17:51:54 -05:00
Justin Li
dd455a6361 Force user to require the profiler themselves 2014-12-04 17:48:26 -05:00
Justin Li
8c70682d6b Don't automatically load hooks 2014-12-04 17:39:41 -05:00
Justin Li
742b3c69bb Remove commented code 2014-12-04 16:30:37 -05:00
Justin Li
1593b784a7 Simplify interface for setting template resource limits 2014-12-04 16:18:21 -05:00
Justin Li
db00ec8b32 Move resource limit tracking to its own class 2014-12-04 16:18:09 -05:00
Justin Li
3ca40b5dea Merge pull request #491 from Shopify/drop-ruby-1-9
Drop Ruby 1.9 from CI, add Ruby head
2014-12-03 12:52:10 -05:00
Justin Li
378775992f Drop Ruby 1.9 from CI, add Ruby head 2014-12-02 14:33:51 -05:00
Florian Weingarten
319400ea23 Merge pull request #489 from alex-ross/patch-1
Fixes syntax error in documentation for unless tag
2014-11-19 14:02:58 +01:00
Alexander Ross
289a03f9d7 Fixes syntax error in documentation for unless tag 2014-11-19 10:49:58 +01:00
Justin Li
a0710f4c70 Merge pull request #486 from Shopify/fix-exponential-warnings
Fix #warnings taking exponential time to compute
2014-11-12 17:22:16 -05:00
Justin Li
737be1a0c1 Use Timeout#timeout for warnings tests 2014-11-12 17:03:48 -05:00
Justin Li
1673098126 Handle potential case where warnings returns nil 2014-11-12 16:46:10 -05:00
Justin Li
422bafd66a Fix #warnings taking exponential time to compute 2014-11-12 16:12:00 -05:00
Justin Li
c0aab820ed Lazily load profiler hooks 2014-11-12 00:05:01 -05:00
Florian Weingarten
3321cffe08 Merge pull request #482 from joshk/patch-1
Use the new beta build env on Travis
2014-11-07 03:06:52 +01:00
Josh Kalderimis
f2772518b0 Use the new beta build env on Travis
job start in seconds, instead of 20-120 seconds
2014-11-07 14:54:21 +13:00
63 changed files with 701 additions and 517 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -34,7 +34,7 @@ module Liquid
return yield tag_name, markup return yield tag_name, markup
end end
else else
raise SyntaxError.new(options[:locale].t("errors.syntax.tag_termination".freeze, :token => token, :tag_end => TagEnd.inspect)) raise_missing_tag_terminator(token, options)
end end
when token.start_with?(VARSTART) when token.start_with?(VARSTART)
new_var = create_variable(token, options) new_var = create_variable(token, options)
@@ -62,15 +62,15 @@ module Liquid
def warnings def warnings
all_warnings = [] all_warnings = []
nodelist.each do |node| nodelist.each do |node|
all_warnings.concat(node.warnings) if node.respond_to?(:warnings) && node.warnings all_warnings.concat(node.warnings || []) if node.respond_to?(:warnings)
end end
all_warnings all_warnings
end end
def render(context) def render(context)
output = [] @output ||= []
context.resource_limits[:render_length_current] = 0 @output.clear
context.resource_limits[:render_score_current] += @nodelist.length context.resource_limits.render_score += @nodelist.length
@nodelist.each do |token| @nodelist.each do |token|
# Break out if we have any unhanded interrupts. # Break out if we have any unhanded interrupts.
@@ -88,28 +88,29 @@ module Liquid
token_output = render_token(token, context) token_output = render_token(token, context)
unless token.is_a?(Block) && token.blank? unless token.is_a?(Block) && token.blank?
output << token_output @output << token_output
end end
rescue MemoryError => e rescue MemoryError => e
raise e raise e
rescue ::StandardError => e rescue ::StandardError => e
output << context.handle_error(e, token) @output << context.handle_error(e, token)
end end
end end
output.join @output.join
end end
private private
def render_token(token, context) def render_token(token, context)
token_output = (token.respond_to?(:render) ? token.render(context) : token) token_output = (token.respond_to?(:render) ? token.render(context) : token)
context.increment_used_resources(:render_length_current, token_output) token_str = token_output.is_a?(Array) ? token_output.join : token_output.to_s
if context.resource_limits_reached?
context.resource_limits[:reached] = true context.resource_limits.render_length += token_str.length
if context.resource_limits.reached?
raise MemoryError.new("Memory limits exceeded".freeze) raise MemoryError.new("Memory limits exceeded".freeze)
end end
token_output token_str
end end
def create_variable(token, options) def create_variable(token, options)
@@ -117,6 +118,14 @@ module Liquid
markup = token.is_a?(Token) ? token.child(content.first) : content.first markup = token.is_a?(Token) ? token.child(content.first) : content.first
return Variable.new(markup, options) return Variable.new(markup, options)
end end
raise_missing_variable_terminator(token, options)
end
def raise_missing_tag_terminator(token, options)
raise SyntaxError.new(options[:locale].t("errors.syntax.tag_termination".freeze, :token => token, :tag_end => TagEnd.inspect))
end
def raise_missing_variable_terminator(token, options)
raise SyntaxError.new(options[:locale].t("errors.syntax.variable_termination".freeze, :token => token, :tag_end => VariableEnd.inspect)) raise SyntaxError.new(options[:locale].t("errors.syntax.variable_termination".freeze, :token => token, :tag_end => VariableEnd.inspect))
end end
end end

View File

@@ -3,7 +3,7 @@ module Liquid
# #
# Example: # Example:
# #
# c = Condition.new('1', '==', '1') # c = Condition.new(1, '==', 1)
# c.evaluate #=> true # c.evaluate #=> true
# #
class Condition #:nodoc: class Condition #:nodoc:
@@ -96,10 +96,10 @@ module Liquid
# If the operator is empty this means that the decision statement is just # If the operator is empty this means that the decision statement is just
# a single variable. We can just poll this variable from the context and # a single variable. We can just poll this variable from the context and
# return this as the result. # return this as the result.
return context[left] if op == nil return context.evaluate(left) if op == nil
left = context[left] left = context.evaluate(left)
right = context[right] right = context.evaluate(right)
operation = self.class.operators[op] || raise(Liquid::ArgumentError.new("Unknown operator #{op}")) operation = self.class.operators[op] || raise(Liquid::ArgumentError.new("Unknown operator #{op}"))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,16 +7,17 @@
# to the allowed method passed with the liquid_methods call # to the allowed method passed with the liquid_methods call
# Example: # Example:
# #
# class SomeClass # class SomeClass
# liquid_methods :an_allowed_method # liquid_methods :an_allowed_method
# #
# def an_allowed_method # def an_allowed_method
# 'this comes from an allowed method' # 'this comes from an allowed method'
# end
#
# def unallowed_method
# 'this will never be an output'
# end
# end # end
# def unallowed_method
# 'this will never be an output'
# end
# end
# #
# if you want to extend the drop to other methods you can defines more methods # if you want to extend the drop to other methods you can defines more methods
# in the class <YourClass>::LiquidDropClass # in the class <YourClass>::LiquidDropClass
@@ -26,31 +27,33 @@
# 'and this from another allowed method' # 'and this from another allowed method'
# end # end
# end # end
# end #
# #
# usage: # usage:
# @something = SomeClass.new # @something = SomeClass.new
# #
# template: # template:
# {{something.an_allowed_method}}{{something.unallowed_method}} {{something.another_allowed_method}} # {{something.an_allowed_method}}{{something.unallowed_method}} {{something.another_allowed_method}}
# #
# output: # output:
# 'this comes from an allowed method and this from another allowed method' # 'this comes from an allowed method and this from another allowed method'
# #
# You can also chain associations, by adding the liquid_method call in the # You can also chain associations, by adding the liquid_method call in the
# association models. # association models.
# #
class Module class Module
def liquid_methods(*allowed_methods) def liquid_methods(*allowed_methods)
drop_class = eval "class #{self.to_s}::LiquidDropClass < Liquid::Drop; self; end" drop_class = eval "class #{self.to_s}::LiquidDropClass < Liquid::Drop; self; end"
define_method :to_liquid do define_method :to_liquid do
drop_class.new(self) drop_class.new(self)
end end
drop_class.class_eval do drop_class.class_eval do
def initialize(object) def initialize(object)
@object = object @object = object
end end
allowed_methods.each do |sym| allowed_methods.each do |sym|
define_method sym do define_method sym do
@object.send sym @object.send sym
@@ -58,5 +61,4 @@ class Module
end end
end end
end end
end end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,7 +18,9 @@ module Liquid
:locale => I18n.new :locale => I18n.new
} }
attr_accessor :root, :resource_limits attr_accessor :root
attr_reader :resource_limits
@@file_system = BlankFileSystem.new @@file_system = BlankFileSystem.new
class TagRegistry class TagRegistry
@@ -110,7 +112,7 @@ module Liquid
end end
def initialize def initialize
@resource_limits = self.class.default_resource_limits.dup @resource_limits = ResourceLimits.new(self.class.default_resource_limits)
end end
# Parse source code. # Parse source code.
@@ -203,6 +205,9 @@ module Liquid
context.add_filters(args.pop) context.add_filters(args.pop)
end end
# Retrying a render resets resource usage
context.resource_limits.reset
begin begin
# render the nodelist. # render the nodelist.
# for performance reasons we get an array back here. join will make a string out of it. # for performance reasons we get an array back here. join will make a string out of it.
@@ -250,6 +255,8 @@ module Liquid
def with_profiling def with_profiling
if @profiling && !@options[:included] if @profiling && !@options[:included]
raise "Profiler not loaded, require 'liquid/profiler' first" unless defined?(Liquid::Profiler)
@profiler = Profiler.new @profiler = Profiler.new
@profiler.start @profiler.start

View File

@@ -12,7 +12,6 @@ module Liquid
# #
class Variable class Variable
FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o
EasyParse = /\A *(\w+(?:\.\w+)*) *\z/
attr_accessor :filters, :name, :warnings attr_accessor :filters, :name, :warnings
attr_accessor :line_number attr_accessor :line_number
include ParserSwitching include ParserSwitching
@@ -34,7 +33,8 @@ module Liquid
end end
def lax_parse(markup) def lax_parse(markup)
@filters = [] @filters ||= []
@filters.clear
if markup =~ /(#{QuotedFragment})(.*)/om if markup =~ /(#{QuotedFragment})(.*)/om
name_markup = $1 name_markup = $1
filter_markup = $2 filter_markup = $2
@@ -53,17 +53,11 @@ module Liquid
end end
def strict_parse(markup) def strict_parse(markup)
# Very simple valid cases @filters ||= []
if markup =~ EasyParse @filters.clear
@name = Expression.parse($1)
@filters = []
return
end
@filters = []
p = Parser.new(markup) p = Parser.new(markup)
# Could be just filters with no input
@name = p.look(:pipe) ? nil : Expression.parse(p.expression) @name = Expression.parse(p.expression)
while p.consume?(:pipe) while p.consume?(:pipe)
filtername = p.consume(:id) filtername = p.consume(:id)
filterargs = p.consume?(:colon) ? parse_filterargs(p) : [] filterargs = p.consume?(:colon) ? parse_filterargs(p) : []

View File

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

View File

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

View File

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

19
performance/memory.rb Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -28,11 +28,14 @@ class ParsingQuirksTest < Minitest::Test
def test_error_on_empty_filter def test_error_on_empty_filter
assert Template.parse("{{test}}") assert Template.parse("{{test}}")
assert Template.parse("{{|test}}")
with_error_mode(:lax) do
assert Template.parse("{{|test}}")
end
with_error_mode(:strict) do with_error_mode(:strict) do
assert_raises(SyntaxError) do assert_raises(SyntaxError) { Template.parse("{{|test}}") }
Template.parse("{{test |a|b|}}") assert_raises(SyntaxError) { Template.parse("{{test |a|b|}}") }
end
end end
end end

View File

@@ -4,7 +4,7 @@ class RenderProfilingTest < Minitest::Test
include Liquid include Liquid
class ProfilingFileSystem class ProfilingFileSystem
def read_template_file(template_path, context) def read_template_file(template_path)
"Rendering template {% assign template_name = '#{template_path}'%}\n{{ template_name }}" "Rendering template {% assign template_name = '#{template_path}'%}\n{{ template_name }}"
end end
end end
@@ -89,7 +89,7 @@ class RenderProfilingTest < Minitest::Test
include_node = t.profiler[1] include_node = t.profiler[1]
include_node.children.each do |child| include_node.children.each do |child|
assert_equal "'a_template'", child.partial assert_equal "a_template", child.partial
end end
end end
@@ -99,12 +99,12 @@ class RenderProfilingTest < Minitest::Test
a_template = t.profiler[1] a_template = t.profiler[1]
a_template.children.each do |child| a_template.children.each do |child|
assert_equal "'a_template'", child.partial assert_equal "a_template", child.partial
end end
b_template = t.profiler[2] b_template = t.profiler[2]
b_template.children.each do |child| b_template.children.each do |child|
assert_equal "'b_template'", child.partial assert_equal "b_template", child.partial
end end
end end
@@ -114,12 +114,12 @@ class RenderProfilingTest < Minitest::Test
a_template1 = t.profiler[1] a_template1 = t.profiler[1]
a_template1.children.each do |child| a_template1.children.each do |child|
assert_equal "'a_template'", child.partial assert_equal "a_template", child.partial
end end
a_template2 = t.profiler[2] a_template2 = t.profiler[2]
a_template2.children.each do |child| a_template2.children.each do |child|
assert_equal "'a_template'", child.partial assert_equal "a_template", child.partial
end end
end end

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
require 'test_helper' require 'test_helper'
class TestFileSystem class TestFileSystem
def read_template_file(template_path, context) def read_template_file(template_path)
case template_path case template_path
when "product" when "product"
"Product: {{ product.title }} " "Product: {{ product.title }} "
@@ -37,14 +37,14 @@ class TestFileSystem
end end
class OtherFileSystem class OtherFileSystem
def read_template_file(template_path, context) def read_template_file(template_path)
'from OtherFileSystem' 'from OtherFileSystem'
end end
end end
class CountingFileSystem class CountingFileSystem
attr_reader :count attr_reader :count
def read_template_file(template_path, context) def read_template_file(template_path)
@count ||= 0 @count ||= 0
@count += 1 @count += 1
'from CountingFileSystem' 'from CountingFileSystem'
@@ -132,7 +132,7 @@ class IncludeTagTest < Minitest::Test
def test_recursively_included_template_does_not_produce_endless_loop def test_recursively_included_template_does_not_produce_endless_loop
infinite_file_system = Class.new do infinite_file_system = Class.new do
def read_template_file(template_path, context) def read_template_file(template_path)
"-{% include 'loop' %}" "-{% include 'loop' %}"
end end
end end
@@ -145,18 +145,6 @@ class IncludeTagTest < Minitest::Test
end end
def test_backwards_compatability_support_for_overridden_read_template_file
infinite_file_system = Class.new do
def read_template_file(template_path) # testing only one argument here.
"- hi mom"
end
end
Liquid::Template.file_system = infinite_file_system.new
Template.parse("{% include 'hi_mom' %}").render!
end
def test_dynamically_choosen_template def test_dynamically_choosen_template
assert_template_result "Test123", "{% include template %}", "template" => 'Test123' assert_template_result "Test123", "{% include template %}", "template" => 'Test123'
assert_template_result "Test321", "{% include template %}", "template" => 'Test321' assert_template_result "Test321", "{% include template %}", "template" => 'Test321'

View File

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

View File

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

View File

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

View File

@@ -465,20 +465,11 @@ class ContextUnitTest < Minitest::Test
mock_empty = Spy.on_instance_method(Array, :empty?) mock_empty = Spy.on_instance_method(Array, :empty?)
mock_has_interrupt = Spy.on(@context, :has_interrupt?).and_call_through mock_has_interrupt = Spy.on(@context, :has_interrupt?).and_call_through
@context.push_interrupt(StandardError.new)
@context.has_interrupt? @context.has_interrupt?
refute mock_any.has_been_called? refute mock_any.has_been_called?
assert mock_empty.has_been_called? assert mock_empty.has_been_called?
end
def test_variable_lookup_caches_markup
mock_scan = Spy.on_instance_method(String, :scan).and_return(["string"])
@context['string'] = 'string'
@context['string']
@context['string']
assert_equal 1, mock_scan.calls.size
end end
def test_context_initialization_with_a_proc_in_environment def test_context_initialization_with_a_proc_in_environment

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -102,6 +102,17 @@ class VariableUnitTest < Minitest::Test
assert_equal 1000.01, var.name assert_equal 1000.01, var.name
end end
def test_dashes
assert_equal VariableLookup.new('foo-bar'), Variable.new('foo-bar').name
assert_equal VariableLookup.new('foo-bar-2'), Variable.new('foo-bar-2').name
with_error_mode :strict do
assert_raises(Liquid::SyntaxError) { Variable.new('foo - bar') }
assert_raises(Liquid::SyntaxError) { Variable.new('-foo') }
assert_raises(Liquid::SyntaxError) { Variable.new('2foo') }
end
end
def test_string_with_special_chars def test_string_with_special_chars
var = Variable.new(%| 'hello! $!@.;"ddasd" ' |) var = Variable.new(%| 'hello! $!@.;"ddasd" ' |)
assert_equal 'hello! $!@.;"ddasd" ', var.name assert_equal 'hello! $!@.;"ddasd" ', var.name
@@ -136,4 +147,10 @@ class VariableUnitTest < Minitest::Test
var = Variable.new(%! name_of_variable | upcase !) var = Variable.new(%! name_of_variable | upcase !)
assert_equal " name_of_variable | upcase ", var.raw assert_equal " name_of_variable | upcase ", var.raw
end end
def test_variable_lookup_interface
lookup = VariableLookup.new('a.b.c')
assert_equal 'a', lookup.name
assert_equal ['b', 'c'], lookup.lookups
end
end end