liquid 4.0.3 → 5.1.0

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 (125) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +54 -0
  3. data/README.md +6 -0
  4. data/lib/liquid/block.rb +31 -14
  5. data/lib/liquid/block_body.rb +166 -54
  6. data/lib/liquid/condition.rb +41 -20
  7. data/lib/liquid/context.rb +107 -52
  8. data/lib/liquid/document.rb +47 -9
  9. data/lib/liquid/drop.rb +4 -2
  10. data/lib/liquid/errors.rb +20 -18
  11. data/lib/liquid/expression.rb +29 -34
  12. data/lib/liquid/extensions.rb +2 -0
  13. data/lib/liquid/file_system.rb +6 -4
  14. data/lib/liquid/forloop_drop.rb +11 -4
  15. data/lib/liquid/i18n.rb +5 -3
  16. data/lib/liquid/interrupts.rb +3 -1
  17. data/lib/liquid/lexer.rb +30 -23
  18. data/lib/liquid/locales/en.yml +3 -1
  19. data/lib/liquid/parse_context.rb +20 -4
  20. data/lib/liquid/parse_tree_visitor.rb +2 -2
  21. data/lib/liquid/parser.rb +30 -18
  22. data/lib/liquid/parser_switching.rb +17 -3
  23. data/lib/liquid/partial_cache.rb +24 -0
  24. data/lib/liquid/profiler/hooks.rb +26 -14
  25. data/lib/liquid/profiler.rb +67 -86
  26. data/lib/liquid/range_lookup.rb +13 -3
  27. data/lib/liquid/register.rb +6 -0
  28. data/lib/liquid/resource_limits.rb +47 -8
  29. data/lib/liquid/standardfilters.rb +95 -46
  30. data/lib/liquid/static_registers.rb +44 -0
  31. data/lib/liquid/strainer_factory.rb +36 -0
  32. data/lib/liquid/strainer_template.rb +53 -0
  33. data/lib/liquid/tablerowloop_drop.rb +6 -4
  34. data/lib/liquid/tag/disableable.rb +22 -0
  35. data/lib/liquid/tag/disabler.rb +21 -0
  36. data/lib/liquid/tag.rb +28 -6
  37. data/lib/liquid/tags/assign.rb +24 -10
  38. data/lib/liquid/tags/break.rb +8 -3
  39. data/lib/liquid/tags/capture.rb +11 -8
  40. data/lib/liquid/tags/case.rb +40 -27
  41. data/lib/liquid/tags/comment.rb +5 -3
  42. data/lib/liquid/tags/continue.rb +8 -3
  43. data/lib/liquid/tags/cycle.rb +25 -14
  44. data/lib/liquid/tags/decrement.rb +6 -3
  45. data/lib/liquid/tags/echo.rb +34 -0
  46. data/lib/liquid/tags/for.rb +68 -44
  47. data/lib/liquid/tags/if.rb +39 -23
  48. data/lib/liquid/tags/ifchanged.rb +11 -10
  49. data/lib/liquid/tags/include.rb +34 -47
  50. data/lib/liquid/tags/increment.rb +7 -3
  51. data/lib/liquid/tags/raw.rb +14 -11
  52. data/lib/liquid/tags/render.rb +84 -0
  53. data/lib/liquid/tags/table_row.rb +23 -19
  54. data/lib/liquid/tags/unless.rb +23 -15
  55. data/lib/liquid/template.rb +53 -72
  56. data/lib/liquid/template_factory.rb +9 -0
  57. data/lib/liquid/tokenizer.rb +18 -10
  58. data/lib/liquid/usage.rb +8 -0
  59. data/lib/liquid/utils.rb +13 -3
  60. data/lib/liquid/variable.rb +46 -41
  61. data/lib/liquid/variable_lookup.rb +11 -6
  62. data/lib/liquid/version.rb +2 -1
  63. data/lib/liquid.rb +17 -5
  64. data/test/integration/assign_test.rb +74 -5
  65. data/test/integration/blank_test.rb +11 -8
  66. data/test/integration/block_test.rb +47 -1
  67. data/test/integration/capture_test.rb +18 -10
  68. data/test/integration/context_test.rb +609 -5
  69. data/test/integration/document_test.rb +4 -2
  70. data/test/integration/drop_test.rb +67 -83
  71. data/test/integration/error_handling_test.rb +73 -61
  72. data/test/integration/expression_test.rb +46 -0
  73. data/test/integration/filter_test.rb +53 -42
  74. data/test/integration/hash_ordering_test.rb +5 -3
  75. data/test/integration/output_test.rb +26 -24
  76. data/test/integration/parsing_quirks_test.rb +19 -7
  77. data/test/integration/{render_profiling_test.rb → profiler_test.rb} +84 -25
  78. data/test/integration/security_test.rb +30 -21
  79. data/test/integration/standard_filter_test.rb +385 -281
  80. data/test/integration/tag/disableable_test.rb +59 -0
  81. data/test/integration/tag_test.rb +45 -0
  82. data/test/integration/tags/break_tag_test.rb +4 -2
  83. data/test/integration/tags/continue_tag_test.rb +4 -2
  84. data/test/integration/tags/echo_test.rb +13 -0
  85. data/test/integration/tags/for_tag_test.rb +107 -51
  86. data/test/integration/tags/if_else_tag_test.rb +5 -3
  87. data/test/integration/tags/include_tag_test.rb +70 -54
  88. data/test/integration/tags/increment_tag_test.rb +4 -2
  89. data/test/integration/tags/liquid_tag_test.rb +116 -0
  90. data/test/integration/tags/raw_tag_test.rb +14 -11
  91. data/test/integration/tags/render_tag_test.rb +213 -0
  92. data/test/integration/tags/standard_tag_test.rb +38 -31
  93. data/test/integration/tags/statements_test.rb +23 -21
  94. data/test/integration/tags/table_row_test.rb +2 -0
  95. data/test/integration/tags/unless_else_tag_test.rb +4 -2
  96. data/test/integration/template_test.rb +132 -124
  97. data/test/integration/trim_mode_test.rb +78 -44
  98. data/test/integration/variable_test.rb +74 -32
  99. data/test/test_helper.rb +113 -22
  100. data/test/unit/block_unit_test.rb +19 -24
  101. data/test/unit/condition_unit_test.rb +79 -77
  102. data/test/unit/file_system_unit_test.rb +6 -4
  103. data/test/unit/i18n_unit_test.rb +7 -5
  104. data/test/unit/lexer_unit_test.rb +11 -9
  105. data/test/{integration → unit}/parse_tree_visitor_test.rb +16 -2
  106. data/test/unit/parser_unit_test.rb +37 -35
  107. data/test/unit/partial_cache_unit_test.rb +128 -0
  108. data/test/unit/regexp_unit_test.rb +17 -15
  109. data/test/unit/static_registers_unit_test.rb +156 -0
  110. data/test/unit/strainer_factory_unit_test.rb +100 -0
  111. data/test/unit/strainer_template_unit_test.rb +82 -0
  112. data/test/unit/tag_unit_test.rb +5 -3
  113. data/test/unit/tags/case_tag_unit_test.rb +3 -1
  114. data/test/unit/tags/for_tag_unit_test.rb +4 -2
  115. data/test/unit/tags/if_tag_unit_test.rb +3 -1
  116. data/test/unit/template_factory_unit_test.rb +12 -0
  117. data/test/unit/template_unit_test.rb +19 -10
  118. data/test/unit/tokenizer_unit_test.rb +26 -19
  119. data/test/unit/variable_unit_test.rb +51 -49
  120. metadata +76 -50
  121. data/lib/liquid/strainer.rb +0 -66
  122. data/lib/liquid/truffle.rb +0 -5
  123. data/test/truffle/truffle_test.rb +0 -9
  124. data/test/unit/context_unit_test.rb +0 -489
  125. data/test/unit/strainer_unit_test.rb +0 -164
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  # Context keeps the variable stack and resolves variables, as well as keywords
3
5
  #
