liquid 4.0.0 → 5.10.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 (117) hide show
  1. checksums.yaml +5 -5
  2. data/History.md +235 -2
  3. data/README.md +58 -8
  4. data/lib/liquid/block.rb +51 -20
  5. data/lib/liquid/block_body.rb +216 -82
  6. data/lib/liquid/condition.rb +83 -32
  7. data/lib/liquid/const.rb +8 -0
  8. data/lib/liquid/context.rb +130 -59
  9. data/lib/liquid/deprecations.rb +22 -0
  10. data/lib/liquid/document.rb +47 -9
  11. data/lib/liquid/drop.rb +8 -2
  12. data/lib/liquid/environment.rb +159 -0
  13. data/lib/liquid/errors.rb +23 -20
  14. data/lib/liquid/expression.rb +114 -31
  15. data/lib/liquid/extensions.rb +8 -0
  16. data/lib/liquid/file_system.rb +6 -4
  17. data/lib/liquid/forloop_drop.rb +51 -4
  18. data/lib/liquid/i18n.rb +5 -3
  19. data/lib/liquid/interrupts.rb +3 -1
  20. data/lib/liquid/lexer.rb +165 -39
  21. data/lib/liquid/locales/en.yml +16 -6
  22. data/lib/liquid/parse_context.rb +62 -7
  23. data/lib/liquid/parse_tree_visitor.rb +42 -0
  24. data/lib/liquid/parser.rb +31 -19
  25. data/lib/liquid/parser_switching.rb +42 -3
  26. data/lib/liquid/partial_cache.rb +33 -0
  27. data/lib/liquid/profiler/hooks.rb +26 -14
  28. data/lib/liquid/profiler.rb +67 -86
  29. data/lib/liquid/range_lookup.rb +26 -6
  30. data/lib/liquid/registers.rb +51 -0
  31. data/lib/liquid/resource_limits.rb +47 -8
  32. data/lib/liquid/snippet_drop.rb +22 -0
  33. data/lib/liquid/standardfilters.rb +813 -137
  34. data/lib/liquid/strainer_template.rb +62 -0
  35. data/lib/liquid/tablerowloop_drop.rb +64 -5
  36. data/lib/liquid/tag/disableable.rb +22 -0
  37. data/lib/liquid/tag/disabler.rb +13 -0
  38. data/lib/liquid/tag.rb +42 -6
  39. data/lib/liquid/tags/assign.rb +46 -18
  40. data/lib/liquid/tags/break.rb +15 -4
  41. data/lib/liquid/tags/capture.rb +26 -18
  42. data/lib/liquid/tags/case.rb +108 -32
  43. data/lib/liquid/tags/comment.rb +76 -4
  44. data/lib/liquid/tags/continue.rb +15 -13
  45. data/lib/liquid/tags/cycle.rb +117 -34
  46. data/lib/liquid/tags/decrement.rb +30 -23
  47. data/lib/liquid/tags/doc.rb +81 -0
  48. data/lib/liquid/tags/echo.rb +39 -0
  49. data/lib/liquid/tags/for.rb +109 -96
  50. data/lib/liquid/tags/if.rb +72 -41
  51. data/lib/liquid/tags/ifchanged.rb +10 -11
  52. data/lib/liquid/tags/include.rb +89 -63
  53. data/lib/liquid/tags/increment.rb +31 -20
  54. data/lib/liquid/tags/inline_comment.rb +28 -0
  55. data/lib/liquid/tags/raw.rb +25 -13
  56. data/lib/liquid/tags/render.rb +151 -0
  57. data/lib/liquid/tags/snippet.rb +45 -0
  58. data/lib/liquid/tags/table_row.rb +104 -21
  59. data/lib/liquid/tags/unless.rb +37 -20
  60. data/lib/liquid/tags.rb +51 -0
  61. data/lib/liquid/template.rb +90 -106
  62. data/lib/liquid/template_factory.rb +9 -0
  63. data/lib/liquid/tokenizer.rb +143 -13
  64. data/lib/liquid/usage.rb +8 -0
  65. data/lib/liquid/utils.rb +114 -5
  66. data/lib/liquid/variable.rb +119 -45
  67. data/lib/liquid/variable_lookup.rb +35 -13
  68. data/lib/liquid/version.rb +3 -1
  69. data/lib/liquid.rb +31 -18
  70. metadata +56 -107
  71. data/lib/liquid/strainer.rb +0 -66
  72. data/test/fixtures/en_locale.yml +0 -9
  73. data/test/integration/assign_test.rb +0 -48
  74. data/test/integration/blank_test.rb +0 -106
  75. data/test/integration/capture_test.rb +0 -50
  76. data/test/integration/context_test.rb +0 -32
  77. data/test/integration/document_test.rb +0 -19
  78. data/test/integration/drop_test.rb +0 -273
  79. data/test/integration/error_handling_test.rb +0 -260
  80. data/test/integration/filter_test.rb +0 -178
  81. data/test/integration/hash_ordering_test.rb +0 -23
  82. data/test/integration/output_test.rb +0 -123
  83. data/test/integration/parsing_quirks_test.rb +0 -118
  84. data/test/integration/render_profiling_test.rb +0 -154
  85. data/test/integration/security_test.rb +0 -66
  86. data/test/integration/standard_filter_test.rb +0 -535
  87. data/test/integration/tags/break_tag_test.rb +0 -15
  88. data/test/integration/tags/continue_tag_test.rb +0 -15
  89. data/test/integration/tags/for_tag_test.rb +0 -410
  90. data/test/integration/tags/if_else_tag_test.rb +0 -188
  91. data/test/integration/tags/include_tag_test.rb +0 -238
  92. data/test/integration/tags/increment_tag_test.rb +0 -23
  93. data/test/integration/tags/raw_tag_test.rb +0 -31
  94. data/test/integration/tags/standard_tag_test.rb +0 -296
  95. data/test/integration/tags/statements_test.rb +0 -111
  96. data/test/integration/tags/table_row_test.rb +0 -64
  97. data/test/integration/tags/unless_else_tag_test.rb +0 -26
  98. data/test/integration/template_test.rb +0 -323
  99. data/test/integration/trim_mode_test.rb +0 -525
  100. data/test/integration/variable_test.rb +0 -92
  101. data/test/test_helper.rb +0 -117
  102. data/test/unit/block_unit_test.rb +0 -58
  103. data/test/unit/condition_unit_test.rb +0 -158
  104. data/test/unit/context_unit_test.rb +0 -483
  105. data/test/unit/file_system_unit_test.rb +0 -35
  106. data/test/unit/i18n_unit_test.rb +0 -37
  107. data/test/unit/lexer_unit_test.rb +0 -51
  108. data/test/unit/parser_unit_test.rb +0 -82
  109. data/test/unit/regexp_unit_test.rb +0 -44
  110. data/test/unit/strainer_unit_test.rb +0 -148
  111. data/test/unit/tag_unit_test.rb +0 -21
  112. data/test/unit/tags/case_tag_unit_test.rb +0 -10
  113. data/test/unit/tags/for_tag_unit_test.rb +0 -13
  114. data/test/unit/tags/if_tag_unit_test.rb +0 -8
  115. data/test/unit/template_unit_test.rb +0 -78
  116. data/test/unit/tokenizer_unit_test.rb +0 -55
  117. data/test/unit/variable_unit_test.rb +0 -162
