Compare commits

...

93 Commits

Author SHA1 Message Date
Dylan Thacker-Smith
f68fa84a2f WIP: Add column number 2014-12-16 20:59:23 -05:00
Justin Li
e2f8b28f56 Merge pull request #492 from Shopify/resource-counting-perf
Resource counting perf
2014-12-11 16:05:41 -05:00
Justin Li
3080f95a4f Make render_length tests stricter 2014-12-11 10:41:47 -05:00
Justin Li
cc57908c03 Add test for render_length persisting between block bodies 2014-12-11 10:38:47 -05:00
Justin Li
4df4f218cf Use same template instance 2014-12-09 17:25:15 -05:00
Justin Li
c2f71ee86b Reset resource consumption before each render 2014-12-09 17:23:07 -05:00
Justin Li
9f7e601110 Convert render output to strings in BlockBody 2014-12-05 15:17:09 -05:00
Justin Li
3755031c18 Merge pull request #485 from Shopify/lazy-load-profiler-hooks
Defer loading profiler hooks
2014-12-05 15:10:16 -05:00
Justin Li
b628477af1 Disambiguate checking if Liquid::Profiler is defined 2014-12-04 17:51:54 -05:00
Justin Li
dd455a6361 Force user to require the profiler themselves 2014-12-04 17:48:26 -05:00
Justin Li
8c70682d6b Don't automatically load hooks 2014-12-04 17:39:41 -05:00
Justin Li
742b3c69bb Remove commented code 2014-12-04 16:30:37 -05:00
Justin Li
1593b784a7 Simplify interface for setting template resource limits 2014-12-04 16:18:21 -05:00
Justin Li
db00ec8b32 Move resource limit tracking to its own class 2014-12-04 16:18:09 -05:00
Justin Li
3ca40b5dea Merge pull request #491 from Shopify/drop-ruby-1-9
Drop Ruby 1.9 from CI, add Ruby head
2014-12-03 12:52:10 -05:00
Justin Li
378775992f Drop Ruby 1.9 from CI, add Ruby head 2014-12-02 14:33:51 -05:00
Florian Weingarten
319400ea23 Merge pull request #489 from alex-ross/patch-1
Fixes syntax error in documentation for unless tag
2014-11-19 14:02:58 +01:00
Alexander Ross
289a03f9d7 Fixes syntax error in documentation for unless tag 2014-11-19 10:49:58 +01:00
Justin Li
a0710f4c70 Merge pull request #486 from Shopify/fix-exponential-warnings
Fix #warnings taking exponential time to compute
2014-11-12 17:22:16 -05:00
Justin Li
737be1a0c1 Use Timeout#timeout for warnings tests 2014-11-12 17:03:48 -05:00
Justin Li
1673098126 Handle potential case where warnings returns nil 2014-11-12 16:46:10 -05:00
Justin Li
422bafd66a Fix #warnings taking exponential time to compute 2014-11-12 16:12:00 -05:00
Justin Li
c0aab820ed Lazily load profiler hooks 2014-11-12 00:05:01 -05:00
Florian Weingarten
3321cffe08 Merge pull request #482 from joshk/patch-1
Use the new beta build env on Travis
2014-11-07 03:06:52 +01:00
Josh Kalderimis
f2772518b0 Use the new beta build env on Travis
job start in seconds, instead of 20-120 seconds
2014-11-07 14:54:21 +13:00
Justin Li
76ef675eb2 Merge pull request #481 from Shopify/fix-nil-blank
Coerce regex @blank output to a boolean
2014-11-06 13:03:15 -05:00
Justin Li
e5fd4d929f Coerce regex @blank output to a boolean 2014-11-05 20:44:06 -05:00
Justin Li
2e42c7be1f Merge pull request #480 from Shopify/number_variables
Add quirks test for variables with number prefixes
2014-11-05 12:05:21 -05:00
Justin Li
95b031ee04 Add quirks test for extra dots in ranges 2014-11-05 11:41:12 -05:00
Justin Li
4d97a714a9 Add quirks test for variables with number prefixes 2014-11-05 10:56:58 -05:00
Justin Li
aa182f64b4 Merge pull request #479 from Shopify/tweaks-for-c
Tweaks for C
2014-11-04 14:02:14 -05:00
Justin Li
4e870302b1 Add env var for saving stackprof graphviz output 2014-11-04 18:38:14 +00:00
Justin Li
098c89b5f5 Convenience methods for raising terminator syntax errors 2014-11-04 18:38:08 +00:00
Justin Li
70c45f8cd8 Use SVG badge URLs
[ci skip]
2014-11-03 17:41:42 -05:00
Justin Li
12d526a05c Merge pull request #458 from Shopify/block-body
Create a BlockBody class to decouple block body parsing from tags.
2014-11-03 17:34:39 -05:00
Dylan Thacker-Smith
2fd8ad08c0 Remove unused local variable that was accidentally added. 2014-11-03 17:07:42 -05:00
Dylan Thacker-Smith
15e1d46125 Avoid storing options instance variable in BlockBody.
There is no need to pass parse options to the BlockBody initializer, since
it does all the parsing in the parse method, unlike tags which parse the
tag markup in the initializer.
2014-11-03 17:07:42 -05:00
Dylan Thacker-Smith
73fcd42403 Create a BlockBody class to decouple block body parsing from tags. 2014-11-03 17:07:42 -05:00
Justin Li
263e90e772 Merge pull request #478 from Shopify/numbers-in-identifiers
Use a single token for identifiers
2014-10-30 21:59:26 -04:00
Justin Li
81770f094d Remove unnecessary + 2014-10-29 13:39:43 -04:00
Justin Li
dd5ee81089 Disallow number and dash identifier prefixes 2014-10-29 12:08:00 -04:00
Justin Li
a07e382617 Use a single token for identifiers 2014-10-29 11:28:41 -04:00
Justin Li
4dc682313f Merge pull request #476 from Shopify/missing-variable-name-error
Disallow filters with no variable in strict mode
2014-10-27 13:56:11 -04:00
Justin Li
5616ddf00e Remove obsolete comment 2014-10-27 13:44:14 -04:00
Justin Li
fcb23a4cd2 Disallow filters with no variable in strict mode 2014-10-27 13:34:27 -04:00
Justin Li
a8f60ff6b1 Merge pull request #472 from Shopify/fix-leaky-test
Fix test leaking error_mode, fix equality check for VariableLookup
2014-10-23 10:12:41 -04:00
Justin Li
a206c8301d Fix test leaking error_mode, fix equality check for VariableLookup 2014-10-22 15:40:41 -04:00
Justin Li
ee0de01480 Merge pull request #469 from Shopify/falsy-variable-fix
Fix case where a variable name is falsy
2014-10-21 15:06:34 -04:00
Justin Li
887b05e6ed Clarify test name 2014-10-21 14:06:30 -04:00
Justin Li
5d68e8803f Ensure nil works as a variable name 2014-10-21 14:03:10 -04:00
Justin Li
dedd1d3dc0 Fix case where a variable name is falsy 2014-10-21 12:09:26 -04:00
Dylan Thacker-Smith
d9ae36ec40 Merge pull request #466 from Shopify/remove-expression-cache
Remove expression cache
2014-10-20 13:57:17 -04:00
Dylan Thacker-Smith
b9ac3fef8f Remove the quotes from the partial string in the profiler timing objects. 2014-10-18 16:26:18 -04:00
Dylan Thacker-Smith
f5faa4858c Remove parsed expression cache. 2014-10-18 15:03:40 -04:00
Dylan Thacker-Smith
bc5e444d04 Use Expression.parse and Context#evaluate in the Include class. 2014-10-18 15:03:40 -04:00
Dylan Thacker-Smith
3a4b63f37e Use Expression.parse and Context#evaluate in the TableRow class. 2014-10-18 15:03:40 -04:00
Dylan Thacker-Smith
a1a128db19 Refactor Condition so that it takes a parsed expression. 2014-10-18 15:03:40 -04:00
Dylan Thacker-Smith
d502b9282a Use Expression.parse and Context#evaluate in the For class. 2014-10-18 15:03:36 -04:00
Dylan Thacker-Smith
fee8e41466 Use Expression.parse and Context#evaluate in the Cycle class. 2014-10-18 14:27:58 -04:00
Dylan Thacker-Smith
37260f17ff Use Expression.parse and Context#evaluate in the Condition class. 2014-10-18 14:27:58 -04:00
Florian Weingarten
2da9d49478 Merge pull request #465 from Shopify/avoid_multi_assigns
Avoid parallel assignments
2014-10-18 16:19:02 +02:00
Florian Weingarten
7196a2d58e Avoid parallel assignments 2014-10-18 13:58:32 +00:00
Justin Li
a056f6521c Merge pull request #463 from Shopify/stricter-identifiers
Separate ? and - into special tokens
2014-10-17 13:45:48 -04:00
Justin Li
de16db9b72 Don't allow - to end a variable name 2014-10-17 13:38:07 -04:00
Justin Li
b4ea483c4e Separate ? and - into special tokens 2014-10-17 13:30:54 -04:00
Justin Li
7843bcca8d Merge pull request #443 from Shopify/completely-parse-variables
Parse expressions in Liquid::Variable#parse.
2014-10-17 13:12:46 -04:00
Florian Weingarten
76ea5596ff Merge pull request #462 from Shopify/flat_map
nodelist flat_map over map.flatten
2014-10-17 18:32:00 +02:00
Florian Weingarten
f9318e8c93 flat_map 2014-10-17 16:11:12 +00:00
Florian Weingarten
71253ec6f9 Merge pull request #459 from Shopify/pop_vs_shift
Use pop over shift to avoid reverse
2014-10-15 21:42:53 +02:00
Florian Weingarten
0fa075b879 Use pop over shift to avoid reverse 2014-10-15 19:26:39 +00:00
Dylan Thacker-Smith
6d080afd22 Merge pull request #446 from Shopify/remove-end-tag
Remove unused Block#end_tag method.
2014-10-14 03:03:31 -04:00
Dylan Thacker-Smith
a67e2a0a00 Remove unused Block#end_tag method.
Although the method is called, it is defined with an empty body and not
overridden to do anything else.
2014-10-14 02:58:11 -04:00
Dylan Thacker-Smith
f387508666 Parse expressions in Liquid::Variable#parse. 2014-10-08 21:06:59 -04:00
Florian Weingarten
632b1fb702 Merge pull request #455 from Shopify/parse_error_line_numbers
Line numbers for all parse errors
2014-10-04 17:53:30 +02:00
Dylan Thacker-Smith
d84870d7a5 Test line number of errors in nested blocks. 2014-10-03 16:25:12 -05:00
Florian Weingarten
584b492e70 Line numbers for all parse errors 2014-10-03 21:00:31 +00:00
Dylan Thacker-Smith
b79c9cb9bf Merge pull request #453 from Shopify/no-modify-default-resource-limit
Avoid modifying the default resources limits hash.
2014-10-01 19:02:09 -05:00
Dylan Thacker-Smith
cf5ccede50 Avoid modifying the default resources limits hash. 2014-10-01 18:51:06 -05:00
Evan Huus
23622a9739 Merge pull request #440 from Shopify/drop-tainting
Variable tainting
2014-09-22 13:43:35 -04:00
Florian Weingarten
7ba5a6ab75 Merge pull request #450 from Shopify/additional_test_for_includes
Regression test for including assignments
2014-09-18 21:05:37 +02:00
Florian Weingarten
be3d261e11 Regression test for including assignments 2014-09-18 10:37:44 +00:00
Evan Huus
eeb061ef44 Address code review comments
- clean up comment wording
- fix potentially leaky tests
2014-09-16 17:23:26 +00:00
Evan Huus
67b2c320a1 Add tainting tests 2014-09-16 17:23:26 +00:00
Evan Huus
1d151885be Auto-untaint variables passed through 'escape' 2014-09-16 17:23:26 +00:00
Evan Huus
e836024dd9 Check and handle when a tainted variable is used 2014-09-16 17:23:26 +00:00
Dylan Thacker-Smith
638455ed92 Merge pull request #448 from Shopify/remove-unused-filter-not-found-error
Remove Liquid::FilterNotFoundError since it is never raised.
2014-09-15 17:43:33 -04:00
Dylan Thacker-Smith
b2a74883e9 Remove Liquid::FilterNotFoundError since it is never raised. 2014-09-15 17:42:07 -04:00
Dylan Thacker-Smith
6875e5e16f Merge pull request #449 from Shopify/fix-flaky-total-render-time-test
Fix flaky test which assumes total_render_time can't be 0.
2014-09-15 17:41:41 -04:00
Dylan Thacker-Smith
a5717a3f8d Fix flaky test which assumes total_render_time can't be 0.
jruby has millisecond precision for Time.now, so total_render_time can be 0
due to this lack of precision.
2014-09-15 17:26:55 -04:00
Dylan Thacker-Smith
804fcfebd1 Merge pull request #444 from Shopify/remove-block-children
Avoid keeping track of two lists of nodes during parsing.
2014-09-15 09:56:08 -04:00
Dylan Thacker-Smith
b37ee5684a Merge pull request #445 from Shopify/prefer-super-over-render-all
Use super rather than render_all in single block render classes.
2014-09-15 09:53:54 -04:00
Dylan Thacker-Smith
0573b63b4c Use super rather than render_all in single block render classes. 2014-09-12 16:58:07 -04:00
Dylan Thacker-Smith
29c21d7867 Avoid keeping track of two lists of nodes during parsing. 2014-09-12 16:43:00 -04:00
55 changed files with 837 additions and 455 deletions