@@ -12,37 +14,49 @@ module Liquid
12
14
  #
13
15
  # context['bob'] #=> nil class Context
14
16
  class Context
15
- attr_reader :scopes, :errors, :registers, :environments, :resource_limits
17
+ attr_reader :scopes, :errors, :registers, :environments, :resource_limits, :static_registers, :static_environments
16
18
  attr_accessor :exception_renderer, :template_name, :partial, :global_filter, :strict_variables, :strict_filters
17
19
 
18
- def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil)
19
- @environments = [environments].flatten
20
- @scopes = [(outer_scope || {})]
21
- @registers = registers
22
- @errors = []
23
- @partial = false
24
- @strict_variables = false
25
- @resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
26
- squash_instance_assigns_with_environments
20
+ # rubocop:disable Metrics/ParameterLists
21
+ def self.build(environments: {}, outer_scope: {}, registers: {}, rethrow_errors: false, resource_limits: nil, static_environments: {}, &block)
22
+ new(environments, outer_scope, registers, rethrow_errors, resource_limits, static_environments, &block)
23
+ end
27
24
 
28
- @this_stack_used = false
25
+ def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil, static_environments = {})
26
+ @environments = [environments]
27
+ @environments.flatten!
28
+
29
+ @static_environments = [static_environments].flat_map(&:freeze).freeze
30
+ @scopes = [(outer_scope || {})]
31
+ @registers = registers
32
+ @errors = []
33
+ @partial = false
34
+ @strict_variables = false
35
+ @resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
36
+ @base_scope_depth = 0
37
+ @interrupts = []
38
+ @filters = []
39
+ @global_filter = nil
40
+ @disabled_tags = {}
29
41
 