@@ -1,31 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "strscan"
4
+
1
5
  module Liquid
2
6
  class Tokenizer
3
- attr_reader :line_number
7
+ attr_reader :line_number, :for_liquid_tag
8
+
9
+ TAG_END = /%\}/
10
+ TAG_OR_VARIABLE_START = /\{[\{\%]/
11
+ NEWLINE = /\n/
12
+
13
+ OPEN_CURLEY = "{".ord
14
+ CLOSE_CURLEY = "}".ord
15
+ PERCENTAGE = "%".ord
16
+
17
+ def initialize(
18
+ source:,
19
+ string_scanner:,
20
+ line_numbers: false,
21
+ line_number: nil,
22
+ for_liquid_tag: false
23
+ )
24
+ @line_number = line_number || (line_numbers ? 1 : nil)
25
+ @for_liquid_tag = for_liquid_tag
26
+ @source = source.to_s.to_str
27
+ @offset = 0
28
+ @tokens = []
4
29
 
5
- def initialize(source, line_numbers = false)
6
- @source = source
7
- @line_number = line_numbers ? 1 : nil
8
- @tokens = tokenize
30
+ if @source
31
+ @ss = string_scanner
32
+ @ss.string = @source
33
+ tokenize
34
+ end
9
35
  end
10
36
 
11
37
  def shift
12
- token = @tokens.shift
13
- @line_number += token.count("\n") if @line_number && token
38
+ token = @tokens[@offset]
39
+
40
+ return unless token
41
+
42
+ @offset += 1
43
+
44
+ if @line_number
45
+ @line_number += @for_liquid_tag ? 1 : token.count("\n")
46
+ end
47
+
14
48
  token
15
49
  end
16
50
 
17
51
  private
18
52
 
19
53
  def tokenize
20
- @source = @source.source if @source.respond_to?(:source)
21
- return [] if @source.to_s.empty?
54
+ if @for_liquid_tag
55
+ @tokens = @source.split("\n")
56
+ else
57
+ @tokens << shift_normal until @ss.eos?
58
+ end
59
+
60
+ @source = nil
61
+ @ss = nil
62
+ end
63
+
64
+ def shift_normal
65
+ token = next_token
66
+
67
+ return unless token
68
+
69
+ token
70
+ end
71
+
72
+ def next_token
73
+ # possible states: :text, :tag, :variable
74
+ byte_a = @ss.peek_byte
75
+
76
+ if byte_a == OPEN_CURLEY
77
+ @ss.scan_byte
78
+
79
+ byte_b = @ss.peek_byte
22
80
 
23
- tokens = @source.split(TemplateParser)
81
+ if byte_b == PERCENTAGE
82
+ @ss.scan_byte
83
+ return next_tag_token
84
+ elsif byte_b == OPEN_CURLEY
85
+ @ss.scan_byte
86
+ return next_variable_token
87
+ end
24
88
 
25
- # removes the rogue empty element at the beginning of the array
26
- tokens.shift if tokens[0] && tokens[0].empty?
89
+ @ss.pos -= 1
90
+ end
91
+
92
+ next_text_token
93
+ end
94
+
95
+ def next_text_token
96
+ start = @ss.pos
97
+
98
+ unless @ss.skip_until(TAG_OR_VARIABLE_START)
99
+ token = @ss.rest
100
+ @ss.terminate
101
+ return token
102
+ end
103
+
104
+ pos = @ss.pos -= 2
105
+ @source.byteslice(start, pos - start)
106
+ rescue ::ArgumentError => e
107
+ if e.message == "invalid byte sequence in #{@ss.string.encoding}"
108
+ raise SyntaxError, "Invalid byte sequence in #{@ss.string.encoding}"
109
+ else
110
+ raise
111
+ end
112
+ end
113
+
114
+ def next_variable_token
115
+ start = @ss.pos - 2
116
+
117
+ byte_a = byte_b = @ss.scan_byte
118
+
119
+ while byte_b
120
+ byte_a = @ss.scan_byte while byte_a && (byte_a != CLOSE_CURLEY && byte_a != OPEN_CURLEY)
121
+
122
+ break unless byte_a
123
+
124
+ if @ss.eos?
125
+ return byte_a == CLOSE_CURLEY ? @source.byteslice(start, @ss.pos - start) : "{{"
126
+ end
127
+
128
+ byte_b = @ss.scan_byte
129
+
130
+ if byte_a == CLOSE_CURLEY
131
+ if byte_b == CLOSE_CURLEY
132
+ return @source.byteslice(start, @ss.pos - start)
133
+ elsif byte_b != CLOSE_CURLEY
134
+ @ss.pos -= 1
135
+ return @source.byteslice(start, @ss.pos - start)
136
+ end
137
+ elsif byte_a == OPEN_CURLEY && byte_b == PERCENTAGE
138
+ return next_tag_token_with_start(start)
139
+ end
140
+
141
+ byte_a = byte_b
142
+ end
143
+
144
+ "{{"
145
+ end
146
+
147
+ def next_tag_token
148
+ start = @ss.pos - 2
149
+ if (len = @ss.skip_until(TAG_END))
150
+ @source.byteslice(start, len + 2)
151
+ else
152
+ "{%"
153
+ end
154
+ end
27
155
 
28
- tokens
156
+ def next_tag_token_with_start(start)
157
+ @ss.skip_until(TAG_END)
158
+ @source.byteslice(start, @ss.pos - start)
29
159
  end
30
160
  end
31
161
  end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liquid
4
+ module Usage
5
+ def self.increment(name)
6
+ end
7
+ end
8
+ end
data/lib/liquid/utils.rb CHANGED
@@ -1,5 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  module Utils
5
+ DECIMAL_REGEX = /\A-?\d+\.\d+\z/
6
+ UNIX_TIMESTAMP_REGEX = /\A\d+\z/
7
+
3
8
  def self.slice_collection(collection, from, to)
4
9
  if (from != 0 || !to.nil?) && collection.respond_to?(:load_slice)
5
10
  collection.load_slice(from, to)
@@ -10,7 +15,7 @@ module Liquid
10
15
 
11
16
  def self.slice_collection_using_each(collection, from, to)
12
17
  segments = []
13
- index = 0
18
+ index = 0
14
19
 
15
20
  # Maintains Ruby 1.8.7 String#each behaviour on 1.9
16
21
  if collection.is_a?(String)
@@ -46,11 +51,11 @@ module Liquid
46
51
  def self.to_number(obj)
47
52
  case obj
48
53
  when Float
49
- BigDecimal.new(obj.to_s)
54
+ BigDecimal(obj.to_s)
50
55
  when Numeric
51
56
  obj
52
57
  when String
53
- (obj.strip =~ /\A-?\d+\.\d+\z/) ? BigDecimal.new(obj) : obj.to_i
58
+ DECIMAL_REGEX.match?(obj.strip) ? BigDecimal(obj) : obj.to_i
54
59
  else
55
60
  if obj.respond_to?(:to_number)
56
61
  obj.to_number
@@ -69,9 +74,9 @@ module Liquid
69
74
  end
70
75
 
71
76
  case obj
72
- when 'now'.freeze, 'today'.freeze
77
+ when 'now', 'today'
73
78
  Time.now
74
- when /\A\d+\z/, Integer
79
+ when UNIX_TIMESTAMP_REGEX, Integer
75
80
  Time.at(obj.to_i)
76
81
  when String
77
82
  Time.parse(obj)
@@ -79,5 +84,109 @@ module Liquid
79
84
  rescue ::ArgumentError
80
85
  nil
81
86
  end
87
+
88
+ def self.to_liquid_value(obj)
89
+ # Enable "obj" to represent itself as a primitive value like integer, string, or boolean
90
+ return obj.to_liquid_value if obj.respond_to?(:to_liquid_value)
91
+
92
+ # Otherwise return the object itself
93
+ obj
94
+ end
95
+
96
+ def self.to_s(obj, seen = {})
97
+ case obj
98
+ when Hash
99
+ # If the custom hash implementation overrides `#to_s`, use their
100
+ # custom implementation. Otherwise we use Liquid's default
101
+ # implementation.
102
+ if obj.class.instance_method(:to_s) == HASH_TO_S_METHOD
103
+ hash_inspect(obj, seen)
104
+ else
105
+ obj.to_s
106
+ end
107
+ when Array
108
+ array_inspect(obj, seen)
109
+ else
110
+ obj.to_s
111
+ end
112
+ end
113
+
114
+ def self.inspect(obj, seen = {})
115
+ case obj
116
+ when Hash
117
+ # If the custom hash implementation overrides `#inspect`, use their
118
+ # custom implementation. Otherwise we use Liquid's default
119
+ # implementation.
120
+ if obj.class.instance_method(:inspect) == HASH_INSPECT_METHOD
121
+ hash_inspect(obj, seen)
122
+ else
123
+ obj.inspect
124
+ end
125
+ when Array
126
+ array_inspect(obj, seen)
127
+ else
128
+ obj.inspect
129
+ end
130
+ end
131
+
132
+ def self.array_inspect(arr, seen = {})
133
+ if seen[arr.object_id]
134
+ return "[...]"
135
+ end
136
+
137
+ seen[arr.object_id] = true
138
+ str = +"["
139
+ cursor = 0
140
+ len = arr.length
141
+
142
+ while cursor < len
143
+ if cursor > 0
144
+ str << ", "
145
+ end
146
+
147
+ item_str = inspect(arr[cursor], seen)
148
+ str << item_str
149
+ cursor += 1
150
+ end
151
+
152
+ str << "]"
153
+ str
154
+ ensure
155
+ seen.delete(arr.object_id)
156
+ end
157
+
158
+ def self.hash_inspect(hash, seen = {})
159
+ if seen[hash.object_id]
160
+ return "{...}"
161
+ end
162
+ seen[hash.object_id] = true
163
+
164
+ str = +"{"
165
+ first = true
166
+ hash.each do |key, value|
167
+ if first
168
+ first = false
169
+ else
170
+ str << ", "
171
+ end
172
+
173
+ key_str = inspect(key, seen)
174
+ str << key_str
175
+ str << "=>"
176
+
177
+ value_str = inspect(value, seen)
178
+ str << value_str
179
+ end
180
+ str << "}"
181
+ str
182
+ ensure
183
+ seen.delete(hash.object_id)
184
+ end
185
+
186
+ HASH_TO_S_METHOD = Hash.instance_method(:to_s)
187
+ private_constant :HASH_TO_S_METHOD
188
+
189
+ HASH_INSPECT_METHOD = Hash.instance_method(:inspect)
190
+ private_constant :HASH_INSPECT_METHOD
82
191
  end
83
192
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  # Holds variables. Variables are only loaded "just in time"
3
5
  # and are not evaluated as part of the render stage
@@ -10,19 +12,25 @@ module Liquid
10
12
  # {{ user | link }}
11
13
  #
12
14
  class Variable
13
- FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o
15
+ FilterMarkupRegex = /#{FilterSeparator}\s*(.*)/om
16
+ FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o
17
+ FilterArgsRegex = /(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o
18
+ JustTagAttributes = /\A#{TagAttributes}\z/o
19
+ MarkupWithQuotedFragment = /(#{QuotedFragment})(.*)/om
20
+
14
21
  attr_accessor :filters, :name, :line_number
15
22
  attr_reader :parse_context
16
23
  alias_method :options, :parse_context
24
+
17
25
  include ParserSwitching
18
26
 
19
27
  def initialize(markup, parse_context)
20
- @markup = markup
21
- @name = nil
28
+ @markup = markup
29
+ @name = nil
22
30
  @parse_context = parse_context
23
- @line_number = parse_context.line_number
31
+ @line_number = parse_context.line_number
24
32
 
25
- parse_with_selected_parser(markup)
33
+ strict_parse_with_error_mode_fallback(markup)
26
34
  end
27
35
 
28
36
  def raw
@@ -35,35 +43,48 @@ module Liquid
35
43
 
36
44
  def lax_parse(markup)
37
45
  @filters = []
38
- return unless markup =~ /(#{QuotedFragment})(.*)/om
46
+ return unless markup =~ MarkupWithQuotedFragment
39
47
 
40
- name_markup = $1
41
- filter_markup = $2
42
- @name = Expression.parse(name_markup)
43
- if filter_markup =~ /#{FilterSeparator}\s*(.*)/om
44
- filters = $1.scan(FilterParser)
48
+ name_markup = Regexp.last_match(1)
49
+ filter_markup = Regexp.last_match(2)
50
+ @name = parse_context.parse_expression(name_markup)
51
+ if filter_markup =~ FilterMarkupRegex
52
+ filters = Regexp.last_match(1).scan(FilterParser)
45
53
  filters.each do |f|
46
54
  next unless f =~ /\w+/
47
55
  filtername = Regexp.last_match(0)
48
- filterargs = f.scan(/(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o).flatten
49
- @filters << parse_filter_expressions(filtername, filterargs)
56
+ filterargs = f.scan(FilterArgsRegex).flatten
57
+ @filters << lax_parse_filter_expressions(filtername, filterargs)
50
58
  end
51
59
  end
52
60
  end
53
61
 
54
62
  def strict_parse(markup)
55
63
  @filters = []
56
- p = Parser.new(markup)
64
+ p = @parse_context.new_parser(markup)
57
65
 
58
- @name = Expression.parse(p.expression)
66
+ return if p.look(:end_of_string)
67
+
68
+ @name = parse_context.safe_parse_expression(p)
59
69
  while p.consume?(:pipe)
60
70
  filtername = p.consume(:id)
61
- filterargs = p.consume?(:colon) ? parse_filterargs(p) : []
62
- @filters << parse_filter_expressions(filtername, filterargs)
71
+ filterargs = p.consume?(:colon) ? parse_filterargs(p) : Const::EMPTY_ARRAY
72
+ @filters << lax_parse_filter_expressions(filtername, filterargs)
63
73
  end
64
74
  p.consume(:end_of_string)
65
75
  end
66
76
 
77
+ def rigid_parse(markup)
78
+ @filters = []
79
+ p = @parse_context.new_parser(markup)
80
+
81
+ return if p.look(:end_of_string)
82
+
83
+ @name = parse_context.safe_parse_expression(p)
84
+ @filters << rigid_parse_filter_expressions(p) while p.consume?(:pipe)
85
+ p.consume(:end_of_string)
86
+ end
87
+
67
88
  def parse_filterargs(p)
68
89
  # first argument
69
90
  filterargs = [p.argument]
@@ -73,37 +94,103 @@ module Liquid
73
94
  end
74
95
 
75
96
  def render(context)
76
- obj = @filters.inject(context.evaluate(@name)) do |output, (filter_name, filter_args, filter_kwargs)|
97
+ obj = context.evaluate(@name)
98
+
99
+ @filters.each do |filter_name, filter_args, filter_kwargs|
77
100
  filter_args = evaluate_filter_expressions(context, filter_args, filter_kwargs)
78
- context.invoke(filter_name, output, *filter_args)
101
+ obj = context.invoke(filter_name, obj, *filter_args)
79
102
  end
80
103
 
81
- obj = context.apply_global_filter(obj)
104
+ context.apply_global_filter(obj)
105
+ end
82
106
 
83
- taint_check(context, obj)
107
+ def render_to_output_buffer(context, output)
108
+ obj = render(context)
109
+ render_obj_to_output(obj, output)
110
+ output
111
+ end
112
+
113
+ def render_obj_to_output(obj, output)
114
+ case obj
115
+ when NilClass
116
+ # Do nothing
117
+ when Array
118
+ obj.each do |o|
119
+ render_obj_to_output(o, output)
120
+ end
121
+ else
122
+ output << Liquid::Utils.to_s(obj)
123
+ end
124
+ end
125
+
126
+ def disabled?(_context)
127
+ false
128
+ end
84
129
 
85
- obj
130
+ def disabled_tags
131
+ []
86
132
  end
87
133
 
88
134
  private
89
135
 
90
- def parse_filter_expressions(filter_name, unparsed_args)
91
- filter_args = []
92
- keyword_args = {}
136
+ def lax_parse_filter_expressions(filter_name, unparsed_args)
137
+ filter_args = []
138
+ keyword_args = nil
93
139
  unparsed_args.each do |a|
94
- if matches = a.match(/\A#{TagAttributes}\z/o)
95
- keyword_args[matches[1]] = Expression.parse(matches[2])
140
+ if (matches = a.match(JustTagAttributes))
141
+ keyword_args ||= {}
142
+ keyword_args[matches[1]] = parse_context.parse_expression(matches[2])
96
143
  else
97
- filter_args << Expression.parse(a)
144
+ filter_args << parse_context.parse_expression(a)
98
145
  end
99
146
  end
100
147
  result = [filter_name, filter_args]
148
+ result << keyword_args if keyword_args
149
+ result
150
+ end
151
+
152
+ # Surprisingly, positional and keyword arguments can be mixed.
153
+ #
154
+ # filter = filtername [":" filterargs?]
155
+ # filterargs = argument ("," argument)*
156
+ # argument = (positional_argument | keyword_argument)
157
+ # positional_argument = expression
158
+ # keyword_argument = id ":" expression
159
+ def rigid_parse_filter_expressions(p)
160
+ filtername = p.consume(:id)
161
+ filter_args = []
162
+ keyword_args = {}
163
+
164
+ if p.consume?(:colon)
165
+ # Parse first argument (no leading comma)
166
+ argument(p, filter_args, keyword_args) unless end_of_arguments?(p)
167
+
168
+ # Parse remaining arguments (with leading commas) and optional trailing comma
169
+ argument(p, filter_args, keyword_args) while p.consume?(:comma) && !end_of_arguments?(p)
170
+ end
171
+
172
+ result = [filtername, filter_args]
101
173
  result << keyword_args unless keyword_args.empty?
102
174
  result
103
175
  end
104
176
 
177
+ def argument(p, positional_arguments, keyword_arguments)
178
+ if p.look(:id) && p.look(:colon, 1)
179
+ key = p.consume(:id)
180
+ p.consume(:colon)
181
+ value = parse_context.safe_parse_expression(p)
182
+ keyword_arguments[key] = value
183
+ else
184
+ positional_arguments << parse_context.safe_parse_expression(p)
185
+ end
186
+ end
187
+
188
+ def end_of_arguments?(p)
189
+ p.look(:pipe) || p.look(:end_of_string)
190
+ end
191
+
105
192
  def evaluate_filter_expressions(context, filter_args, filter_kwargs)
106
- parsed_args = filter_args.map{ |expr| context.evaluate(expr) }
193
+ parsed_args = filter_args.map { |expr| context.evaluate(expr) }
107
194
  if filter_kwargs
108
195
  parsed_kwargs = {}
109
196
  filter_kwargs.each do |key, expr|
@@ -114,22 +201,9 @@ module Liquid
114
201
  parsed_args
115
202
  end
116
203
 
117
- def taint_check(context, obj)
118
- return unless obj.tainted?
119
- return if Template.taint_mode == :lax
120
-
121
- @markup =~ QuotedFragment
122
- name = Regexp.last_match(0)
123
-
124
- error = TaintedError.new("variable '#{name}' is tainted and was not escaped")
125
- error.line_number = line_number
126
- error.template_name = context.template_name
127
-
128
- case Template.taint_mode
129
- when :warn
130
- context.warnings << error
131
- when :error
132
- raise error
204
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
205
+ def children
206
+ [@node.name] + @node.filters.flatten
133
207
  end
134
208
  end
135
209
  end
@@ -1,43 +1,59 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  class VariableLookup
3
- SQUARE_BRACKETED = /\A\[(.*)\]\z/m
4
- COMMAND_METHODS = ['size'.freeze, 'first'.freeze, 'last'.freeze]
5
+ COMMAND_METHODS = ['size', 'first', 'last'].freeze
5
6
 
6
7
  attr_reader :name, :lookups
7
8
 
8
- def self.parse(markup)
9
- new(markup)
9
+ def self.parse(markup, string_scanner = StringScanner.new(""), cache = nil)
10
+ new(markup, string_scanner, cache)
10
11
  end
11
12
 
12
- def initialize(markup)
13
+ def initialize(markup, string_scanner = StringScanner.new(""), cache = nil)
13
14
  lookups = markup.scan(VariableParser)
14
15
 
15
16
  name = lookups.shift
16
- if name =~ SQUARE_BRACKETED
17
- name = Expression.parse($1)
17
+ if name&.start_with?('[') && name&.end_with?(']')
18
+ name = Expression.parse(
19
+ name[1..-2],
20
+ string_scanner,
21
+ cache,
22
+ )
18
23
  end
19
24
  @name = name
20
25
 
21
- @lookups = lookups
26
+ @lookups = lookups
22
27
  @command_flags = 0
23
28
 
24
29
  @lookups.each_index do |i|
25
30
  lookup = lookups[i]
26
- if lookup =~ SQUARE_BRACKETED
27
- lookups[i] = Expression.parse($1)
31
+ if lookup&.start_with?('[') && lookup&.end_with?(']')
32
+ lookups[i] = Expression.parse(
33
+ lookup[1..-2],
34
+ string_scanner,
35
+ cache,
36
+ )
28
37
  elsif COMMAND_METHODS.include?(lookup)
29
38
  @command_flags |= 1 << i
30
39
  end
31
40
  end
32
41
  end
33
42
 
43
+ def lookup_command?(lookup_index)
44
+ @command_flags & (1 << lookup_index) != 0
45
+ end
46
+
34
47
  def evaluate(context)
35
- name = context.evaluate(@name)
48
+ name = context.evaluate(@name)
36
49
  object = context.find_variable(name)
37
50
 
38
51
  @lookups.each_index do |i|
39
52
  key = context.evaluate(@lookups[i])
40
53
 
54
+ # Cast "key" to its liquid value to enable it to act as a primitive value
55
+ key = Liquid::Utils.to_liquid_value(key)
56
+
41
57
  # If object is a hash- or array-like object we look for the
42
58
  # presence of the key and if its available we return it
43
59
  if object.respond_to?(:[]) &&
@@ -45,13 +61,13 @@ module Liquid
45
61
  (object.respond_to?(:fetch) && key.is_a?(Integer)))
46
62
 
47
63
  # if its a proc we will replace the entry with the proc
48
- res = context.lookup_and_evaluate(object, key)
64
+ res = context.lookup_and_evaluate(object, key)
49
65
  object = res.to_liquid
50
66
 
51
67
  # Some special cases. If the part wasn't in square brackets and
52
68
  # no key with the same name was found we interpret following calls
53
69
  # as commands and call them on the current object
54
- elsif @command_flags & (1 << i) != 0 && object.respond_to?(key)
70
+ elsif lookup_command?(i) && object.respond_to?(key)
55
71
  object = object.send(key).to_liquid
56
72
 
57
73
  # No key was present with the desired value and it wasn't one of the directly supported
@@ -78,5 +94,11 @@ module Liquid
78
94
  def state
79
95
  [@name, @lookups, @command_flags]
80
96
  end
97
+
98
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
99
+ def children
100
+ @node.lookups
101
+ end
102
+ end
81
103
  end
82
104
  end
@@ -1,4 +1,6 @@
1
1
  # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
2
4
  module Liquid
3
- VERSION = "4.0.0"
5
+ VERSION = "5.10.0"
4
6
  end