liquid 2.6.3 → 3.0.0.rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +42 -13
  3. data/README.md +27 -2
  4. data/lib/liquid.rb +11 -11
  5. data/lib/liquid/block.rb +75 -45
  6. data/lib/liquid/condition.rb +15 -11
  7. data/lib/liquid/context.rb +68 -29
  8. data/lib/liquid/document.rb +3 -3
  9. data/lib/liquid/drop.rb +17 -1
  10. data/lib/liquid/file_system.rb +17 -6
  11. data/lib/liquid/i18n.rb +39 -0
  12. data/lib/liquid/interrupts.rb +1 -1
  13. data/lib/liquid/lexer.rb +51 -0
  14. data/lib/liquid/locales/en.yml +22 -0
  15. data/lib/liquid/parser.rb +90 -0
  16. data/lib/liquid/standardfilters.rb +115 -52
  17. data/lib/liquid/strainer.rb +14 -4
  18. data/lib/liquid/tag.rb +42 -7
  19. data/lib/liquid/tags/assign.rb +10 -8
  20. data/lib/liquid/tags/break.rb +1 -1
  21. data/lib/liquid/tags/capture.rb +10 -8
  22. data/lib/liquid/tags/case.rb +13 -13
  23. data/lib/liquid/tags/comment.rb +9 -2
  24. data/lib/liquid/tags/continue.rb +1 -4
  25. data/lib/liquid/tags/cycle.rb +5 -7
  26. data/lib/liquid/tags/decrement.rb +3 -4
  27. data/lib/liquid/tags/for.rb +69 -36
  28. data/lib/liquid/tags/if.rb +52 -25
  29. data/lib/liquid/tags/ifchanged.rb +2 -2
  30. data/lib/liquid/tags/include.rb +8 -7
  31. data/lib/liquid/tags/increment.rb +4 -8
  32. data/lib/liquid/tags/raw.rb +3 -3
  33. data/lib/liquid/tags/table_row.rb +73 -0
  34. data/lib/liquid/tags/unless.rb +2 -4
  35. data/lib/liquid/template.rb +69 -10
  36. data/lib/liquid/utils.rb +13 -4
  37. data/lib/liquid/variable.rb +59 -8
  38. data/lib/liquid/version.rb +1 -1
  39. data/test/fixtures/en_locale.yml +9 -0
  40. data/test/{liquid → integration}/assign_test.rb +6 -0
  41. data/test/integration/blank_test.rb +106 -0
  42. data/test/{liquid → integration}/capture_test.rb +2 -2
  43. data/test/integration/context_test.rb +33 -0
  44. data/test/integration/drop_test.rb +245 -0
  45. data/test/{liquid → integration}/error_handling_test.rb +31 -2
  46. data/test/{liquid → integration}/filter_test.rb +7 -7
  47. data/test/{liquid → integration}/hash_ordering_test.rb +0 -0
  48. data/test/{liquid → integration}/output_test.rb +12 -12
  49. data/test/integration/parsing_quirks_test.rb +94 -0
  50. data/test/{liquid → integration}/security_test.rb +9 -9
  51. data/test/{liquid → integration}/standard_filter_test.rb +103 -33
  52. data/test/{liquid → integration}/tags/break_tag_test.rb +0 -0
  53. data/test/{liquid → integration}/tags/continue_tag_test.rb +0 -0
  54. data/test/{liquid → integration}/tags/for_tag_test.rb +78 -0
  55. data/test/{liquid → integration}/tags/if_else_tag_test.rb +1 -1
  56. data/test/integration/tags/include_tag_test.rb +212 -0
  57. data/test/{liquid → integration}/tags/increment_tag_test.rb +0 -0
  58. data/test/{liquid → integration}/tags/raw_tag_test.rb +1 -0
  59. data/test/{liquid → integration}/tags/standard_tag_test.rb +24 -22
  60. data/test/integration/tags/statements_test.rb +113 -0
  61. data/test/{liquid/tags/html_tag_test.rb → integration/tags/table_row_test.rb} +5 -5
  62. data/test/{liquid → integration}/tags/unless_else_tag_test.rb +0 -0
  63. data/test/{liquid → integration}/template_test.rb +66 -42
  64. data/test/integration/variable_test.rb +72 -0
  65. data/test/test_helper.rb +32 -7
  66. data/test/{liquid/block_test.rb → unit/block_unit_test.rb} +1 -1
  67. data/test/{liquid/condition_test.rb → unit/condition_unit_test.rb} +19 -1
  68. data/test/{liquid/context_test.rb → unit/context_unit_test.rb} +27 -19
  69. data/test/{liquid/file_system_test.rb → unit/file_system_unit_test.rb} +7 -1
  70. data/test/unit/i18n_unit_test.rb +37 -0
  71. data/test/unit/lexer_unit_test.rb +48 -0
  72. data/test/{liquid/module_ex_test.rb → unit/module_ex_unit_test.rb} +7 -7
  73. data/test/unit/parser_unit_test.rb +82 -0
  74. data/test/{liquid/regexp_test.rb → unit/regexp_unit_test.rb} +3 -3
  75. data/test/{liquid/strainer_test.rb → unit/strainer_unit_test.rb} +19 -1
  76. data/test/unit/tag_unit_test.rb +11 -0
  77. data/test/unit/tags/case_tag_unit_test.rb +10 -0
  78. data/test/unit/tags/for_tag_unit_test.rb +13 -0
  79. data/test/unit/tags/if_tag_unit_test.rb +8 -0
  80. data/test/unit/template_unit_test.rb +69 -0
  81. data/test/unit/tokenizer_unit_test.rb +29 -0
  82. data/test/{liquid/variable_test.rb → unit/variable_unit_test.rb} +17 -67
  83. metadata +117 -73
  84. data/lib/extras/liquid_view.rb +0 -51
  85. data/lib/liquid/htmltags.rb +0 -73
  86. data/test/liquid/drop_test.rb +0 -180
  87. data/test/liquid/parsing_quirks_test.rb +0 -52
  88. data/test/liquid/tags/include_tag_test.rb +0 -166
  89. data/test/liquid/tags/statements_test.rb +0 -134
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 556c422717dbe398a9c414ac93d2d7fa6cf824d9
4
- data.tar.gz: 1b63c67741c2e9c1bb0fa61c0e84856ecd17a6a7
3
+ metadata.gz: f98aa57f8713db177b94a4cd217b6c5f40234bc5
4
+ data.tar.gz: 50a9905b550fcedf21315f4764360f104d0964bb
5
5
  SHA512:
