liquid 2.6.3 → 3.0.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 (102) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +46 -13
  3. data/README.md +27 -2
  4. data/lib/liquid/block.rb +85 -51
  5. data/lib/liquid/block_body.rb +123 -0
  6. data/lib/liquid/condition.rb +26 -15
  7. data/lib/liquid/context.rb +106 -140
  8. data/lib/liquid/document.rb +3 -3
  9. data/lib/liquid/drop.rb +17 -1
  10. data/lib/liquid/errors.rb +50 -2
  11. data/lib/liquid/expression.rb +33 -0
  12. data/lib/liquid/file_system.rb +17 -6
  13. data/lib/liquid/i18n.rb +39 -0
  14. data/lib/liquid/interrupts.rb +1 -1
  15. data/lib/liquid/lexer.rb +51 -0
  16. data/lib/liquid/locales/en.yml +22 -0
  17. data/lib/liquid/parser.rb +90 -0
  18. data/lib/liquid/parser_switching.rb +31 -0
  19. data/lib/liquid/profiler/hooks.rb +23 -0
  20. data/lib/liquid/profiler.rb +159 -0
  21. data/lib/liquid/range_lookup.rb +22 -0
  22. data/lib/liquid/standardfilters.rb +143 -55
  23. data/lib/liquid/strainer.rb +14 -4
  24. data/lib/liquid/tag.rb +25 -9
  25. data/lib/liquid/tags/assign.rb +12 -9
  26. data/lib/liquid/tags/break.rb +1 -1
  27. data/lib/liquid/tags/capture.rb +10 -8
  28. data/lib/liquid/tags/case.rb +13 -13
  29. data/lib/liquid/tags/comment.rb +9 -2
  30. data/lib/liquid/tags/continue.rb +1 -4
  31. data/lib/liquid/tags/cycle.rb +5 -7
  32. data/lib/liquid/tags/decrement.rb +3 -4
  33. data/lib/liquid/tags/for.rb +69 -36
  34. data/lib/liquid/tags/if.rb +52 -25
  35. data/lib/liquid/tags/ifchanged.rb +3 -3
  36. data/lib/liquid/tags/include.rb +19 -8
  37. data/lib/liquid/tags/increment.rb +4 -8
  38. data/lib/liquid/tags/raw.rb +4 -7
  39. data/lib/liquid/tags/table_row.rb +73 -0
  40. data/lib/liquid/tags/unless.rb +2 -4
  41. data/lib/liquid/template.rb +124 -14
  42. data/lib/liquid/token.rb +18 -0
  43. data/lib/liquid/utils.rb +13 -4
  44. data/lib/liquid/variable.rb +103 -25
  45. data/lib/liquid/variable_lookup.rb +78 -0
  46. data/lib/liquid/version.rb +1 -1
  47. data/lib/liquid.rb +19 -11
  48. data/test/fixtures/en_locale.yml +9 -0
  49. data/test/{liquid → integration}/assign_test.rb +18 -1
  50. data/test/integration/blank_test.rb +106 -0
  51. data/test/{liquid → integration}/capture_test.rb +3 -3
  52. data/test/integration/context_test.rb +32 -0
  53. data/test/integration/drop_test.rb +271 -0
  54. data/test/integration/error_handling_test.rb +207 -0
  55. data/test/{liquid → integration}/filter_test.rb +11 -11
  56. data/test/integration/hash_ordering_test.rb +23 -0
  57. data/test/{liquid → integration}/output_test.rb +13 -13
  58. data/test/integration/parsing_quirks_test.rb +116 -0
  59. data/test/integration/render_profiling_test.rb +154 -0
  60. data/test/{liquid → integration}/security_test.rb +10 -10
  61. data/test/{liquid → integration}/standard_filter_test.rb +148 -32
  62. data/test/{liquid → integration}/tags/break_tag_test.rb +1 -1
  63. data/test/{liquid → integration}/tags/continue_tag_test.rb +1 -1
  64. data/test/{liquid → integration}/tags/for_tag_test.rb +80 -2
  65. data/test/{liquid → integration}/tags/if_else_tag_test.rb +24 -21
  66. data/test/integration/tags/include_tag_test.rb +234 -0
  67. data/test/{liquid → integration}/tags/increment_tag_test.rb +1 -1
  68. data/test/{liquid → integration}/tags/raw_tag_test.rb +2 -1
  69. data/test/{liquid → integration}/tags/standard_tag_test.rb +28 -26
  70. data/test/integration/tags/statements_test.rb +113 -0
  71. data/test/{liquid/tags/html_tag_test.rb → integration/tags/table_row_test.rb} +5 -5
  72. data/test/{liquid → integration}/tags/unless_else_tag_test.rb +1 -1
  73. data/test/{liquid → integration}/template_test.rb +81 -45
  74. data/test/integration/variable_test.rb +82 -0
  75. data/test/test_helper.rb +73 -20
  76. data/test/{liquid/block_test.rb → unit/block_unit_test.rb} +2 -5
  77. data/test/{liquid/condition_test.rb → unit/condition_unit_test.rb} +23 -1
  78. data/test/{liquid/context_test.rb → unit/context_unit_test.rb} +39 -25
  79. data/test/{liquid/file_system_test.rb → unit/file_system_unit_test.rb} +11 -5
  80. data/test/unit/i18n_unit_test.rb +37 -0
  81. data/test/unit/lexer_unit_test.rb +48 -0
  82. data/test/{liquid/module_ex_test.rb → unit/module_ex_unit_test.rb} +7 -7
  83. data/test/unit/parser_unit_test.rb +82 -0
  84. data/test/{liquid/regexp_test.rb → unit/regexp_unit_test.rb} +3 -3
  85. data/test/{liquid/strainer_test.rb → unit/strainer_unit_test.rb} +20 -1
  86. data/test/unit/tag_unit_test.rb +16 -0
  87. data/test/unit/tags/case_tag_unit_test.rb +10 -0
  88. data/test/unit/tags/for_tag_unit_test.rb +13 -0
  89. data/test/unit/tags/if_tag_unit_test.rb +8 -0
  90. data/test/unit/template_unit_test.rb +69 -0
  91. data/test/unit/tokenizer_unit_test.rb +38 -0
  92. data/test/unit/variable_unit_test.rb +139 -0
  93. metadata +135 -67
  94. data/lib/extras/liquid_view.rb +0 -51
  95. data/lib/liquid/htmltags.rb +0 -73
  96. data/test/liquid/drop_test.rb +0 -180
  97. data/test/liquid/error_handling_test.rb +0 -81
  98. data/test/liquid/hash_ordering_test.rb +0 -25
  99. data/test/liquid/parsing_quirks_test.rb +0 -52
  100. data/test/liquid/tags/include_tag_test.rb +0 -166
  101. data/test/liquid/tags/statements_test.rb +0 -134
  102. data/test/liquid/variable_test.rb +0 -186