30
42
  self.exception_renderer = Template.default_exception_renderer
31
43
  if rethrow_errors
32
- self.exception_renderer = ->(e) { raise }
44
+ self.exception_renderer = Liquid::RAISE_EXCEPTION_LAMBDA
33
45
  end
34
46
 
35
- @interrupts = []
36
- @filters = []
37
- @global_filter = nil
47
+ yield self if block_given?
48
+
49
+ # Do this last, since it could result in this object being passed to a Proc in the environment
50
+ squash_instance_assigns_with_environments
38
51
  end
52
+ # rubocop:enable Metrics/ParameterLists
39
53
 
40
54
  def warnings
41
55
  @warnings ||= []
42
56
  end
43
57
 
44
58
  def strainer
45
- @strainer ||= Strainer.create(self, @filters)
59
+ @strainer ||= StrainerFactory.create(self, @filters)
46
60
  end
47
61
 
48
62
  # Adds filters to this context.
@@ -77,7 +91,7 @@ module Liquid
77
91
  def handle_error(e, line_number = nil)
78
92
  e = internal_error unless e.is_a?(Liquid::Error)
79
93
  e.template_name ||= template_name
80
- e.line_number ||= line_number
94
+ e.line_number ||= line_number
81
95
  errors.push(e)
82
96
  exception_renderer.call(e).to_s
83
97
  end
@@ -89,7 +103,7 @@ module Liquid
89
103
  # Push new local scope on the stack. use <tt>Context#stack</tt> instead
90
104
  def push(new_scope = {})
91
105
  @scopes.unshift(new_scope)
92
- raise StackLevelError, "Nesting too deep".freeze if @scopes.length > Block::MAX_DEPTH
106
+ check_overflow
93
107
  end
94
108
 
95
109
  # Merge a hash of variables in the current local scope
@@ -110,20 +124,32 @@ module Liquid
110
124
  # context['var'] = 'hi'
111
125
  # end
112
126
  #
113
- # context['var] #=> nil
114
- def stack(new_scope = nil)
115
- old_stack_used = @this_stack_used
116
- if new_scope
117
- push(new_scope)
118
- @this_stack_used = true
119
- else
120
- @this_stack_used = false
121
- end
122
-
127
+ # context['var'] #=> nil
128
+ def stack(new_scope = {})
129
+ push(new_scope)
123
130
  yield
124
131
  ensure