6
- metadata.gz: fefb1fb53e150321193372e6f75dac1b860454a214a16a3cdb526e13d524b00761d50450b986b0ddab7297d253e2db46025e3a23144362b188a974834adfd08d
7
- data.tar.gz: 8e1449c8d9923df9ca9ea64c52848725106d84b39e16311fee5c7a8e006f9b8c78f663efa2574f15d1f32c6bfe7acb1001ac79881fe87435dbd273f5ea7eb213
6
+ metadata.gz: 7c9c6d8d006ef94568651f21dba6d4619cef50c575510e74159602140263a10f20148b4c278c17aa77221690e7dedb547aedf4af5fc866b1c3ab4644243b8d68
7
+ data.tar.gz: ff19880456ff443065ed50371ad4223fedcf350494564ba69e51c9c2c4e06281ffc54773e9f0cbae60fbee95a60be4514f13924b0d831db43a41fcabadab1a8c
data/History.md CHANGED
@@ -1,17 +1,38 @@
1
1
  # Liquid Version History
2
2
 
3
- IMPORTANT: Liquid 2.6 is going to be the last version of Liquid which maintains explicit Ruby 1.8 compatability.
4
- The following releases will only be tested against Ruby 1.9 and Ruby 2.0 and are likely to break on Ruby 1.8.
5
-
6
- ## 2.6.3 / 2015-07-23 / branch "2-6-stable"
7
-
8
- * Fix test failure under certain timezones [Dylan Thacker-Smith]
9
-
10
- ## 2.6.2 / 2015-01-23
3
+ ## 3.0.0 / not yet released / branch "master"
11
4
 