@@ -1,12 +1,11 @@
1
1
  module Liquid
2
-
3
2
  # increment is used in a place where one needs to insert a counter
4
3
  # into a template, and needs the counter to survive across
5
4
  # multiple instantiations of the template.
6
5
  # (To achieve the survival, the application must keep the context)
7
6
  #
8
7
  # if the variable does not exist, it is created with value 0.
9
-
8
+ #
10
9
  # Hello: {% increment variable %}
11
10
  #
12
11
  # gives you:
@@ -16,10 +15,9 @@ module Liquid
16
15
  # Hello: 2
17
16
  #
18
17
  class Increment < Tag
19
- def initialize(tag_name, markup, tokens)
20
- @variable = markup.strip
21
-
18
+ def initialize(tag_name, markup, options)
22
19
  super
20
+ @variable = markup.strip
23
21
  end
24
22
 
25
23
  def render(context)
@@ -27,9 +25,7 @@ module Liquid
27
25
  context.environments.first[@variable] = value + 1
28
26
  value.to_s
29
27
  end
30
-
31
- private
32
28
  end
33
29
 
34
- Template.register_tag('increment', Increment)
30
+ Template.register_tag('increment'.freeze, Increment)
35
31
  end
@@ -1,22 +1,19 @@
1
1
  module Liquid
2
2
  class Raw < Block
3
- FullTokenPossiblyInvalid = /^(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}$/o
3
+ FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
4
4
 
5
5
  def parse(tokens)
6
6
  @nodelist ||= []
7
7
  @nodelist.clear
8
8
  while token = tokens.shift
9
9
  if token =~ FullTokenPossiblyInvalid