View File

@@ -1,13 +1,16 @@
language: ruby
rvm: rvm:
- 1.9
- 2.0 - 2.0
- 2.1 - 2.1
- jruby-19mode - 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

@@ -3,6 +3,8 @@
## 3.0.0 / not yet released / branch "master" ## 3.0.0 / not yet released / branch "master"
* ... * ...
* Block parsing moved to BlockBody class, see #458 [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, see #423 [Bogdan Gusiev]
* Add url_encode to standard filters, see #421 [Derrick Reimer, djreimer] * Add url_encode to standard filters, see #421 [Derrick Reimer, djreimer]
* Add uniq to standard filters [Florian Weingarten, fw42] * Add uniq to standard filters [Florian Weingarten, fw42]

View File

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

View File

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

View File

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

View File

@@ -1,68 +1,26 @@
module Liquid module Liquid
class Block < Tag class Block < Tag
FullToken = /\A#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om def initialize(tag_name, markup, options)
ContentOfVariable = /\A#{VariableStart}(.*)#{VariableEnd}\z/om super
TAGSTART = "{%".freeze @blank = true
VARSTART = "{{".freeze end
def parse(tokens)
@body = BlockBody.new
while more = parse_body(@body, tokens)
end
end
def render(context)
@body.render(context)
end
def blank? def blank?
@blank @blank
end end
def parse(tokens) def nodelist
@blank = true @body.nodelist
@nodelist ||= []
@nodelist.clear
# All child tags of the current block.
@children = []
while token = tokens.shift
unless token.empty?
case
when token.start_with?(TAGSTART)
if token =~ FullToken
# if we found the proper block delimiter just end parsing here and let the outer block
# proceed
if block_delimiter == $1
end_tag
return
end
# fetch the tag from registered blocks
if tag = Template.tags[$1]
markup = token.is_a?(Token) ? token.child($2) : $2
new_tag = tag.parse($1, markup, tokens, @options)
new_tag.line_number = token.line_number if token.is_a?(Token)
@blank &&= new_tag.blank?
@nodelist << new_tag
@children << new_tag
else
# this tag is not registered with the system
# pass it to the current block for special handling or error reporting
unknown_tag($1, $2, tokens)
end
else
raise SyntaxError.new(options[:locale].t("errors.syntax.tag_termination".freeze, :token => token, :tag_end => TagEnd.inspect))
end
when token.start_with?(VARSTART)
new_var = create_variable(token)
new_var.line_number = token.line_number if token.is_a?(Token)
@nodelist << new_var
@children << new_var
@blank = false
else
@nodelist << token
@blank &&= (token =~ /\A\s*\z/)
end
end
end
# Make sure that it's ok to end parsing in the current block.
# Effectively this method will throw an exception unless the current block is
# of type Document
assert_missing_delimitation!
end end
# warnings of this block and all sub-tags # warnings of this block and all sub-tags
@@ -70,16 +28,13 @@ module Liquid
all_warnings = [] all_warnings = []
all_warnings.concat(@warnings) if @warnings all_warnings.concat(@warnings) if @warnings
(@children || []).each do |node| (nodelist || []).each do |node|
all_warnings.concat(node.warnings || []) all_warnings.concat(node.warnings || []) if node.respond_to?(:warnings)
end end
all_warnings all_warnings
end end
def end_tag
end
def unknown_tag(tag, params, tokens) def unknown_tag(tag, params, tokens)
case tag case tag
when 'else'.freeze when 'else'.freeze
@@ -102,65 +57,23 @@ module Liquid
@block_delimiter ||= "end#{block_name}" @block_delimiter ||= "end#{block_name}"
end end
def create_variable(token)
token.scan(ContentOfVariable) do |content|
markup = token.is_a?(Token) ? token.child(content.first) : content.first
return Variable.new(markup, @options)
end
raise SyntaxError.new(options[:locale].t("errors.syntax.variable_termination".freeze, :token => token, :tag_end => VariableEnd.inspect))
end
def render(context)
render_all(@nodelist, context)
end
protected protected
def assert_missing_delimitation! def parse_body(body, tokens)
raise SyntaxError.new(options[:locale].t("errors.syntax.tag_never_closed".freeze, :block_name => block_name)) body.parse(tokens, options) do |end_tag_name, end_tag_params|
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

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

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

