liquid 2.6.3 → 3.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
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