10
- @nodelist << $1 if $1 != ""
11
- if block_delimiter == $2
12
- end_tag
13
- return
14
- end
10
+ @nodelist << $1 if $1 != "".freeze
11
+ return if block_delimiter == $2
15
12
  end
16
13
  @nodelist << token if not token.empty?
17
14
  end
18
15
  end
19
16
  end
20
17
 
21
- Template.register_tag('raw', Raw)
18
+ Template.register_tag('raw'.freeze, Raw)
22
19
  end
@@ -0,0 +1,73 @@
1
+ module Liquid
2
+ class TableRow < Block
3
+ Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
4
+
5
+ def initialize(tag_name, markup, options)
6
+ super
7
+ if markup =~ Syntax
8
+ @variable_name = $1
9
+ @collection_name = $2
10
+ @attributes = {}
11
+ markup.scan(TagAttributes) do |key, value|
12
+ @attributes[key] = value
13
+ end
14
+ else
15
+ raise SyntaxError.new(options[:locale].t("errors.syntax.table_row".freeze))
16
+ end
17
+ end
18
+
19
+ def render(context)
20
+ collection = context[@collection_name] or return ''.freeze
21
+
22
+ from = @attributes['offset'.freeze] ? context[@attributes['offset'.freeze]].to_i : 0
23
+ to = @attributes['limit'.freeze] ? from + context[@attributes['limit'.freeze]].to_i : nil
24
+
25
+ collection = Utils.slice_collection(collection, from, to)
26
+
27
+ length = collection.length
28
+
29
+ cols = context[@attributes['cols'.freeze]].to_i
30
+
31
+ row = 1
32
+ col = 0
33
+
34
+ result = "<tr class=\"row1\">\n"
35
+ context.stack do
36
+
37
+ collection.each_with_index do |item, index|
38
+ context[@variable_name] = item
39
+ context['tablerowloop'.freeze] = {
40
+ 'length'.freeze => length,
41
+ 'index'.freeze => index + 1,
42
+ 'index0'.freeze => index,
43
+ 'col'.freeze => col + 1,
44
+ 'col0'.freeze => col,
45
+ 'index0'.freeze => index,
46
+ 'rindex'.freeze => length - index,
47
+ 'rindex0'.freeze => length - index - 1,
48
+ 'first'.freeze => (index == 0),
49
+ 'last'.freeze => (index == length - 1),
50
+ 'col_first'.freeze => (col == 0),
51
+ 'col_last'.freeze => (col == cols - 1)
52
+ }
53
+
54
+
55
+ col += 1
56
+
57
+ result << "<td class=\"col#{col}\">" << super << '</td>'
58
+
59
+ if col == cols and (index != length - 1)
60
+ col = 0
61
+ row += 1
62
+ result << "</tr>\n<tr class=\"row#{row}\">"
63
+ end
64
+
65
+ end
66
+ end
67
+ result << "</tr>\n"
68
+ result
69
+ end
70
+ end
71
+
72
+ Template.register_tag('tablerow'.freeze, TableRow)
73
+ end
@@ -1,7 +1,6 @@
1
1
  require File.dirname(__FILE__) + '/if'
2
2
 
3
3
  module Liquid
4
-
5
4
  # Unless is a conditional just like 'if' but works on the inverse logic.
6
5
  #
7
6
  # {% unless x < 0 %} x is greater than zero {% end %}
@@ -23,11 +22,10 @@ module Liquid
23
22
  end
24
23
  end
25
24
 
26
- ''
25
+ ''.freeze
27
26
  end
28
27
  end
29
28
  end
30
29
 
31
-
32
- Template.register_tag('unless', Unless)
30
+ Template.register_tag('unless'.freeze, Unless)
33
31
  end
@@ -14,10 +14,58 @@ module Liquid
14
14
  # template.render('user_name' => 'bob')
15
15
  #
16
16
  class Template
17
+ DEFAULT_OPTIONS = {
18
+ :locale => I18n.new
19
+ }
20
+
17
21
  attr_accessor :root, :resource_limits
18
22
  @@file_system = BlankFileSystem.new
19
23
 