125
- pop if @this_stack_used
126
- @this_stack_used = old_stack_used
132
+ pop
133
+ end
134
+
135
+ # Creates a new context inheriting resource limits, filters, environment etc.,
136
+ # but with an isolated scope.
137
+ def new_isolated_subcontext
138
+ check_overflow
139
+
140
+ self.class.build(
141
+ resource_limits: resource_limits,
142
+ static_environments: static_environments,
143
+ registers: StaticRegisters.new(registers)
144
+ ).tap do |subcontext|
145
+ subcontext.base_scope_depth = base_scope_depth + 1
146
+ subcontext.exception_renderer = exception_renderer
147
+ subcontext.filters = @filters
148
+ subcontext.strainer = nil
149
+ subcontext.errors = errors
150
+ subcontext.warnings = warnings
151
+ subcontext.disabled_tags = @disabled_tags
152
+ end
127
153
  end
128
154
 
129
155
  def clear_instance_assigns
@@ -132,10 +158,6 @@ module Liquid
132
158
 
133
159
  # Only allow String, Numeric, Hash, Array, Proc, Boolean or <tt>Liquid::Drop</tt>
134
160
  def []=(key, value)
135
- unless @this_stack_used
136
- @this_stack_used = true
137
- push({})
138
- end
139
161
  @scopes[0][key] = value
140
162
  end
141
163
 
@@ -164,26 +186,14 @@ module Liquid
164
186
  # This was changed from find() to find_index() because this is a very hot
165
187
  # path and find_index() is optimized in MRI to reduce object allocation
166
188
  index = @scopes.find_index { |s| s.key?(key) }
167
- scope = @scopes[index] if index
168
-
169
- variable = nil
170
189
 
171
- if scope.nil?
172
- @environments.each do |e|
173
- variable = lookup_and_evaluate(e, key, raise_on_not_found: raise_on_not_found)
174
- # When lookup returned a value OR there is no value but the lookup also did not raise
175
- # then it is the value we are looking for.
176
- if !variable.nil? || @strict_variables && raise_on_not_found
177
- scope = e
178
- break
179
- end
180
- end
190
+ variable = if index
191
+ lookup_and_evaluate(@scopes[index], key, raise_on_not_found: raise_on_not_found)
192
+ else
193
+ try_variable_find_in_environments(key, raise_on_not_found: raise_on_not_found)
181
194
  end
182
195
 
183
- scope ||= @environments.last || @scopes.last
184
- variable ||= lookup_and_evaluate(scope, key, raise_on_not_found: raise_on_not_found)
185
-
186
- variable = variable.to_liquid
196
+ variable = variable.to_liquid
187
197
  variable.context = self if variable.respond_to?(:context=)
188
198
 
189
199
  variable
@@ -197,14 +207,59 @@ module Liquid
197
207
  value = obj[key]
198
208
 
199
209
  if value.is_a?(Proc) && obj.respond_to?(:[]=)
200
- obj[key] = (value.arity == 0) ? value.call : value.call(self)
210
+ obj[key] = value.arity == 0 ? value.call : value.call(self)
201
211
  else
202
212
  value
203
213
  end
204
214
  end
205
215
 
216
+ def with_disabled_tags(tag_names)
217
+ tag_names.each do |name|
218
+ @disabled_tags[name] = @disabled_tags.fetch(name, 0) + 1
219
+ end
220
+ yield
221
+ ensure
222
+ tag_names.each do |name|
223
+ @disabled_tags[name] -= 1
224
+ end
225
+ end
226
+
227
+ def tag_disabled?(tag_name)
228
+ @disabled_tags.fetch(tag_name, 0) > 0
229
+ end
230
+
231
+ protected
232
+
233
+ attr_writer :base_scope_depth, :warnings, :errors, :strainer, :filters, :disabled_tags
234
+
206
235
  private
207
236
 
237
+ attr_reader :base_scope_depth
238
+
239
+ def try_variable_find_in_environments(key, raise_on_not_found:)
240
+ @environments.each do |environment|
241
+ found_variable = lookup_and_evaluate(environment, key, raise_on_not_found: raise_on_not_found)
242
+ if !found_variable.nil? || @strict_variables && raise_on_not_found
243
+ return found_variable
244
+ end
245
+ end
246
+ @static_environments.each do |environment|
247
+ found_variable = lookup_and_evaluate(environment, key, raise_on_not_found: raise_on_not_found)
248
+ if !found_variable.nil? || @strict_variables && raise_on_not_found
249
+ return found_variable
250
+ end
251
+ end
252
+ nil
253
+ end
254
+
255
+ def check_overflow
256
+ raise StackLevelError, "Nesting too deep" if overflow?
257
+ end
258
+
259
+ def overflow?
260
+ base_scope_depth + @scopes.length > Block::MAX_DEPTH
261
+ end
262
+
208
263
  def internal_error