12
- * Remove duplicate hash key [Parker Moore]
13
-
14
- ## 2.6.1 / 2014-01-10
5
+ * ...
6
+ * Add exception_handler feature, see #397 and #254 [Bogdan Gusiev, bogdan and Florian Weingarten, fw42]
7
+ * Optimize variable parsing to avoid repeated regex evaluation during template rendering #383 [Jason Hiltz-Laforge, jasonhl]
8
+ * Optimize checking for block interrupts to reduce object allocation #380 [Jason Hiltz-Laforge, jasonhl]
9
+ * Properly set context rethrow_errors on render! #349 [Thierry Joyal, tjoyal]
10
+ * Fix broken rendering of variables which are equal to false, see #345 [Florian Weingarten, fw42]
11
+ * Remove ActionView template handler [Dylan Thacker-Smith, dylanahsmith]
12
+ * Freeze lots of string literals for new Ruby 2.1 optimization, see #297 [Florian Weingarten, fw42]
13
+ * Allow newlines in tags and variables, see #324 [Dylan Thacker-Smith, dylanahsmith]
14
+ * Tag#parse is called after initialize, which now takes options instead of tokens as the 3rd argument. See #321 [Dylan Thacker-Smith, dylanahsmith]
15
+ * Raise `Liquid::ArgumentError` instead of `::ArgumentError` when filter has wrong number of arguments #309 [Bogdan Gusiev, bogdan]
16
+ * Add a to_s default for liquid drops, see #306 [Adam Doeler, releod]
17
+ * Add strip, lstrip, and rstrip to standard filters [Florian Weingarten, fw42]
18
+ * Make if, for & case tags return complete and consistent nodelists, see #250 [Nick Jones, dntj]
19
+ * Prevent arbitrary method invocation on condition objects, see #274 [Dylan Thacker-Smith, dylanahsmith]
20
+ * Don't call to_sym when creating conditions for security reasons, see #273 [Bouke van der Bijl, bouk]
21
+ * Fix resource counting bug with respond_to?(:length), see #263 [Florian Weingarten, fw42]
22
+ * Allow specifying custom patterns for template filenames, see #284 [Andrei Gladkyi, agladkyi]
23
+ * Allow drops to optimize loading a slice of elements, see #282 [Tom Burns, boourns]
24
+ * Support for passing variables to snippets in subdirs, see #271 [Joost Hietbrink, joost]
25
+ * Add a class cache to avoid runtime extend calls, see #249 [James Tucker, raggi]
26
+ * Remove some legacy Ruby 1.8 compatibility code, see #276 [Florian Weingarten, fw42]
27
+ * Add default filter to standard filters, see #267 [Derrick Reimer, djreimer]
28
+ * Add optional strict parsing and warn parsing, see #235 [Tristan Hume, trishume]
29
+ * Add I18n syntax error translation, see #241 [Simon Hørup Eskildsen, Sirupsen]
30
+ * Make sort filter work on enumerable drops, see #239 [Florian Weingarten, fw42]
31
+ * Fix clashing method names in enumerable drops, see #238 [Florian Weingarten, fw42]
32
+ * Make map filter work on enumerable drops, see #233 [Florian Weingarten, fw42]
33
+ * Improved whitespace stripping for blank blocks, related to #216 [Florian Weingarten, fw42]
34
+
35
+ ## 2.6.1 / 2014-01-10 / branch "2-6-stable"
15
36
 
16
37
  Security fix, cherry-picked from master (4e14a65):
17
38
  * Don't call to_sym when creating conditions for security reasons, see #273 [Bouke van der Bijl, bouk]
@@ -19,7 +40,9 @@ Security fix, cherry-picked from master (4e14a65):
19
40
 
20
41
  ## 2.6.0 / 2013-11-25
21
42
 
22
- * ...
43
+ IMPORTANT: Liquid 2.6 is going to be the last version of Liquid which maintains explicit Ruby 1.8 compatability.
44
+ The following releases will only be tested against Ruby 1.9 and Ruby 2.0 and are likely to break on Ruby 1.8.
45
+
23
46
  * Bugfix for #106: fix example servlet [gnowoel]
24
47
  * Bugfix for #97: strip_html filter supports multi-line tags [Jo Liss, joliss]
25
48
  * Bugfix for #114: strip_html filter supports style tags [James Allardice, jamesallardice]