24
+ class TagRegistry
25
+ def initialize
26
+ @tags = {}
27
+ @cache = {}
28
+ end
29
+
30
+ def [](tag_name)
31
+ return nil unless @tags.has_key?(tag_name)
32
+ return @cache[tag_name] if Liquid.cache_classes
33
+
34
+ lookup_class(@tags[tag_name]).tap { |o| @cache[tag_name] = o }
35
+ end
36
+
37
+ def []=(tag_name, klass)
38
+ @tags[tag_name] = klass.name
39
+ @cache[tag_name] = klass
40
+ end
41
+
42
+ def delete(tag_name)
43
+ @tags.delete(tag_name)
44
+ @cache.delete(tag_name)
45
+ end
46
+
47
+ private
48
+
49
+ def lookup_class(name)
50
+ name.split("::").reject(&:empty?).reduce(Object) { |scope, const| scope.const_get(const) }
51
+ end
52
+ end
53
+
54
+ attr_reader :profiler
55
+
20
56
  class << self
57
+ # Sets how strict the parser should be.
58
+ # :lax acts like liquid 2.5 and silently ignores malformed tags in most cases.
59
+ # :warn is the default and will give deprecation warnings when invalid syntax is used.
60
+ # :strict will enforce correct syntax.
61
+ attr_writer :error_mode
62
+
63
+ # Sets how strict the taint checker should be.
64
+ # :lax is the default, and ignores the taint flag completely
65
+ # :warn adds a warning, but does not interrupt the rendering
66
+ # :error raises an error when tainted output is used
67
+ attr_writer :taint_mode
68
+
21
69
  def file_system
22
70
  @@file_system
23
71
  end
@@ -31,7 +79,15 @@ module Liquid
31
79
  end
32
80
 
33
81
  def tags
34
- @tags ||= {}
82
+ @tags ||= TagRegistry.new
83
+ end
84
+
85
+ def error_mode
86
+ @error_mode || :lax
87
+ end
88
+
89
+ def taint_mode
90
+ @taint_mode || :lax
35
91
  end
36
92
 
37
93
  # Pass a module with filter methods which should be available
@@ -40,26 +96,39 @@ module Liquid
40
96
  Strainer.global_filter(mod)
41
97
  end
42
98
 
99
+ def default_resource_limits
100
+ @default_resource_limits ||= {}
101
+ end
102
+
43
103
  # creates a new <tt>Template</tt> object from liquid source code
44
- def parse(source)
104
+ # To enable profiling, pass in <tt>profile: true</tt> as an option.
105
+ # See Liquid::Profiler for more information
106
+ def parse(source, options = {})
45
107
  template = Template.new
46
- template.parse(source)
47
- template
108
+ template.parse(source, options)
48
109
  end
49
110
  end
50
111
 
51
- # creates a new <tt>Template</tt> from an array of tokens. Use <tt>Template.parse</tt> instead
52
112
  def initialize
53
- @resource_limits = {}
113
+ @resource_limits = self.class.default_resource_limits.dup
54
114
  end
55
115
 
56
116
  # Parse source code.
57
117
  # Returns self for easy chaining
58
- def parse(source)
59
- @root = Document.new(tokenize(source))
118
+ def parse(source, options = {})
119
+ @options = options
120
+ @profiling = options[:profile]
121
+ @line_numbers = options[:line_numbers] || @profiling
122
+ @root = Document.parse(tokenize(source), DEFAULT_OPTIONS.merge(options))
123
+ @warnings = nil
60
124
  self
61
125
  end
62
126
 
127
+ def warnings
128
+ return [] unless @root
129
+ @warnings ||= @root.warnings
130
+ end
131
+
63
132
  def registers
64
133
  @registers ||= {}
65
134
  end
@@ -81,6 +150,9 @@ module Liquid
81
150
  # if you use the same filters over and over again consider registering them globally
82
151
  # with <tt>Template.register_filter</tt>
83
152
  #
153
+ # if profiling was enabled in <tt>Template#parse</tt> then the resulting profiling information
154
+ # will be available via <tt>Template#profiler</tt>
155
+ #
84
156
  # Following options can be passed:
85
157
  #
86
158
  # * <tt>filters</tt> : array with local filters
@@ -88,11 +160,17 @@ module Liquid
88
160
  # filters and tags and might be useful to integrate liquid more with its host application
89
161
  #
90
162
  def render(*args)
91
- return '' if @root.nil?
163
+ return ''.freeze if @root.nil?
92
164
 
93
165
  context = case args.first
94
166
  when Liquid::Context
95
- args.shift
167
+ c = args.shift
168
+
169
+ if @rethrow_errors
170
+ c.exception_handler = ->(e) { true }
171
+ end
172
+
173
+ c
96
174
  when Liquid::Drop