209
264
  # raise and catch to set backtrace and cause on exception
210
265
  raise Liquid::InternalError, 'internal'
@@ -1,26 +1,64 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
- class Document < BlockBody
4
+ class Document
3
5
  def self.parse(tokens, parse_context)
4
- doc = new
6
+ doc = new(parse_context)
5
7
  doc.parse(tokens, parse_context)
6
8
  doc
7
9
  end
8
10
 
9
- def parse(tokens, parse_context)
10
- super do |end_tag_name, end_tag_params|
11
- unknown_tag(end_tag_name, parse_context) if end_tag_name
11
+ attr_reader :parse_context, :body
12
+
13
+ def initialize(parse_context)
14
+ @parse_context = parse_context
15
+ @body = new_body
16
+ end
17
+
18
+ def nodelist
19
+ @body.nodelist
20
+ end
21
+
22
+ def parse(tokenizer, parse_context)
23
+ while parse_body(tokenizer)
12
24
  end
25
+ @body.freeze
13
26
  rescue SyntaxError => e
14
27
  e.line_number ||= parse_context.line_number
15
28
  raise
16
29
  end
17
30
 
18
- def unknown_tag(tag, parse_context)
31
+ def unknown_tag(tag, _markup, _tokenizer)
19
32
  case tag
20
- when 'else'.freeze, 'end'.freeze
21
- raise SyntaxError.new(parse_context.locale.t("errors.syntax.unexpected_outer_tag".freeze, tag: tag))
33
+ when 'else', 'end'
34
+ raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_outer_tag", tag: tag)
22
35
  else
23
- raise SyntaxError.new(parse_context.locale.t("errors.syntax.unknown_tag".freeze, tag: tag))
36
+ raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag", tag: tag)
37
+ end
38
+ end
39
+
40
+ def render_to_output_buffer(context, output)
41
+ @body.render_to_output_buffer(context, output)
42
+ end
43
+
44
+ def render(context)
45
+ render_to_output_buffer(context, +'')
46
+ end
47
+
48
+ private
49
+
50
+ def new_body
51
+ parse_context.new_block_body
52
+ end
53
+
54
+ def parse_body(tokenizer)
55
+ @body.parse(tokenizer, parse_context) do |unknown_tag_name, unknown_tag_markup|
56
+ if unknown_tag_name
57
+ unknown_tag(unknown_tag_name, unknown_tag_markup, tokenizer)
58
+ true
59
+ else
60
+ false
61
+ end
24
62
  end
25
63
  end
26
64
  end
data/lib/liquid/drop.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'set'
2
4
 
3
5
  module Liquid
@@ -25,7 +27,7 @@ module Liquid
25
27
 
26
28
  # Catch all for the method
27
29
  def liquid_method_missing(method)
28
- return nil unless @context && @context.strict_variables
30
+ return nil unless @context&.strict_variables
29
31
  raise Liquid::UndefinedDropMethod, "undefined method #{method}"
30
32
  end
31
33
 
@@ -67,7 +69,7 @@ module Liquid
67
69
 
68
70
  if include?(Enumerable)
69
71
  blacklist += Enumerable.public_instance_methods
70
- blacklist -= [:sort, :count, :first, :min, :max, :include?]
72
+ blacklist -= [:sort, :count, :first, :min, :max]
71
73
  end
72
74
 
73
75
  whitelist = [:to_liquid] + (public_instance_methods - blacklist)
data/lib/liquid/errors.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  class Error < ::StandardError
3
5
  attr_accessor :line_number
@@ -5,7 +7,7 @@ module Liquid
5
7
  attr_accessor :markup_context
6
8
 
7
9
  def to_s(with_prefix = true)
