liquid 4.0.3 → 5.4.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 (115) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +89 -0
  3. data/README.md +10 -4
  4. data/lib/liquid/block.rb +31 -14
  5. data/lib/liquid/block_body.rb +169 -57
  6. data/lib/liquid/condition.rb +48 -21
  7. data/lib/liquid/context.rb +111 -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 +28 -32
  12. data/lib/liquid/extensions.rb +2 -0
  13. data/lib/liquid/file_system.rb +6 -4
  14. data/lib/liquid/forloop_drop.rb +54 -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 +8 -5
  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/registers.rb +51 -0
  28. data/lib/liquid/resource_limits.rb +47 -8
  29. data/lib/liquid/standardfilters.rb +551 -114
  30. data/lib/liquid/strainer_factory.rb +41 -0
  31. data/lib/liquid/strainer_template.rb +62 -0
  32. data/lib/liquid/tablerowloop_drop.rb +64 -5
  33. data/lib/liquid/tag/disableable.rb +22 -0
  34. data/lib/liquid/tag/disabler.rb +21 -0
  35. data/lib/liquid/tag.rb +28 -6
  36. data/lib/liquid/tags/assign.rb +36 -18
  37. data/lib/liquid/tags/break.rb +16 -3
  38. data/lib/liquid/tags/capture.rb +24 -18
  39. data/lib/liquid/tags/case.rb +61 -27
  40. data/lib/liquid/tags/comment.rb +18 -3
  41. data/lib/liquid/tags/continue.rb +16 -12
  42. data/lib/liquid/tags/cycle.rb +37 -25
  43. data/lib/liquid/tags/decrement.rb +22 -20
  44. data/lib/liquid/tags/echo.rb +41 -0
  45. data/lib/liquid/tags/for.rb +90 -87
  46. data/lib/liquid/tags/if.rb +50 -32
  47. data/lib/liquid/tags/ifchanged.rb +11 -10
  48. data/lib/liquid/tags/include.rb +49 -60
  49. data/lib/liquid/tags/increment.rb +23 -17
  50. data/lib/liquid/tags/inline_comment.rb +43 -0
  51. data/lib/liquid/tags/raw.rb +25 -11
  52. data/lib/liquid/tags/render.rb +109 -0
  53. data/lib/liquid/tags/table_row.rb +45 -19
  54. data/lib/liquid/tags/unless.rb +38 -19
  55. data/lib/liquid/template.rb +52 -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 +49 -44
  61. data/lib/liquid/variable_lookup.rb +18 -10
  62. data/lib/liquid/version.rb +2 -1
  63. data/lib/liquid.rb +18 -6
  64. metadata +20 -108
  65. data/lib/liquid/strainer.rb +0 -66
  66. data/lib/liquid/truffle.rb +0 -5
  67. data/test/fixtures/en_locale.yml +0 -9
  68. data/test/integration/assign_test.rb +0 -48
  69. data/test/integration/blank_test.rb +0 -106
  70. data/test/integration/block_test.rb +0 -12
  71. data/test/integration/capture_test.rb +0 -50
  72. data/test/integration/context_test.rb +0 -32
  73. data/test/integration/document_test.rb +0 -19
  74. data/test/integration/drop_test.rb +0 -273
  75. data/test/integration/error_handling_test.rb +0 -260
  76. data/test/integration/filter_test.rb +0 -178
  77. data/test/integration/hash_ordering_test.rb +0 -23
  78. data/test/integration/output_test.rb +0 -123
  79. data/test/integration/parse_tree_visitor_test.rb +0 -247
  80. data/test/integration/parsing_quirks_test.rb +0 -122
  81. data/test/integration/render_profiling_test.rb +0 -154
  82. data/test/integration/security_test.rb +0 -80
  83. data/test/integration/standard_filter_test.rb +0 -776
  84. data/test/integration/tags/break_tag_test.rb +0 -15
  85. data/test/integration/tags/continue_tag_test.rb +0 -15
  86. data/test/integration/tags/for_tag_test.rb +0 -410
  87. data/test/integration/tags/if_else_tag_test.rb +0 -188
  88. data/test/integration/tags/include_tag_test.rb +0 -253
  89. data/test/integration/tags/increment_tag_test.rb +0 -23
  90. data/test/integration/tags/raw_tag_test.rb +0 -31
  91. data/test/integration/tags/standard_tag_test.rb +0 -296
  92. data/test/integration/tags/statements_test.rb +0 -111
  93. data/test/integration/tags/table_row_test.rb +0 -64
  94. data/test/integration/tags/unless_else_tag_test.rb +0 -26
  95. data/test/integration/template_test.rb +0 -332
  96. data/test/integration/trim_mode_test.rb +0 -529
  97. data/test/integration/variable_test.rb +0 -96
  98. data/test/test_helper.rb +0 -116
  99. data/test/truffle/truffle_test.rb +0 -9
  100. data/test/unit/block_unit_test.rb +0 -58
  101. data/test/unit/condition_unit_test.rb +0 -166
  102. data/test/unit/context_unit_test.rb +0 -489
  103. data/test/unit/file_system_unit_test.rb +0 -35
  104. data/test/unit/i18n_unit_test.rb +0 -37
  105. data/test/unit/lexer_unit_test.rb +0 -51
  106. data/test/unit/parser_unit_test.rb +0 -82
  107. data/test/unit/regexp_unit_test.rb +0 -44
  108. data/test/unit/strainer_unit_test.rb +0 -164
  109. data/test/unit/tag_unit_test.rb +0 -21
  110. data/test/unit/tags/case_tag_unit_test.rb +0 -10
  111. data/test/unit/tags/for_tag_unit_test.rb +0 -13
  112. data/test/unit/tags/if_tag_unit_test.rb +0 -8
  113. data/test/unit/template_unit_test.rb +0 -78
  114. data/test/unit/tokenizer_unit_test.rb +0 -55
  115. data/test/unit/variable_unit_test.rb +0 -162