97
175
  drop = args.shift
98
176
  drop.context = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits)
@@ -101,7 +179,7 @@ module Liquid
101
179
  when nil
102
180
  Context.new(assigns, instance_assigns, registers, @rethrow_errors, @resource_limits)
103
181
  else
104
- raise ArgumentError, "Expect Hash or Liquid::Context as parameter"
182
+ raise ArgumentError, "Expected Hash or Liquid::Context as parameter"
105
183
  end
106
184
 
107
185
  case args.last
@@ -116,6 +194,9 @@ module Liquid
116
194
  context.add_filters(options[:filters])
117
195
  end
118
196
 
197
+ if options[:exception_handler]
198
+ context.exception_handler = options[:exception_handler]
199
+ end
119
200
  when Module
120
201
  context.add_filters(args.pop)
121
202
  when Array
@@ -125,7 +206,9 @@ module Liquid
125
206
  begin
126
207
  # render the nodelist.
127
208
  # for performance reasons we get an array back here. join will make a string out of it.
128
- result = @root.render(context)
209
+ result = with_profiling do
210
+ @root.render(context)
211
+ end
129
212
  result.respond_to?(:join) ? result.join : result
130
213
  rescue Liquid::MemoryError => e
131
214
  context.handle_error(e)
@@ -135,7 +218,8 @@ module Liquid
135
218
  end
136
219
 
137
220
  def render!(*args)
138
- @rethrow_errors = true; render(*args)
221
+ @rethrow_errors = true
222
+ render(*args)
139
223
  end
140
224
 
141
225
  private
@@ -144,7 +228,8 @@ module Liquid
144
228
  def tokenize(source)
145
229
  source = source.source if source.respond_to?(:source)
146
230
  return [] if source.to_s.empty?
147
- tokens = source.split(TemplateParser)
231
+
232
+ tokens = calculate_line_numbers(source.split(TemplateParser))
148
233
 
149
234
  # removes the rogue empty element at the beginning of the array
150
235
  tokens.shift if tokens[0] and tokens[0].empty?
@@ -152,5 +237,30 @@ module Liquid
152
237
  tokens
153
238
  end
154
239
 
240
+ def calculate_line_numbers(raw_tokens)
241
+ return raw_tokens unless @line_numbers
242
+
243
+ current_line = 1
244
+ raw_tokens.map do |token|
245
+ Token.new(token, current_line).tap do
246
+ current_line += token.count("\n")
247
+ end
248
+ end
249
+ end
250
+
251
+ def with_profiling
252
+ if @profiling && !@options[:included]
253
+ @profiler = Profiler.new
254
+ @profiler.start
255
+
256
+ begin
257
+ yield
258
+ ensure
259
+ @profiler.stop
260
+ end
261
+ else
262
+ yield
263
+ end
264
+ end
155
265
  end
156
266
  end
@@ -0,0 +1,18 @@
1
+ module Liquid
2
+ class Token < String
3
+ attr_reader :line_number
4
+
5
+ def initialize(content, line_number)
6
+ super(content)
7
+ @line_number = line_number
8
+ end
9
+
10
+ def raw
11
+ "<raw>"
12
+ end
13
+
14
+ def child(string)
15
+ Token.new(string, @line_number)
16
+ end
17
+ end
18
+ end
data/lib/liquid/utils.rb CHANGED
@@ -1,5 +1,18 @@
1
1
  module Liquid
2
2
  module Utils
3
+
4
+ def self.slice_collection(collection, from, to)
5
+ if (from != 0 || to != nil) && collection.respond_to?(:load_slice)
6
+ collection.load_slice(from, to)
7
+ else
8
+ slice_collection_using_each(collection, from, to)
9
+ end
10
+ end
11
+
12
+ def self.non_blank_string?(collection)
13
+ collection.is_a?(String) && collection != ''.freeze
14
+ end
15
+
3
16
  def self.slice_collection_using_each(collection, from, to)
4
17
  segments = []
5
18
  index = 0
@@ -22,9 +35,5 @@ module Liquid
22
35
 
23
36
  segments
24
37
  end
25
-
26
- def self.non_blank_string?(collection)
27
- collection.is_a?(String) && collection != ''
28
- end
29
38
  end
30
39
  end