8
- str = ""
10
+ str = +""
9
11
  str << message_prefix if with_prefix
10
12
  str << super()
11
13
 
@@ -20,11 +22,11 @@ module Liquid
20
22
  private
21
23
 
22
24
  def message_prefix
23
- str = ""
24
- if is_a?(SyntaxError)
25
- str << "Liquid syntax error"
25
+ str = +""
26
+ str << if is_a?(SyntaxError)
27
+ "Liquid syntax error"
26
28
  else
27
- str << "Liquid error"
29
+ "Liquid error"
28
30
  end
29
31
 
30
32
  if line_number
@@ -38,19 +40,19 @@ module Liquid
38
40
  end
39
41
  end
40
42
 
41
- ArgumentError = Class.new(Error)
42
- ContextError = Class.new(Error)
43
- FileSystemError = Class.new(Error)
44
- StandardError = Class.new(Error)
45
- SyntaxError = Class.new(Error)
46
- StackLevelError = Class.new(Error)
47
- TaintedError = Class.new(Error)
48
- MemoryError = Class.new(Error)
49
- ZeroDivisionError = Class.new(Error)
50
- FloatDomainError = Class.new(Error)
51
- UndefinedVariable = Class.new(Error)
43
+ ArgumentError = Class.new(Error)
44
+ ContextError = Class.new(Error)
45
+ FileSystemError = Class.new(Error)
46
+ StandardError = Class.new(Error)
47
+ SyntaxError = Class.new(Error)
48
+ StackLevelError = Class.new(Error)
49
+ MemoryError = Class.new(Error)
50
+ ZeroDivisionError = Class.new(Error)
51
+ FloatDomainError = Class.new(Error)
52
+ UndefinedVariable = Class.new(Error)
52
53
  UndefinedDropMethod = Class.new(Error)
53
- UndefinedFilter = Class.new(Error)
54
+ UndefinedFilter = Class.new(Error)
54
55
  MethodOverrideError = Class.new(Error)
55
- InternalError = Class.new(Error)
56
+ DisabledError = Class.new(Error)
57
+ InternalError = Class.new(Error)
56
58
  end
@@ -1,45 +1,40 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  class Expression
3
- class MethodLiteral
4
- attr_reader :method_name, :to_s
5
-
6
- def initialize(method_name, to_s)
7
- @method_name = method_name
8
- @to_s = to_s
9
- end
10
-
11
- def to_liquid
12
- to_s
13
- end
14
- end
15
-
16
5
  LITERALS = {
17
- nil => nil, 'nil'.freeze => nil, 'null'.freeze => nil, ''.freeze => nil,
18
- 'true'.freeze => true,
19
- 'false'.freeze => false,
20
- 'blank'.freeze => MethodLiteral.new(:blank?, '').freeze,
21
- 'empty'.freeze => MethodLiteral.new(:empty?, '').freeze
6
+ nil => nil, 'nil' => nil, 'null' => nil, '' => nil,
7
+ 'true' => true,
8
+ 'false' => false,
9
+ 'blank' => '',
10
+ 'empty' => ''
22
11
  }.freeze
23
12
 
24
- SINGLE_QUOTED_STRING = /\A'(.*)'\z/m
25
- DOUBLE_QUOTED_STRING = /\A"(.*)"\z/m
26
- INTEGERS_REGEX = /\A(-?\d+)\z/
27
- FLOATS_REGEX = /\A(-?\d[\d\.]+)\z/
28
- RANGES_REGEX = /\A\((\S+)\.\.(\S+)\)\z/
13
+ SINGLE_QUOTED_STRING = /\A\s*'(.*)'\s*\z/m
14
+ DOUBLE_QUOTED_STRING = /\A\s*"(.*)"\s*\z/m
15
+ INTEGERS_REGEX = /\A\s*(-?\d+)\s*\z/
16
+ FLOATS_REGEX = /\A\s*(-?\d[\d\.]+)\s*\z/
17
+
18
+ # Use an atomic group (?>...) to avoid pathological backtracing from
19
+ # malicious input as described in https://github.com/Shopify/liquid/issues/1357
20
+ RANGES_REGEX = /\A\s*\(\s*(?>(\S+)\s*\.\.)\s*(\S+)\s*\)\s*\z/
29
21
 