View File

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

View File

@@ -21,10 +21,7 @@ module Liquid
@scopes = [(outer_scope || {})] @scopes = [(outer_scope || {})]
@registers = registers @registers = registers
@errors = [] @errors = []
@resource_limits = resource_limits || Template.default_resource_limits @resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
@resource_limits[:render_score_current] = 0
@resource_limits[:assign_score_current] = 0
@parsed_expression = Hash.new{ |cache, markup| cache[markup] = Expression.parse(markup) }
squash_instance_assigns_with_environments squash_instance_assigns_with_environments
@this_stack_used = false @this_stack_used = false
@@ -37,20 +34,6 @@ module Liquid
@filters = [] @filters = []
end end
def increment_used_resources(key, obj)
@resource_limits[key] += if obj.kind_of?(String) || obj.kind_of?(Array) || obj.kind_of?(Hash)
obj.length
else
1
end
end
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
def strainer def strainer
@strainer ||= Strainer.create(self, @filters) @strainer ||= Strainer.create(self, @filters)
end end
@@ -170,7 +153,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

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

View File

@@ -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

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

View File

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

View File

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

View File

@@ -34,7 +34,7 @@ module Liquid
end end
def escape(input) def escape(input)
CGI.escapeHTML(input) rescue input CGI.escapeHTML(input).untaint rescue input
end end
alias_method :h, :escape alias_method :h, :escape

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