@@ -11,45 +11,123 @@ module Liquid
11
11
  # {{ user | link }}
12
12
  #
13
13
  class Variable
14
- FilterParser = /(?:#{FilterSeparator}|(?:\s*(?:#{QuotedFragment}|#{ArgumentSeparator})\s*)+)/o
15
- attr_accessor :filters, :name
14
+ FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o
15
+ EasyParse = /\A *(\w+(?:\.\w+)*) *\z/
16
+ attr_accessor :filters, :name, :warnings
17
+ attr_accessor :line_number
18
+ include ParserSwitching
16
19
 
17
- def initialize(markup)
20
+ def initialize(markup, options = {})
18
21
  @markup = markup
19
22
  @name = nil
23
+ @options = options || {}
24
+
25
+ parse_with_selected_parser(markup)
26
+ end
27
+
28
+ def raw
29
+ @markup
30
+ end
31
+
32
+ def markup_context(markup)
33
+ "in \"{{#{markup}}}\""
34
+ end
35
+
36
+ def lax_parse(markup)
20
37
  @filters = []
21
- if match = markup.match(/\s*(#{QuotedFragment})(.*)/o)
22
- @name = match[1]
23
- if match[2].match(/#{FilterSeparator}\s*(.*)/o)
24
- filters = Regexp.last_match(1).scan(FilterParser)
38
+ if markup =~ /(#{QuotedFragment})(.*)/om
39
+ name_markup = $1
40
+ filter_markup = $2
41
+ @name = Expression.parse(name_markup)
42
+ if filter_markup =~ /#{FilterSeparator}\s*(.*)/om
43
+ filters = $1.scan(FilterParser)
25
44
  filters.each do |f|
26
- if matches = f.match(/\s*(\w+)/)
27
- filtername = matches[1]
45
+ if f =~ /\w+/
46
+ filtername = Regexp.last_match(0)
28
47
  filterargs = f.scan(/(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o).flatten
29
- @filters << [filtername, filterargs]
48
+ @filters << parse_filter_expressions(filtername, filterargs)
30
49
  end
31
50
  end
32
51
  end
33
52
  end
34
53
  end
35
54
 
55
+ def strict_parse(markup)
56
+ # Very simple valid cases
57
+ if markup =~ EasyParse
58
+ @name = Expression.parse($1)
59
+ @filters = []
60
+ return
61
+ end
62
+
63
+ @filters = []
64
+ p = Parser.new(markup)
65
+ # Could be just filters with no input
66
+ @name = p.look(:pipe) ? nil : Expression.parse(p.expression)
67
+ while p.consume?(:pipe)
68
+ filtername = p.consume(:id)
69
+ filterargs = p.consume?(:colon) ? parse_filterargs(p) : []
70
+ @filters << parse_filter_expressions(filtername, filterargs)
71
+ end
72
+ p.consume(:end_of_string)
73
+ end
74
+
75
+ def parse_filterargs(p)
76
+ # first argument
77
+ filterargs = [p.argument]
78
+ # followed by comma separated others
79
+ while p.consume?(:comma)
80
+ filterargs << p.argument
81
+ end
82
+ filterargs
83
+ end
84
+
36
85
  def render(context)
37
- return '' if @name.nil?
38
- @filters.inject(context[@name]) do |output, filter|
39
- filterargs = []
40
- keyword_args = {}
41
- filter[1].to_a.each do |a|
42
- if matches = a.match(/\A#{TagAttributes}\z/o)
43
- keyword_args[matches[1]] = context[matches[2]]
44
- else
45
- filterargs << context[a]
46
- end
86
+ @filters.inject(context.evaluate(@name)) do |output, (filter_name, filter_args, filter_kwargs)|
87
+ filter_args = evaluate_filter_expressions(context, filter_args, filter_kwargs)
88
+ output = context.invoke(filter_name, output, *filter_args)
89
+ end.tap{ |obj| taint_check(obj) }
90
+ end
91
+
92
+ private
93
+
94
+ def parse_filter_expressions(filter_name, unparsed_args)
95
+ filter_args = []
96
+ keyword_args = {}
97
+ unparsed_args.each do |a|
98
+ if matches = a.match(/\A#{TagAttributes}\z/o)
99
+ keyword_args[matches[1]] = Expression.parse(matches[2])
100
+ else
101
+ filter_args << Expression.parse(a)
47
102
  end
48
- filterargs << keyword_args unless keyword_args.empty?
49
- begin
50
- output = context.invoke(filter[0], output, *filterargs)
51
- rescue FilterNotFound
52
- raise FilterNotFound, "Error - filter '#{filter[0]}' in '#{@markup.strip}' could not be found."
103
+ end
104
+ result = [filter_name, filter_args]
105
+ result << keyword_args unless keyword_args.empty?
106
+ result
107
+ end
108
+
109
+ def evaluate_filter_expressions(context, filter_args, filter_kwargs)
110
+ parsed_args = filter_args.map{ |expr| context.evaluate(expr) }
111
+ if filter_kwargs
112
+ parsed_kwargs = {}
113
+ filter_kwargs.each do |key, expr|
114
+ parsed_kwargs[key] = context.evaluate(expr)
115
+ end
116
+ parsed_args << parsed_kwargs
117
+ end
118
+ parsed_args
119
+ end
120
+
121
+ def taint_check(obj)
122
+ if obj.tainted?
123
+ @markup =~ QuotedFragment
124
+ name = Regexp.last_match(0)
125
+ case Template.taint_mode
126
+ when :warn
127
+ @warnings ||= []
128
+ @warnings << "variable '#{name}' is tainted and was not escaped"
129
+ when :error
130
+ raise TaintedError, "Error - variable '#{name}' is tainted and was not escaped"
53
131
  end
54
132
  end
55
133
  end
@@ -0,0 +1,78 @@
1
+ module Liquid
2
+ class VariableLookup
3
+ SQUARE_BRACKETED = /\A\[(.*)\]\z/m
4
+ COMMAND_METHODS = ['size'.freeze, 'first'.freeze, 'last'.freeze]
5
+
6
+ def self.parse(markup)
7
+ new(markup)
8
+ end
9
+
10
+ def initialize(markup)
11
+ lookups = markup.scan(VariableParser)
12
+
13
+ name = lookups.shift
14
+ if name =~ SQUARE_BRACKETED
15
+ name = Expression.parse($1)
16
+ end
17
+ @name = name
18
+
19
+ @lookups = lookups
20
+ @command_flags = 0
21
+
22
+ @lookups.each_index do |i|
23
+ lookup = lookups[i]
24
+ if lookup =~ SQUARE_BRACKETED
25
+ lookups[i] = Expression.parse($1)
26
+ elsif COMMAND_METHODS.include?(lookup)
27
+ @command_flags |= 1 << i
28
+ end
29
+ end
30
+ end
31
+
32
+ def evaluate(context)
33
+ name = context.evaluate(@name)
34
+ object = context.find_variable(name)
35
+
36
+ @lookups.each_index do |i|
37
+ key = context.evaluate(@lookups[i])
38
+
39
+ # If object is a hash- or array-like object we look for the
40
+ # presence of the key and if its available we return it
41
+ if object.respond_to?(:[]) &&
42
+ ((object.respond_to?(:has_key?) && object.has_key?(key)) ||
43
+ (object.respond_to?(:fetch) && key.is_a?(Integer)))
44
+
45
+ # if its a proc we will replace the entry with the proc
46
+ res = context.lookup_and_evaluate(object, key)
47
+ object = res.to_liquid
48
+
49
+ # Some special cases. If the part wasn't in square brackets and
50
+ # no key with the same name was found we interpret following calls
51
+ # as commands and call them on the current object
52
+ elsif @command_flags & (1 << i) != 0 && object.respond_to?(key)
53
+ object = object.send(key).to_liquid
54
+
55
+ # No key was present with the desired value and it wasn't one of the directly supported
56
+ # keywords either. The only thing we got left is to return nil
57
+ else
58
+ return nil
59
+ end
60
+
61
+ # If we are dealing with a drop here we have to
62
+ object.context = context if object.respond_to?(:context=)
63
+ end
64
+
65
+ object
66
+ end
67
+
68
+ def ==(other)
69
+ self.class == other.class && self.state == other.state
70
+ end
71
+
72
+ protected
73
+
74
+ def state
75
+ [@name, @lookups, @command_flags]
76
+ end
77
+ end
78
+ end
@@ -1,4 +1,4 @@
1
1
  # encoding: utf-8
2
2
  module Liquid
3
- VERSION = "2.6.3"
3
+ VERSION = "3.0.0"
4
4
  end