@@ -39,7 +62,13 @@ Security fix, cherry-picked from master (4e14a65):
39
62
  * Better documentation for 'include' tag (closes #163) [Peter Schröder, phoet]
40
63
  * Use of BigDecimal on filters to have better precision (closes #155) [Arthur Nogueira Neves, arthurnn]
41
64
 
42
- ## 2.5.4 / 2013-11-11 / branch "2.5-stable"
65
+ ## 2.5.5 / 2014-01-10 / branch "2-5-stable"
66
+
67
+ Security fix, cherry-picked from master (4e14a65):
68
+ * Don't call to_sym when creating conditions for security reasons, see #273 [Bouke van der Bijl, bouk]
69
+ * Prevent arbitrary method invocation on condition objects, see #274 [Dylan Thacker-Smith, dylanahsmith]
70
+
71
+ ## 2.5.4 / 2013-11-11
43
72
 
44
73
  * Fix "can't convert Fixnum into String" for "replace", see #173, [wǒ_is神仙, jsw0528]
45
74
 
data/README.md CHANGED
@@ -1,9 +1,12 @@
1
+ [![Build Status](https://secure.travis-ci.org/Shopify/liquid.png?branch=master)](http://travis-ci.org/Shopify/liquid)
2
+ [![Inline docs](http://inch-ci.org/github/Shopify/liquid.png)](http://inch-ci.org/github/Shopify/liquid)
3
+
1
4
  # Liquid template engine
2
5
 
3
6
  * [Contributing guidelines](CONTRIBUTING.md)
4
7
  * [Version history](History.md)
5
8
  * [Liquid documentation from Shopify](http://docs.shopify.com/themes/liquid-basics)
6
- * [Liquid Wiki from Shopify](http://wiki.shopify.com/Liquid)
9
+ * [Liquid Wiki at GitHub](https://github.com/Shopify/liquid/wiki)
7
10
  * [Website](http://liquidmarkup.org/)
8
11
 
9
12
  ## Introduction
@@ -47,4 +50,26 @@ For standard use you can just pass it the content of a file and call render with
47
50
  @template.render('name' => 'tobi') # => "hi tobi"
48
51
  ```
49
52
 
50
- [![Build Status](https://secure.travis-ci.org/Shopify/liquid.png)](http://travis-ci.org/Shopify/liquid)
53
+ ### Error Modes
54
+
55
+ Setting the error mode of Liquid lets you specify how strictly you want your templates to be interpreted.
56
+ Normally the parser is very lax and will accept almost anything without error. Unfortunately this can make
57
+ it very hard to debug and can lead to unexpected behaviour.
58
+
59
+ Liquid also comes with a stricter parser that can be used when editing templates to give better error messages
60
+ when templates are invalid. You can enable this new parser like this:
61
+
62
+ ```ruby
63
+ Liquid::Template.error_mode = :strict # Raises a SyntaxError when invalid syntax is used
64
+ Liquid::Template.error_mode = :warn # Adds errors to template.errors but continues as normal
65
+ Liquid::Template.error_mode = :lax # The default mode, accepts almost anything.
66
+ ```
67
+
68
+ If you want to set the error mode only on specific templates you can pass `:error_mode` as an option to `parse`:
69
+ ```ruby
70
+ Liquid::Template.parse(source, :error_mode => :strict)
71
+ ```
72
+ This is useful for doing things like enabling strict mode only in the theme editor.
73
+
74
+ It is recommended that you enable `:strict` or `:warn` mode on new apps to stop invalid templates from being created.
75
+ It is also recommended that you use it in the template editors of existing apps to give editors better error messages.
data/lib/liquid.rb CHANGED
@@ -21,9 +21,9 @@
21
21
 
22
22
  module Liquid
23
23
  FilterSeparator = /\|/
24
- ArgumentSeparator = ','
25
- FilterArgumentSeparator = ':'
26
- VariableAttributeSeparator = '.'
24
+ ArgumentSeparator = ','.freeze
25
+ FilterArgumentSeparator = ':'.freeze
26
+ VariableAttributeSeparator = '.'.freeze
27
27
  TagStart = /\{\%/
28
28
  TagEnd = /\%\}/
29
29
  VariableSignature = /\(?[\w\-\.\[\]]\)?/
@@ -33,19 +33,20 @@ module Liquid
33
33
  VariableIncompleteEnd = /\}\}?/
34
34
  QuotedString = /"[^"]*"|'[^']*'/
35
35
  QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o
36
- StrictQuotedFragment = /"[^"]+"|'[^']+'|[^\s|:,]+/
37
- FirstFilterArgument = /#{FilterArgumentSeparator}(?:#{StrictQuotedFragment})/o
38
- OtherFilterArgument = /#{ArgumentSeparator}(?:#{StrictQuotedFragment})/o
39
- SpacelessFilter = /^(?:'[^']+'|"[^"]+"|[^'"])*#{FilterSeparator}(?:#{StrictQuotedFragment})(?:#{FirstFilterArgument}(?:#{OtherFilterArgument})*)?/o
40
- Expression = /(?:#{QuotedFragment}(?:#{SpacelessFilter})*)/o
41
36
  TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/o
42
37
  AnyStartingTag = /\{\{|\{\%/
43
- PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/o
44
- TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/o
38
+ PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om
39
+ TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om
45
40
  VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o
41
+
42
+ singleton_class.send(:attr_accessor, :cache_classes)
43
+ self.cache_classes = true
46
44
  end
47
45
 
48
46
  require "liquid/version"
47
+ require 'liquid/lexer'
48
+ require 'liquid/parser'
49
+ require 'liquid/i18n'
49
50
  require 'liquid/drop'
50
51
  require 'liquid/extensions'
51
52
  require 'liquid/errors'
@@ -58,7 +59,6 @@ require 'liquid/document'
58
59
  require 'liquid/variable'
59
60
  require 'liquid/file_system'
60
61
  require 'liquid/template'
61
- require 'liquid/htmltags'
62
62
  require 'liquid/standardfilters'
63
63
  require 'liquid/condition'
64
64
  require 'liquid/module_ex'
data/lib/liquid/block.rb CHANGED
@@ -1,45 +1,58 @@
1
1
  module Liquid
2
-
3
2
  class Block < Tag
4
- IsTag = /^#{TagStart}/o
5
- IsVariable = /^#{VariableStart}/o
6
- FullToken = /^#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}$/o
7
- ContentOfVariable = /^#{VariableStart}(.*)#{VariableEnd}$/o
3
+ FullToken = /\A#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
4
+ ContentOfVariable = /\A#{VariableStart}(.*)#{VariableEnd}\z/om
5
+ TAGSTART = "{%".freeze
6
+ VARSTART = "{{".freeze
7
+
8
+ def blank?
9
+ @blank
10
+ end
8
11
 
9
12
  def parse(tokens)
13
+ @blank = true
10
14
  @nodelist ||= []
11
15
  @nodelist.clear
12
16
 
13
- while token = tokens.shift
14
-
15
- case token
16
- when IsTag
17
- if token =~ FullToken
17
+ # All child tags of the current block.
18
+ @children = []
18
19
 
19
- # if we found the proper block delimiter just end parsing here and let the outer block
20
- # proceed
21
- if block_delimiter == $1
22
- end_tag
23
- return
24
- end
25
-
26
- # fetch the tag from registered blocks
27
- if tag = Template.tags[$1]
28
- @nodelist << tag.new($1, $2, tokens)
20
+ while token = tokens.shift
21
+ unless token.empty?
22
+ case
23
+ when token.start_with?(TAGSTART)
24
+ if token =~ FullToken
25
+
26
+ # if we found the proper block delimiter just end parsing here and let the outer block
27
+ # proceed
28
+ if block_delimiter == $1
29
+ end_tag
30
+ return
31
+ end
32
+
33
+ # fetch the tag from registered blocks
34
+ if tag = Template.tags[$1]
35
+ new_tag = tag.parse($1, $2, tokens, @options)
36
+ @blank &&= new_tag.blank?
37
+ @nodelist << new_tag
38
+ @children << new_tag
39
+ else
40
+ # this tag is not registered with the system
41
+ # pass it to the current block for special handling or error reporting
42
+ unknown_tag($1, $2, tokens)
43
+ end
29
44
  else
30
- # this tag is not registered with the system
31
- # pass it to the current block for special handling or error reporting
32
- unknown_tag($1, $2, tokens)
45
+ raise SyntaxError.new(options[:locale].t("errors.syntax.tag_termination".freeze, :token => token, :tag_end => TagEnd.inspect))
33
46
  end
47
+ when token.start_with?(VARSTART)
48
+ new_var = create_variable(token)
49
+ @nodelist << new_var
50
+ @children << new_var
51
+ @blank = false
34
52
  else
35
- raise SyntaxError, "Tag '#{token}' was not properly terminated with regexp: #{TagEnd.inspect} "
53
+ @nodelist << token
54
+ @blank &&= (token =~ /\A\s*\z/)
36
55
  end
37
- when IsVariable
38
- @nodelist << create_variable(token)
39
- when ''
40
- # pass
41
- else
42
- @nodelist << token
43
56
  end
44
57
  end
45
58
 
@@ -49,33 +62,48 @@ module Liquid
49
62
  assert_missing_delimitation!
50
63
  end
51
64
 
65
+ # warnings of this block and all sub-tags
66
+ def warnings
67
+ all_warnings = []
68
+ all_warnings.concat(@warnings) if @warnings
69
+
70
+ (@children || []).each do |node|
71
+ all_warnings.concat(node.warnings || [])
72
+ end
73
+
74
+ all_warnings
75
+ end
76
+
52
77
  def end_tag
53
78
  end
54
79
 
55
80
  def unknown_tag(tag, params, tokens)
56
81
  case tag
57
- when 'else'
58
- raise SyntaxError, "#{block_name} tag does not expect else tag"
59
- when 'end'
60
- raise SyntaxError, "'end' is not a valid delimiter for #{block_name} tags. use #{block_delimiter}"
82
+ when 'else'.freeze
83
+ raise SyntaxError.new(options[:locale].t("errors.syntax.unexpected_else".freeze,
84
+ :block_name => block_name))
85
+ when 'end'.freeze
86
+ raise SyntaxError.new(options[:locale].t("errors.syntax.invalid_delimiter".freeze,
87
+ :block_name => block_name,
88
+ :block_delimiter => block_delimiter))
61
89
  else
62
- raise SyntaxError, "Unknown tag '#{tag}'"
90
+ raise SyntaxError.new(options[:locale].t("errors.syntax.unknown_tag".freeze, :tag => tag))
63
91
  end
64
92
  end
65
93
 
66
- def block_delimiter
67
- "end#{block_name}"
68
- end
69
-
70
94
  def block_name
71
95
  @tag_name
72
96
  end
73
97
 
98
+ def block_delimiter
99
+ @block_delimiter ||= "end#{block_name}"
100
+ end
101
+
74
102
  def create_variable(token)
75
103
  token.scan(ContentOfVariable) do |content|
76
- return Variable.new(content.first)
104
+ return Variable.new(content.first, @options)
77
105
  end
78
- raise SyntaxError.new("Variable '#{token}' was not properly terminated with regexp: #{VariableEnd.inspect} ")
106
+ raise SyntaxError.new(options[:locale].t("errors.syntax.variable_termination".freeze, :token => token, :tag_end => VariableEnd.inspect))
79
107
  end
80
108
 
81
109
  def render(context)
@@ -85,7 +113,7 @@ module Liquid
85
113
  protected
86
114
 
87
115
  def assert_missing_delimitation!
88
- raise SyntaxError.new("#{block_name} tag was never closed")
116
+ raise SyntaxError.new(options[:locale].t("errors.syntax.tag_never_closed".freeze, :block_name => block_name))
89
117
  end
90
118
 
91
119
  def render_all(list, context)
@@ -107,12 +135,14 @@ module Liquid
107
135
  end
108
136
 
109
137
  token_output = (token.respond_to?(:render) ? token.render(context) : token)
110
- context.resource_limits[:render_length_current] += (token_output.respond_to?(:length) ? token_output.length : 1)
138
+ context.increment_used_resources(:render_length_current, token_output)
111
139
  if context.resource_limits_reached?
112
140
  context.resource_limits[:reached] = true
113
- raise MemoryError.new("Memory limits exceeded")
141
+ raise MemoryError.new("Memory limits exceeded".freeze)
142
+ end
143
+ unless token.is_a?(Block) && token.blank?
144
+ output << token_output
114
145
  end
115
- output << token_output
116
146
  rescue MemoryError => e
117
147
  raise e
118
148
  rescue ::StandardError => e
@@ -8,14 +8,14 @@ module Liquid
8
8
  #
9
9
  class Condition #:nodoc:
10
10
  @@operators = {
11
- '==' => lambda { |cond, left, right| cond.send(:equal_variables, left, right) },
12
- '!=' => lambda { |cond, left, right| !cond.send(:equal_variables, left, right) },
13
- '<>' => lambda { |cond, left, right| !cond.send(:equal_variables, left, right) },
14
- '<' => :<,
15
- '>' => :>,
16
- '>=' => :>=,
17
- '<=' => :<=,
18
- 'contains' => lambda { |cond, left, right| left && right ? left.include?(right) : false }
11
+ '=='.freeze => lambda { |cond, left, right| cond.send(:equal_variables, left, right) },
12
+ '!='.freeze => lambda { |cond, left, right| !cond.send(:equal_variables, left, right) },
13
+ '<>'.freeze => lambda { |cond, left, right| !cond.send(:equal_variables, left, right) },
14
+ '<'.freeze => :<,
15
+ '>'.freeze => :>,
16
+ '>='.freeze => :>=,
17
+ '<='.freeze => :<=,
18
+ 'contains'.freeze => lambda { |cond, left, right| left && right ? left.include?(right) : false }
19
19
  }
20
20
 
21
21
  def self.operators
@@ -61,7 +61,7 @@ module Liquid
61
61
  end
62
62
 
63
63
  def inspect
64
- "#<Condition #{[@left, @operator, @right].compact.join(' ')}>"
64
+ "#<Condition #{[@left, @operator, @right].compact.join(' '.freeze)}>"
65
65
  end
66
66
 
67
67
  private
@@ -94,12 +94,16 @@ module Liquid
94
94
 
95
95
  left, right = context[left], context[right]
96
96
 
97
- operation = self.class.operators[op] || raise(ArgumentError.new("Unknown operator #{op}"))
97
+ operation = self.class.operators[op] || raise(Liquid::ArgumentError.new("Unknown operator #{op}"))
98
98
 
99
99
  if operation.respond_to?(:call)
100
100
  operation.call(self, left, right)
101
101
  elsif left.respond_to?(operation) and right.respond_to?(operation)
102
- left.send(operation, right)
102
+ begin
103
+ left.send(operation, right)
104
+ rescue ::ArgumentError => e
105
+ raise Liquid::ArgumentError.new(e.message)
106
+ end
103
107
  else
104
108
  nil
105
109
  end
@@ -14,17 +14,33 @@ module Liquid
14
14
  # context['bob'] #=> nil class Context
15
15
  class Context
16
16
  attr_reader :scopes, :errors, :registers, :environments, :resource_limits
17
+ attr_accessor :exception_handler
18
+
19
+ SQUARE_BRACKETED = /\A\[(.*)\]\z/m
17
20
 
18
21
  def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = {})
19
22
  @environments = [environments].flatten
20
23
  @scopes = [(outer_scope || {})]
21
24
  @registers = registers
22
25
  @errors = []
23
- @rethrow_errors = rethrow_errors
24
26
  @resource_limits = (resource_limits || {}).merge!({ :render_score_current => 0, :assign_score_current => 0 })
25
27
  squash_instance_assigns_with_environments
26
28
 
29
+ if rethrow_errors
30
+ self.exception_handler = ->(e) { true }
31
+ end
32
+
27
33
  @interrupts = []
34
+ @filters = []
35
+ @parsed_variables = Hash.new{ |cache, markup| cache[markup] = variable_parse(markup) }
36
+ end
37
+
38
+ def increment_used_resources(key, obj)
39
+ @resource_limits[key] += if obj.kind_of?(String) || obj.kind_of?(Array) || obj.kind_of?(Hash)
40
+ obj.length
41
+ else
42
+ 1
43
+ end
28
44
  end
29
45
 
30
46
  def resource_limits_reached?
@@ -34,7 +50,7 @@ module Liquid
34
50
  end
35
51
 
36
52
  def strainer
37
- @strainer ||= Strainer.create(self)
53
+ @strainer ||= Strainer.create(self, @filters)
38
54
  end
39
55
 
40
56
  # Adds filters to this context.
@@ -43,17 +59,26 @@ module Liquid
43
59
  # for that
44
60
  def add_filters(filters)
45
61
  filters = [filters].flatten.compact
46
-
47
62
  filters.each do |f|
48
63
  raise ArgumentError, "Expected module but got: #{f.class}" unless f.is_a?(Module)
49
64
  Strainer.add_known_filter(f)
50
- strainer.extend(f)
65
+ end
66
+
67
+ # If strainer is already setup then there's no choice but to use a runtime
68
+ # extend call. If strainer is not yet created, we can utilize strainers
69
+ # cached class based API, which avoids busting the method cache.
70
+ if @strainer
71
+ filters.each do |f|
72
+ strainer.extend(f)
73
+ end
74
+ else
75
+ @filters.concat filters
51
76
  end
52
77
  end
53
78
 
54
79
  # are there any not handled interrupts?
55
80
  def has_interrupt?
56
- @interrupts.any?
81
+ !@interrupts.empty?
57
82
  end
58
83
 
59
84
  # push an interrupt to the stack. this interrupt is considered not handled.
@@ -68,7 +93,8 @@ module Liquid
68
93
 
69
94
  def handle_error(e)
70
95
  errors.push(e)
71
- raise if @rethrow_errors
96
+
97
+ raise if exception_handler && exception_handler.call(e)
72
98
 
73
99
  case e
74
100
  when SyntaxError
@@ -85,7 +111,7 @@ module Liquid
85
111
  # Push new local scope on the stack. use <tt>Context#stack</tt> instead
86
112
  def push(new_scope={})
87
113
  @scopes.unshift(new_scope)
88
- raise StackLevelError, "Nesting too deep" if @scopes.length > 100
114
+ raise StackLevelError, "Nesting too deep".freeze if @scopes.length > 100
89
115
  end
90
116
 
91
117
  # Merge a hash of variables in the current local scope
@@ -133,11 +159,11 @@ module Liquid
133
159
 
134
160
  private
135
161
  LITERALS = {
136
- nil => nil, 'nil' => nil, 'null' => nil, '' => nil,
137
- 'true' => true,
138
- 'false' => false,
139
- 'blank' => :blank?,
140
- 'empty' => :empty?
162
+ nil => nil, 'nil'.freeze => nil, 'null'.freeze => nil, ''.freeze => nil,
163
+ 'true'.freeze => true,
164
+ 'false'.freeze => false,
165
+ 'blank'.freeze => :blank?,
166
+ 'empty'.freeze => :empty?
141
167
  }
142
168
 
143
169
  # Look up variable, either resolve directly after considering the name. We can directly handle
@@ -153,15 +179,15 @@ module Liquid
153
179
  LITERALS[key]
154
180
  else
155
181
  case key
156
- when /^'(.*)'$/ # Single quoted strings
182
+ when /\A'(.*)'\z/m # Single quoted strings
157
183
  $1
158
- when /^"(.*)"$/ # Double quoted strings
184
+ when /\A"(.*)"\z/m # Double quoted strings
159
185
  $1
160
- when /^(-?\d+)$/ # Integer and floats
186
+ when /\A(-?\d+)\z/ # Integer and floats
161
187
  $1.to_i
162
- when /^\((\S+)\.\.(\S+)\)$/ # Ranges
188
+ when /\A\((\S+)\.\.(\S+)\)\z/ # Ranges
163
189
  (resolve($1).to_i..resolve($2).to_i)
164
- when /^(-?\d[\d\.]+)$/ # Floats
190
+ when /\A(-?\d[\d\.]+)\z/ # Floats
165
191
  $1.to_f
166
192
  else
167
193
  variable(key)
@@ -171,12 +197,18 @@ module Liquid
171
197
 
172
198
  # Fetches an object starting at the local scope and then moving up the hierachy
173
199
  def find_variable(key)
174
- scope = @scopes.find { |s| s.has_key?(key) }
200
+
201
+ # This was changed from find() to find_index() because this is a very hot
202
+ # path and find_index() is optimized in MRI to reduce object allocation
203
+ index = @scopes.find_index { |s| s.has_key?(key) }
204
+ scope = @scopes[index] if index
205
+
175
206
  variable = nil
176
207
 
177
208
  if scope.nil?
178
209
  @environments.each do |e|
179
- if variable = lookup_and_evaluate(e, key)
210
+ variable = lookup_and_evaluate(e, key)
211
+ unless variable.nil?
180
212
  scope = e
181
213
  break
182
214
  end
@@ -192,6 +224,16 @@ module Liquid
192
224
  return variable
193
225
  end
194
226
 
227
+ def variable_parse(markup)
228
+ parts = markup.scan(VariableParser)
229
+ needs_resolution = false
230
+ if parts.first =~ SQUARE_BRACKETED
231
+ needs_resolution = true
232
+ parts[0] = $1
233
+ end
234
+ {:first => parts.shift, :needs_resolution => needs_resolution, :rest => parts}
235
+ end
236
+
195
237
  # Resolves namespaced queries gracefully.
196
238
  #
197
239
  # Example
@@ -199,19 +241,17 @@ module Liquid
199
241
  # assert_equal 'tobi', @context['hash.name']
200
242
  # assert_equal 'tobi', @context['hash["name"]']
201
243
  def variable(markup)
202
- parts = markup.scan(VariableParser)
203
- square_bracketed = /^\[(.*)\]$/
244
+ parts = @parsed_variables[markup]
204
245
 
205
- first_part = parts.shift
206
-
207
- if first_part =~ square_bracketed
208
- first_part = resolve($1)
246
+ first_part = parts[:first]
247
+ if parts[:needs_resolution]
248
+ first_part = resolve(parts[:first])
209
249
  end
210
250
 
211
251
  if object = find_variable(first_part)
212
252
 
213
- parts.each do |part|
214
- part = resolve($1) if part_resolved = (part =~ square_bracketed)
253
+ parts[:rest].each do |part|
254
+ part = resolve($1) if part_resolved = (part =~ SQUARE_BRACKETED)
215
255
 
216
256
  # If object is a hash- or array-like object we look for the
217
257
  # presence of the key and if its available we return it
@@ -226,7 +266,7 @@ module Liquid
226
266
  # Some special cases. If the part wasn't in square brackets and
227
267
  # no key with the same name was found we interpret following calls
228
268
  # as commands and call them on the current object
229
- elsif !part_resolved and object.respond_to?(part) and ['size', 'first', 'last'].include?(part)
269
+ elsif !part_resolved and object.respond_to?(part) and ['size'.freeze, 'first'.freeze, 'last'.freeze].include?(part)
230
270
 
231
271
  object = object.send(part.intern).to_liquid
232
272
 
@@ -263,5 +303,4 @@ module Liquid
263
303
  end
264
304
  end # squash_instance_assigns_with_environments
265
305
  end # Context
266
-
267
306
  end # Liquid