@@ -25,7 +25,7 @@ module Liquid
def render(context) def render(context)
output = super output = super
context.scopes.last[@to] = output context.scopes.last[@to] = output
context.increment_used_resources(:assign_score_current, output) context.resource_limits.assign_score += output.length
''.freeze ''.freeze
end end

View File

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

View File

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

View File

@@ -49,38 +49,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)
@@ -110,7 +112,7 @@ module Liquid
'last'.freeze => (index == length - 1) 'last'.freeze => (index == length - 1)
} }
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 +130,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 +146,38 @@ module Liquid
p = Parser.new(markup) p = Parser.new(markup)
@variable_name = p.consume(:id) @variable_name = p.consume(:id)
raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_in".freeze)) unless p.id?('in'.freeze) raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_in".freeze)) unless p.id?('in'.freeze)
@collection_name = p.expression collection_name = p.expression
@name = "#{@variable_name}-#{@collection_name}" @name = "#{@variable_name}-#{collection_name}"
@collection_name = Expression.parse(collection_name)
@reversed = p.id?('reversed'.freeze) @reversed = p.id?('reversed'.freeze)
@attributes = {}
while p.look(:id) && p.look(:colon, 1) while p.look(:id) && p.look(:colon, 1)
unless attribute = p.id?('limit'.freeze) || p.id?('offset'.freeze) unless attribute = p.id?('limit'.freeze) || p.id?('offset'.freeze)
raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_attribute".freeze)) raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_attribute".freeze))
end end
p.consume p.consume
val = p.expression set_attribute(attribute, p.expression)
@attributes[attribute] = val
end end
p.consume(:end_of_string) p.consume(:end_of_string)
end end
private private
def set_attribute(key, expr)
case key
when 'offset'.freeze
@from = if expr == 'continue'.freeze
:continue
else
Expression.parse(expr)
end
when 'limit'.freeze
@limit = Expression.parse(expr)
end
end
def render_else(context) def render_else(context)
return @else_block ? [render_all(@else_block, context)] : ''.freeze @else_block ? @else_block.render(context) : ''.freeze
end end
def iterable?(collection) def iterable?(collection)