30
22
  def self.parse(markup)
31
- if LITERALS.key?(markup)
32
- LITERALS[markup]
23
+ case markup
24
+ when nil
25
+ nil
26
+ when SINGLE_QUOTED_STRING, DOUBLE_QUOTED_STRING
27
+ Regexp.last_match(1)
28
+ when INTEGERS_REGEX
29
+ Regexp.last_match(1).to_i
30
+ when RANGES_REGEX
31
+ RangeLookup.parse(Regexp.last_match(1), Regexp.last_match(2))
32
+ when FLOATS_REGEX
33
+ Regexp.last_match(1).to_f
33
34
  else
34
- case markup
35
- when SINGLE_QUOTED_STRING, DOUBLE_QUOTED_STRING
36
- $1
37
- when INTEGERS_REGEX
38
- $1.to_i
39
- when RANGES_REGEX
40
- RangeLookup.parse($1, $2)
41
- when FLOATS_REGEX
42
- $1.to_f
35
+ markup = markup.strip
36
+ if LITERALS.key?(markup)
37
+ LITERALS[markup]
43
38
  else
44
39
  VariableLookup.parse(markup)
45
40
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'time'
2
4
  require 'date'
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  # A Liquid file system is a way to let your templates retrieve other templates for use with the include tag.
3
5
  #
@@ -44,8 +46,8 @@ module Liquid
44
46
  class LocalFileSystem
45
47
  attr_accessor :root
46
48
 
47
- def initialize(root, pattern = "_%s.liquid".freeze)
48
- @root = root
49
+ def initialize(root, pattern = "_%s.liquid")
50
+ @root = root
49
51
  @pattern = pattern
50
52
  end
51
53
 
@@ -57,9 +59,9 @@ module Liquid
57
59
  end
58
60
 
59
61
  def full_path(template_path)
60
- raise FileSystemError, "Illegal template name '#{template_path}'" unless template_path =~ /\A[^.\/][a-zA-Z0-9_\/]+\z/
62
+ raise FileSystemError, "Illegal template name '#{template_path}'" unless %r{\A[^./][a-zA-Z0-9_/]+\z}.match?(template_path)
61
63
 
62
- full_path = if template_path.include?('/'.freeze)
64
+ full_path = if template_path.include?('/')
63
65
  File.join(root, File.dirname(template_path), @pattern % File.basename(template_path))
64
66
  else
65
67
  File.join(root, @pattern % template_path)
@@ -1,13 +1,20 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  class ForloopDrop < Drop
3
5
  def initialize(name, length, parentloop)
4
- @name = name
5
- @length = length
6
+ @name = name
7
+ @length = length
6
8
  @parentloop = parentloop
7
- @index = 0
9
+ @index = 0
8
10
  end
9
11
 
10
- attr_reader :name, :length, :parentloop
12
+ attr_reader :length, :parentloop
13
+
14
+ def name
15
+ Usage.increment('forloop_drop_name')
16
+ @name
17
+ end
11
18
 
12
19
  def index
13
20
  @index + 1
data/lib/liquid/i18n.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'yaml'
2
4
 
3
5
  module Liquid
@@ -26,13 +28,13 @@ module Liquid
26
28
  def interpolate(name, vars)
27
29
  name.gsub(/%\{(\w+)\}/) do
28
30
  # raise TranslationError, "Undefined key #{$1} for interpolation in translation #{name}" unless vars[$1.to_sym]
29
- (vars[$1.to_sym]).to_s
31
+ (vars[Regexp.last_match(1).to_sym]).to_s
30
32
  end
31
33
  end
32
34
 
33
35
  def deep_fetch_translation(name)
34
- name.split('.'.freeze).reduce(locale) do |level, cur|
35
- level[cur] or raise TranslationError, "Translation for #{name} does not exist in locale #{path}"
36
+ name.split('.').reduce(locale) do |level, cur|
37
+ level[cur] || raise(TranslationError, "Translation for #{name} does not exist in locale #{path}")
36
38
  end
37
39
  end
38
40
  end