@@ -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,53 @@ 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.is_a?(Registers) ? registers : Registers.new(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 = {}
41
+
42
+ @registers.static[:cached_partials] ||= {}
43
+ @registers.static[:file_system] ||= Liquid::Template.file_system
44
+ @registers.static[:template_factory] ||= Liquid::TemplateFactory.new
29
45
 
30
46
  self.exception_renderer = Template.default_exception_renderer
31
47
  if rethrow_errors
32
- self.exception_renderer = ->(e) { raise }
48
+ self.exception_renderer = Liquid::RAISE_EXCEPTION_LAMBDA
33
49
  end
34
50
 
35
- @interrupts = []
36
- @filters = []
37
- @global_filter = nil
51
+ yield self if block_given?
52
+
53
+ # Do this last, since it could result in this object being passed to a Proc in the environment
54
+ squash_instance_assigns_with_environments
38
55
  end
56
+ # rubocop:enable Metrics/ParameterLists
39
57
 
40
58
  def warnings
41
59
  @warnings ||= []
42
60
  end
43
61
 
44
62
  def strainer
45
- @strainer ||= Strainer.create(self, @filters)
63
+ @strainer ||= StrainerFactory.create(self, @filters)
46
64
  end
47
65
 
48
66
  # Adds filters to this context.
@@ -77,7 +95,7 @@ module Liquid
77
95
  def handle_error(e, line_number = nil)
78
96
  e = internal_error unless e.is_a?(Liquid::Error)
79
97
  e.template_name ||= template_name
80
- e.line_number ||= line_number
98
+ e.line_number ||= line_number
81
99
  errors.push(e)
82
100
  exception_renderer.call(e).to_s
83
101
  end
@@ -89,7 +107,7 @@ module Liquid
89
107
  # Push new local scope on the stack. use <tt>Context#stack</tt> instead
90
108
  def push(new_scope = {})
91
109
  @scopes.unshift(new_scope)
92
- raise StackLevelError, "Nesting too deep".freeze if @scopes.length > Block::MAX_DEPTH
110
+ check_overflow
93
111
  end
94
112
 
95
113
  # Merge a hash of variables in the current local scope
@@ -110,20 +128,32 @@ module Liquid
110
128
  # context['var'] = 'hi'
111
129
  # end
112
130
  #
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
-
131
+ # context['var'] #=> nil
132
+ def stack(new_scope = {})
133
+ push(new_scope)
123
134
  yield
124
135
  ensure
125
- pop if @this_stack_used
126
- @this_stack_used = old_stack_used
136
+ pop
137
+ end
138
+
139
+ # Creates a new context inheriting resource limits, filters, environment etc.,
140
+ # but with an isolated scope.
141
+ def new_isolated_subcontext
142
+ check_overflow
143
+
144
+ self.class.build(
145
+ resource_limits: resource_limits,
146
+ static_environments: static_environments,
147
+ registers: Registers.new(registers)
148
+ ).tap do |subcontext|
149
+ subcontext.base_scope_depth = base_scope_depth + 1
150
+ subcontext.exception_renderer = exception_renderer
151
+ subcontext.filters = @filters
152
+ subcontext.strainer = nil
153
+ subcontext.errors = errors
154
+ subcontext.warnings = warnings
155
+ subcontext.disabled_tags = @disabled_tags
156
+ end
127
157
  end
128
158
 
129
159
  def clear_instance_assigns
@@ -132,10 +162,6 @@ module Liquid
132
162
 
133
163
  # Only allow String, Numeric, Hash, Array, Proc, Boolean or <tt>Liquid::Drop</tt>
134
164
  def []=(key, value)
135
- unless @this_stack_used
136
- @this_stack_used = true
137
- push({})
138
- end
139
165
  @scopes[0][key] = value
140
166
  end
141
167
 
@@ -164,26 +190,14 @@ module Liquid
164
190
  # This was changed from find() to find_index() because this is a very hot
165
191
  # path and find_index() is optimized in MRI to reduce object allocation
166
192
  index = @scopes.find_index { |s| s.key?(key) }
167
- scope = @scopes[index] if index
168
-
169
- variable = nil
170
193
 
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
194
+ variable = if index
195
+ lookup_and_evaluate(@scopes[index], key, raise_on_not_found: raise_on_not_found)
196
+ else
197
+ try_variable_find_in_environments(key, raise_on_not_found: raise_on_not_found)
181
198
  end
182
199
 
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
200
+ variable = variable.to_liquid
187
201
  variable.context = self if variable.respond_to?(:context=)
188
202
 
189
203
  variable
@@ -197,14 +211,59 @@ module Liquid
197
211
  value = obj[key]
198
212
 
199
213
  if value.is_a?(Proc) && obj.respond_to?(:[]=)
200
- obj[key] = (value.arity == 0) ? value.call : value.call(self)
214
+ obj[key] = value.arity == 0 ? value.call : value.call(self)
201
215
  else
202
216
  value
203
217
  end
204
218
  end
205
219
 
220
+ def with_disabled_tags(tag_names)
221
+ tag_names.each do |name|
222
+ @disabled_tags[name] = @disabled_tags.fetch(name, 0) + 1
223
+ end
224
+ yield
225
+ ensure
226
+ tag_names.each do |name|
227
+ @disabled_tags[name] -= 1
228
+ end
229
+ end
230
+
231
+ def tag_disabled?(tag_name)
232
+ @disabled_tags.fetch(tag_name, 0) > 0
233
+ end
234
+
235
+ protected
236
+
237
+ attr_writer :base_scope_depth, :warnings, :errors, :strainer, :filters, :disabled_tags
238
+
206
239
  private
207
240
 
241
+ attr_reader :base_scope_depth
242
+
243
+ def try_variable_find_in_environments(key, raise_on_not_found:)
244
+ @environments.each do |environment|
245
+ found_variable = lookup_and_evaluate(environment, key, raise_on_not_found: raise_on_not_found)
246
+ if !found_variable.nil? || @strict_variables && raise_on_not_found
247
+ return found_variable
248
+ end
249
+ end
250
+ @static_environments.each do |environment|
251
+ found_variable = lookup_and_evaluate(environment, key, raise_on_not_found: raise_on_not_found)
252
+ if !found_variable.nil? || @strict_variables && raise_on_not_found
253
+ return found_variable
254
+ end
255
+ end
256
+ nil
257
+ end
258
+
259
+ def check_overflow
260
+ raise StackLevelError, "Nesting too deep" if overflow?
261
+ end
262
+
263
+ def overflow?
264
+ base_scope_depth + @scopes.length > Block::MAX_DEPTH
265
+ end
266
+
208
267
  def internal_error
209
268
  # raise and catch to set backtrace and cause on exception
210
269
  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,41 @@
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
13
  INTEGERS_REGEX = /\A(-?\d+)\z/
27
14
  FLOATS_REGEX = /\A(-?\d[\d\.]+)\z/
28
- RANGES_REGEX = /\A\((\S+)\.\.(\S+)\)\z/
15
+
16
+ # Use an atomic group (?>...) to avoid pathological backtracing from
17
+ # malicious input as described in https://github.com/Shopify/liquid/issues/1357
18
+ RANGES_REGEX = /\A\(\s*(?>(\S+)\s*\.\.)\s*(\S+)\s*\)\z/
29
19
 
30
20
  def self.parse(markup)
31
- if LITERALS.key?(markup)
32
- LITERALS[markup]
21
+ return nil unless markup
22
+
23
+ markup = markup.strip
24
+ if (markup.start_with?('"') && markup.end_with?('"')) ||
25
+ (markup.start_with?("'") && markup.end_with?("'"))
26
+ return markup[1..-2]
27
+ end
28
+
29
+ case markup
30
+ when INTEGERS_REGEX
31
+ Regexp.last_match(1).to_i
32
+ when RANGES_REGEX
33
+ RangeLookup.parse(Regexp.last_match(1), Regexp.last_match(2))
34
+ when FLOATS_REGEX
35
+ Regexp.last_match(1).to_f
33
36
  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
37
+ if LITERALS.key?(markup)
38
+ LITERALS[markup]
43
39
  else
44
40
  VariableLookup.parse(markup)
45
41
  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,34 +1,84 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
4
+ # @liquid_public_docs
5
+ # @liquid_type object
6
+ # @liquid_name forloop
7
+ # @liquid_summary
8
+ # Information about a parent [`for` loop](/api/liquid/tags#for).
2
9
  class ForloopDrop < Drop
3
10
  def initialize(name, length, parentloop)
4
- @name = name
5
- @length = length
11
+ @name = name
12
+ @length = length
6
13
  @parentloop = parentloop
7
- @index = 0
14
+ @index = 0
8
15
  end
9
16
 
10
- attr_reader :name, :length, :parentloop
17
+ # @liquid_public_docs
18
+ # @liquid_name length
19
+ # @liquid_summary
20
+ # The total number of iterations in the loop.
21
+ # @liquid_return [number]
22
+ attr_reader :length
23
+
24
+ # @liquid_public_docs
25
+ # @liquid_name parentloop
26
+ # @liquid_summary
27
+ # The parent `forloop` object.
28
+ # @liquid_description
29
+ # If the current `for` loop isn't nested inside another `for` loop, then `nil` is returned.
30
+ # @liquid_return [forloop]
31
+ attr_reader :parentloop
32
+
33
+ def name
34
+ Usage.increment('forloop_drop_name')
35
+ @name
36
+ end
11
37
 
38
+ # @liquid_public_docs
39
+ # @liquid_summary
40
+ # The 1-based index of the current iteration.
41
+ # @liquid_return [number]
12
42
  def index
13
43
  @index + 1
14
44
  end
15
45
 
46
+ # @liquid_public_docs
47
+ # @liquid_summary
48
+ # The 0-based index of the current iteration.
49
+ # @liquid_return [number]
16
50
  def index0
17
51
  @index
18
52
  end
19
53
 
54
+ # @liquid_public_docs
55
+ # @liquid_summary
56
+ # The 1-based index of the current iteration, in reverse order.
57
+ # @liquid_return [number]
20
58
  def rindex
21
59
  @length - @index
22
60
  end
23
61
 
62
+ # @liquid_public_docs
63
+ # @liquid_summary
64
+ # The 0-based index of the current iteration, in reverse order.
65
+ # @liquid_return [number]
24
66
  def rindex0
25
67
  @length - @index - 1
26
68
  end
27
69
 
70
+ # @liquid_public_docs
71
+ # @liquid_summary
72
+ # Returns `true` if the current iteration is the first. Returns `false` if not.
73
+ # @liquid_return [boolean]
28
74
  def first
29
75
  @index == 0
30
76
  end
31
77
 
78
+ # @liquid_public_docs
79
+ # @liquid_summary
80
+ # Returns `true` if the current iteration is the last. Returns `false` if not.
81
+ # @liquid_return [boolean]
32
82
  def last
33
83
  @index == @length - 1
34
84
  end
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