View File

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

View File

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

View File

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

View File

@@ -3,19 +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
if block_delimiter == $2 return if block_delimiter == $2
end_tag
return
end
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
@@ -54,7 +54,7 @@ module Liquid
col += 1 col += 1
result << "<td class=\"col#{col}\">" << render_all(@nodelist, context) << '</td>' result << "<td class=\"col#{col}\">" << super << '</td>'
if col == cols and (index != length - 1) if col == cols and (index != length - 1)
col = 0 col = 0

View File

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

View File

@@ -18,7 +18,9 @@ module Liquid
:locale => I18n.new :locale => I18n.new
} }
attr_accessor :root, :resource_limits attr_accessor :root
attr_reader :resource_limits
@@file_system = BlankFileSystem.new @@file_system = BlankFileSystem.new
class TagRegistry class TagRegistry
@@ -60,6 +62,12 @@ module Liquid
# :strict will enforce correct syntax. # :strict will enforce correct syntax.
attr_writer :error_mode attr_writer :error_mode
# Sets how strict the taint checker should be.
# :lax is the default, and ignores the taint flag completely
# :warn adds a warning, but does not interrupt the rendering
# :error raises an error when tainted output is used
attr_writer :taint_mode
def file_system def file_system
@@file_system @@file_system
end end
@@ -80,6 +88,10 @@ module Liquid
@error_mode || :lax @error_mode || :lax
end end
def taint_mode
@taint_mode || :lax
end
# Pass a module with filter methods which should be available # Pass a module with filter methods which should be available
# to all liquid views. Good for registering the standard library # to all liquid views. Good for registering the standard library
def register_filter(mod) def register_filter(mod)
@@ -100,7 +112,7 @@ module Liquid
end end
def initialize def initialize
@resource_limits = self.class.default_resource_limits.dup @resource_limits = ResourceLimits.new(self.class.default_resource_limits)
end end
# Parse source code. # Parse source code.
@@ -193,6 +205,9 @@ module Liquid
context.add_filters(args.pop) context.add_filters(args.pop)
end end
# Retrying a render resets resource usage
context.resource_limits.reset
begin begin
# render the nodelist. # render the nodelist.
# for performance reasons we get an array back here. join will make a string out of it. # for performance reasons we get an array back here. join will make a string out of it.
@@ -231,15 +246,24 @@ module Liquid
return raw_tokens unless @line_numbers return raw_tokens unless @line_numbers
current_line = 1 current_line = 1
current_column = 1
raw_tokens.map do |token| raw_tokens.map do |token|
Token.new(token, current_line).tap do Token.new(token, current_line, current_column).tap do
current_line += token.count("\n") new_line_count = token.count("\n")
if new_line_count > 0
current_line += new_line_count
current_column = token.size - token.rindex("\n") + 1
else
current_column += token.size
end
end end
end end
end end
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