@@ -1,10 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  # An interrupt is any command that breaks processing of a block (ex: a for loop).
3
5
  class Interrupt
4
6
  attr_reader :message
5
7
 
6
8
  def initialize(message = nil)
7
- @message = message || "interrupt".freeze
9
+ @message = message || "interrupt"
8
10
  end
9
11
  end
10
12
 
data/lib/liquid/lexer.rb CHANGED
@@ -1,24 +1,26 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "strscan"
2
4
  module Liquid
3
5
  class Lexer
4
6
  SPECIALS = {
5
- '|'.freeze => :pipe,
6
- '.'.freeze => :dot,
7
- ':'.freeze => :colon,
8
- ','.freeze => :comma,
9
- '['.freeze => :open_square,
10
- ']'.freeze => :close_square,
11
- '('.freeze => :open_round,
12
- ')'.freeze => :close_round,
13
- '?'.freeze => :question,
14
- '-'.freeze => :dash
7
+ '|' => :pipe,
8
+ '.' => :dot,
9
+ ':' => :colon,
10
+ ',' => :comma,
11
+ '[' => :open_square,
12
+ ']' => :close_square,
13
+ '(' => :open_round,
14
+ ')' => :close_round,
15
+ '?' => :question,
16
+ '-' => :dash,
15
17
  }.freeze
16
- IDENTIFIER = /[a-zA-Z_][\w-]*\??/
18
+ IDENTIFIER = /[a-zA-Z_][\w-]*\??/
17
19
  SINGLE_STRING_LITERAL = /'[^\']*'/
18
20
  DOUBLE_STRING_LITERAL = /"[^\"]*"/
19
- NUMBER_LITERAL = /-?\d+(\.\d+)?/
20
- DOTDOT = /\.\./
21
- COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/
21
+ NUMBER_LITERAL = /-?\d+(\.\d+)?/
22
+ DOTDOT = /\.\./
23
+ COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/
22
24
  WHITESPACE_OR_NOTHING = /\s*/
23
25
 
24
26
  def initialize(input)
@@ -31,16 +33,21 @@ module Liquid
31
33
  until @ss.eos?
32
34
  @ss.skip(WHITESPACE_OR_NOTHING)
33
35
  break if @ss.eos?
34
- tok = case
35
- when t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t]
36
- when t = @ss.scan(SINGLE_STRING_LITERAL) then [:string, t]
37
- when t = @ss.scan(DOUBLE_STRING_LITERAL) then [:string, t]
38
- when t = @ss.scan(NUMBER_LITERAL) then [:number, t]
39
- when t = @ss.scan(IDENTIFIER) then [:id, t]
40
- when t = @ss.scan(DOTDOT) then [:dotdot, t]
36
+ tok = if (t = @ss.scan(COMPARISON_OPERATOR))
37
+ [:comparison, t]
38
+ elsif (t = @ss.scan(SINGLE_STRING_LITERAL))
39
+ [:string, t]
40
+ elsif (t = @ss.scan(DOUBLE_STRING_LITERAL))
41
+ [:string, t]
42
+ elsif (t = @ss.scan(NUMBER_LITERAL))
43
+ [:number, t]
44
+ elsif (t = @ss.scan(IDENTIFIER))
45
+ [:id, t]
46
+ elsif (t = @ss.scan(DOTDOT))
47
+ [:dotdot, t]
41
48
  else
42
- c = @ss.getch
43
- if s = SPECIALS[c]
49
+ c = @ss.getch
50
+ if (s = SPECIALS[c])
44
51
  [s, c]
45
52
  else
46
53
  raise SyntaxError, "Unexpected character #{c}"
@@ -20,7 +20,9 @@
20
20
  tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"
21
21
  variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}"
22
22
  tag_never_closed: "'%{block_name}' tag was never closed"
23
- meta_syntax_error: "Liquid syntax error: #{e.message}"
24
23
  table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
24
+ render: "Syntax error in tag 'render' - Template name must be a quoted string"
25
25
  argument:
26
26
  include: "Argument error in tag 'include' - Illegal template name"
27
+ disabled:
28
+ tag: "usage is not allowed in this context"