@@ -1,10 +1,11 @@
module Liquid module Liquid
class Token < String class Token < String
attr_reader :line_number attr_reader :line_number, :column_number
def initialize(content, line_number) def initialize(content, line_number, column_number=nil)
super(content) super(content)
@line_number = line_number @line_number = line_number
@column_number = column_number
end end
def raw def raw

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

View File

@@ -64,5 +64,15 @@ module Liquid
object object
end end
def ==(other)
self.class == other.class && self.state == other.state
end
protected
def state
[@name, @lookups, @command_flags]
end
end end
end end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
@@ -100,4 +103,17 @@ class ParsingQuirksTest < Minitest::Test
end end
end end
def test_invalid_variables_work
with_error_mode(:lax) do
assert_template_result('bar', "{% assign 123foo = 'bar' %}{{ 123foo }}")
assert_template_result('123', "{% assign 123 = 'bar' %}{{ 123 }}")
end
end
def test_extra_dots_in_ranges
with_error_mode(:lax) do
assert_template_result('12345', "{% for i in (1...5) %}{{ i }}{% endfor %}")
end
end
end # ParsingQuirksTest end # ParsingQuirksTest

View File

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

View File

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

View File

@@ -27,6 +27,9 @@ class TestFileSystem
when "pick_a_source" when "pick_a_source"
"from TestFileSystem" "from TestFileSystem"
when 'assignments'
"{% assign foo = 'bar' %}"
else else
template_path template_path
end end
@@ -108,6 +111,10 @@ class IncludeTagTest < Minitest::Test
'echo1' => 'test123', 'more_echos' => { "echo2" => 'test321'} 'echo1' => 'test123', 'more_echos' => { "echo2" => 'test321'}
end end
def test_included_templates_assigns_variables
assert_template_result "bar", "{% include 'assignments' %}{{ foo }}"
end
def test_nested_include_tag def test_nested_include_tag
assert_template_result "body body_detail", "{% include 'body' %}" assert_template_result "body body_detail", "{% include 'body' %}"

View File

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

View File

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

View File

@@ -5,6 +5,7 @@ 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']
@@ -57,6 +58,14 @@ module Minitest
Liquid::Strainer.class_variable_set(:@@filters, original_filters) Liquid::Strainer.class_variable_set(:@@filters, original_filters)
end end
def with_taint_mode(mode)
old_mode = Liquid::Template.taint_mode
Liquid::Template.taint_mode = mode
yield
ensure
Liquid::Template.taint_mode = old_mode
end
def with_error_mode(mode) def with_error_mode(mode)
old_mode = Liquid::Template.error_mode old_mode = Liquid::Template.error_mode
Liquid::Template.error_mode = mode Liquid::Template.error_mode = mode

View File

@@ -4,110 +4,111 @@ class ConditionUnitTest < Minitest::Test
include Liquid include Liquid
def test_basic_condition def test_basic_condition
assert_equal false, Condition.new('1', '==', '2').evaluate assert_equal false, Condition.new(1, '==', 2).evaluate
assert_equal true, Condition.new('1', '==', '1').evaluate assert_equal true, Condition.new(1, '==', 1).evaluate
end end
def test_default_operators_evalute_true def test_default_operators_evalute_true
assert_evalutes_true '1', '==', '1' assert_evalutes_true 1, '==', 1
assert_evalutes_true '1', '!=', '2' assert_evalutes_true 1, '!=', 2
assert_evalutes_true '1', '<>', '2' assert_evalutes_true 1, '<>', 2
assert_evalutes_true '1', '<', '2' assert_evalutes_true 1, '<', 2
assert_evalutes_true '2', '>', '1' assert_evalutes_true 2, '>', 1
assert_evalutes_true '1', '>=', '1' assert_evalutes_true 1, '>=', 1
assert_evalutes_true '2', '>=', '1' assert_evalutes_true 2, '>=', 1
assert_evalutes_true '1', '<=', '2' assert_evalutes_true 1, '<=', 2
assert_evalutes_true '1', '<=', '1' assert_evalutes_true 1, '<=', 1
# negative numbers # negative numbers
assert_evalutes_true '1', '>', '-1' assert_evalutes_true 1, '>', -1
assert_evalutes_true '-1', '<', '1' assert_evalutes_true -1, '<', 1
assert_evalutes_true '1.0', '>', '-1.0' assert_evalutes_true 1.0, '>', -1.0
assert_evalutes_true '-1.0', '<', '1.0' assert_evalutes_true -1.0, '<', 1.0
end end
def test_default_operators_evalute_false def test_default_operators_evalute_false
assert_evalutes_false '1', '==', '2' assert_evalutes_false 1, '==', 2
assert_evalutes_false '1', '!=', '1' assert_evalutes_false 1, '!=', 1
assert_evalutes_false '1', '<>', '1' assert_evalutes_false 1, '<>', 1
assert_evalutes_false '1', '<', '0' assert_evalutes_false 1, '<', 0
assert_evalutes_false '2', '>', '4' assert_evalutes_false 2, '>', 4
assert_evalutes_false '1', '>=', '3' assert_evalutes_false 1, '>=', 3
assert_evalutes_false '2', '>=', '4' assert_evalutes_false 2, '>=', 4
assert_evalutes_false '1', '<=', '0' assert_evalutes_false 1, '<=', 0
assert_evalutes_false '1', '<=', '0' assert_evalutes_false 1, '<=', 0
end end
def test_contains_works_on_strings def test_contains_works_on_strings
assert_evalutes_true "'bob'", 'contains', "'o'" assert_evalutes_true 'bob', 'contains', 'o'
assert_evalutes_true "'bob'", 'contains', "'b'" assert_evalutes_true 'bob', 'contains', 'b'
assert_evalutes_true "'bob'", 'contains', "'bo'" assert_evalutes_true 'bob', 'contains', 'bo'
assert_evalutes_true "'bob'", 'contains', "'ob'" assert_evalutes_true 'bob', 'contains', 'ob'
assert_evalutes_true "'bob'", 'contains', "'bob'" assert_evalutes_true 'bob', 'contains', 'bob'
assert_evalutes_false "'bob'", 'contains', "'bob2'" assert_evalutes_false 'bob', 'contains', 'bob2'
assert_evalutes_false "'bob'", 'contains', "'a'" assert_evalutes_false 'bob', 'contains', 'a'
assert_evalutes_false "'bob'", 'contains', "'---'" assert_evalutes_false 'bob', 'contains', '---'
end end
def test_invalid_comparation_operator def test_invalid_comparation_operator
assert_evaluates_argument_error "1", '~~', '0' assert_evaluates_argument_error 1, '~~', 0
end end
def test_comparation_of_int_and_str def test_comparation_of_int_and_str
assert_evaluates_argument_error "'1'", '>', '0' assert_evaluates_argument_error '1', '>', 0
assert_evaluates_argument_error "'1'", '<', '0' assert_evaluates_argument_error '1', '<', 0
assert_evaluates_argument_error "'1'", '>=', '0' assert_evaluates_argument_error '1', '>=', 0
assert_evaluates_argument_error "'1'", '<=', '0' assert_evaluates_argument_error '1', '<=', 0
end end
def test_contains_works_on_arrays def test_contains_works_on_arrays
@context = Liquid::Context.new @context = Liquid::Context.new
@context['array'] = [1,2,3,4,5] @context['array'] = [1,2,3,4,5]
array_expr = VariableLookup.new("array")
assert_evalutes_false "array", 'contains', '0' assert_evalutes_false array_expr, 'contains', 0
assert_evalutes_true "array", 'contains', '1' assert_evalutes_true array_expr, 'contains', 1
assert_evalutes_true "array", 'contains', '2' assert_evalutes_true array_expr, 'contains', 2
assert_evalutes_true "array", 'contains', '3' assert_evalutes_true array_expr, 'contains', 3
assert_evalutes_true "array", 'contains', '4' assert_evalutes_true array_expr, 'contains', 4
assert_evalutes_true "array", 'contains', '5' assert_evalutes_true array_expr, 'contains', 5
assert_evalutes_false "array", 'contains', '6' assert_evalutes_false array_expr, 'contains', 6
assert_evalutes_false "array", 'contains', '"1"' assert_evalutes_false array_expr, 'contains', "1"
end end
def test_contains_returns_false_for_nil_operands def test_contains_returns_false_for_nil_operands
@context = Liquid::Context.new @context = Liquid::Context.new
assert_evalutes_false "not_assigned", 'contains', '0' assert_evalutes_false VariableLookup.new('not_assigned'), 'contains', '0'
assert_evalutes_false "0", 'contains', 'not_assigned' assert_evalutes_false 0, 'contains', VariableLookup.new('not_assigned')
end end
def test_contains_return_false_on_wrong_data_type def test_contains_return_false_on_wrong_data_type
assert_evalutes_false "1", 'contains', '0' assert_evalutes_false 1, 'contains', 0
end end
def test_or_condition def test_or_condition
condition = Condition.new('1', '==', '2') condition = Condition.new(1, '==', 2)
assert_equal false, condition.evaluate assert_equal false, condition.evaluate
condition.or Condition.new('2', '==', '1') condition.or Condition.new(2, '==', 1)
assert_equal false, condition.evaluate assert_equal false, condition.evaluate
condition.or Condition.new('1', '==', '1') condition.or Condition.new(1, '==', 1)
assert_equal true, condition.evaluate assert_equal true, condition.evaluate
end end
def test_and_condition def test_and_condition
condition = Condition.new('1', '==', '1') condition = Condition.new(1, '==', 1)
assert_equal true, condition.evaluate assert_equal true, condition.evaluate
condition.and Condition.new('2', '==', '2') condition.and Condition.new(2, '==', 2)
assert_equal true, condition.evaluate assert_equal true, condition.evaluate
condition.and Condition.new('2', '==', '1') condition.and Condition.new(2, '==', 1)
assert_equal false, condition.evaluate assert_equal false, condition.evaluate
end end
@@ -115,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

@@ -469,16 +469,6 @@ class ContextUnitTest < Minitest::Test
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

@@ -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

@@ -57,7 +57,8 @@ class StrainerUnitTest < Minitest::Test
end end
def test_strainer_uses_a_class_cache_to_avoid_method_cache_invalidation def test_strainer_uses_a_class_cache_to_avoid_method_cache_invalidation
a, b = Module.new, Module.new a = Module.new
b = Module.new
strainer = Strainer.create(nil, [a,b]) strainer = Strainer.create(nil, [a,b])
assert_kind_of Strainer, strainer assert_kind_of Strainer, strainer
assert_kind_of a, strainer assert_kind_of a, strainer

View File

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

View File

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

View File

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

View File

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

View File

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