liquid 3.0.0.rc1 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +4 -0
  3. data/README.md +2 -2
  4. data/lib/liquid.rb +8 -0
  5. data/lib/liquid/block.rb +50 -46
  6. data/lib/liquid/block_body.rb +123 -0
  7. data/lib/liquid/condition.rb +12 -5
  8. data/lib/liquid/context.rb +75 -148
  9. data/lib/liquid/errors.rb +50 -2
  10. data/lib/liquid/expression.rb +33 -0
  11. data/lib/liquid/parser_switching.rb +31 -0
  12. data/lib/liquid/profiler.rb +159 -0
  13. data/lib/liquid/profiler/hooks.rb +23 -0
  14. data/lib/liquid/range_lookup.rb +22 -0
  15. data/lib/liquid/standardfilters.rb +29 -4
  16. data/lib/liquid/tag.rb +6 -25
  17. data/lib/liquid/tags/assign.rb +2 -1
  18. data/lib/liquid/tags/case.rb +1 -1
  19. data/lib/liquid/tags/if.rb +5 -5
  20. data/lib/liquid/tags/ifchanged.rb +1 -1
  21. data/lib/liquid/tags/include.rb +11 -1
  22. data/lib/liquid/tags/raw.rb +1 -4
  23. data/lib/liquid/tags/table_row.rb +1 -1
  24. data/lib/liquid/template.rb +55 -4
  25. data/lib/liquid/token.rb +18 -0
  26. data/lib/liquid/variable.rb +68 -41
  27. data/lib/liquid/variable_lookup.rb +78 -0
  28. data/lib/liquid/version.rb +1 -1
  29. data/test/integration/assign_test.rb +12 -1
  30. data/test/integration/blank_test.rb +1 -1
  31. data/test/integration/capture_test.rb +1 -1
  32. data/test/integration/context_test.rb +10 -11
  33. data/test/integration/drop_test.rb +29 -3
  34. data/test/integration/error_handling_test.rb +138 -41
  35. data/test/integration/filter_test.rb +7 -7
  36. data/test/integration/hash_ordering_test.rb +6 -8
  37. data/test/integration/output_test.rb +1 -1
  38. data/test/integration/parsing_quirks_test.rb +40 -18
  39. data/test/integration/render_profiling_test.rb +154 -0
  40. data/test/integration/security_test.rb +1 -1
  41. data/test/integration/standard_filter_test.rb +47 -1
  42. data/test/integration/tags/break_tag_test.rb +1 -1
  43. data/test/integration/tags/continue_tag_test.rb +1 -1
  44. data/test/integration/tags/for_tag_test.rb +2 -2
  45. data/test/integration/tags/if_else_tag_test.rb +23 -20
  46. data/test/integration/tags/include_tag_test.rb +24 -2
  47. data/test/integration/tags/increment_tag_test.rb +1 -1
  48. data/test/integration/tags/raw_tag_test.rb +1 -1
  49. data/test/integration/tags/standard_tag_test.rb +4 -4
  50. data/test/integration/tags/statements_test.rb +1 -1
  51. data/test/integration/tags/table_row_test.rb +1 -1
  52. data/test/integration/tags/unless_else_tag_test.rb +1 -1
  53. data/test/integration/template_test.rb +16 -4
  54. data/test/integration/variable_test.rb +11 -1
  55. data/test/test_helper.rb +59 -31
  56. data/test/unit/block_unit_test.rb +2 -5
  57. data/test/unit/condition_unit_test.rb +5 -1
  58. data/test/unit/context_unit_test.rb +13 -7
  59. data/test/unit/file_system_unit_test.rb +5 -5
  60. data/test/unit/i18n_unit_test.rb +3 -3
  61. data/test/unit/lexer_unit_test.rb +1 -1
  62. data/test/unit/module_ex_unit_test.rb +1 -1
  63. data/test/unit/parser_unit_test.rb +1 -1
  64. data/test/unit/regexp_unit_test.rb +1 -1
  65. data/test/unit/strainer_unit_test.rb +3 -2
  66. data/test/unit/tag_unit_test.rb +6 -1
  67. data/test/unit/tags/case_tag_unit_test.rb +1 -1
  68. data/test/unit/tags/for_tag_unit_test.rb +1 -1
  69. data/test/unit/tags/if_tag_unit_test.rb +1 -1
  70. data/test/unit/template_unit_test.rb +1 -1
  71. data/test/unit/tokenizer_unit_test.rb +10 -1
  72. data/test/unit/variable_unit_test.rb +49 -46
  73. metadata +71 -47
@@ -15,7 +15,8 @@ module Liquid
15
15
  super
16
16
  if markup =~ Syntax
17
17
  @to = $1
18
- @from = Variable.new($2)
18
+ @from = Variable.new($2,options)
19
+ @from.line_number = line_number
19
20
  else
20
21
  raise SyntaxError.new options[:locale].t("errors.syntax.assign".freeze)
21
22
  end
@@ -15,7 +15,7 @@ module Liquid
15
15
  end
16
16
 
17
17
  def nodelist
18
- @blocks.map(&:attachment).flatten
18
+ @blocks.flat_map(&:attachment)
19
19
  end
20
20
 
21
21
  def unknown_tag(tag, markup, tokens)
@@ -21,7 +21,7 @@ module Liquid
21
21
  end
22
22
 
23
23
  def nodelist
24
- @blocks.map(&:attachment).flatten
24
+ @blocks.flat_map(&:attachment)
25
25
  end
26
26
 
27
27
  def unknown_tag(tag, markup, tokens)
@@ -57,15 +57,15 @@ module Liquid
57
57
  end
58
58
 
59
59
  def lax_parse(markup)
60
- expressions = markup.scan(ExpressionsAndOperators).reverse
61
- raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.shift =~ Syntax
60
+ expressions = markup.scan(ExpressionsAndOperators)
61
+ raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop =~ Syntax
62
62
 
63
63
  condition = Condition.new($1, $2, $3)
64
64
 
65
65
  while not expressions.empty?
66
- operator = (expressions.shift).to_s.strip
66
+ operator = expressions.pop.to_s.strip
67
67
 
68
- raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.shift.to_s =~ Syntax
68
+ raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop.to_s =~ Syntax
69
69
 
70
70
  new_condition = Condition.new($1, $2, $3)
71
71
  raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless BOOLEAN_OPERATORS.include?(operator)
@@ -4,7 +4,7 @@ module Liquid
4
4
  def render(context)
5
5
  context.stack do
6
6
 
7
- output = render_all(@nodelist, context)
7
+ output = super
8
8
 
9
9
  if output != context.registers[:ifchanged]
10
10
  context.registers[:ifchanged] = output
@@ -69,7 +69,7 @@ module Liquid
69
69
  return cached
70
70
  end
71
71
  source = read_template_from_file_system(context)
72
- partial = Liquid::Template.parse(source)
72
+ partial = Liquid::Template.parse(source, pass_options)
73
73
  cached_partials[template_name] = partial
74
74
  context.registers[:cached_partials] = cached_partials
75
75
  partial
@@ -88,6 +88,16 @@ module Liquid
88
88
  raise ArgumentError, "file_system.read_template_file expects two parameters: (template_name, context)"
89
89
  end
90
90
  end
91
+
92
+ def pass_options
93
+ dont_pass = @options[:include_options_blacklist]
94
+ return {locale: @options[:locale]} if dont_pass == true
95
+ opts = @options.merge(included: true, include_options_blacklist: false)
96
+ if dont_pass.is_a?(Array)
97
+ dont_pass.each {|o| opts.delete(o)}
98
+ end
99
+ opts
100
+ end
91
101
  end
92
102
 
93
103
  Template.register_tag('include'.freeze, Include)
@@ -8,10 +8,7 @@ module Liquid
8
8
  while token = tokens.shift
9
9
  if token =~ FullTokenPossiblyInvalid
10
10
  @nodelist << $1 if $1 != "".freeze
11
- if block_delimiter == $2
12
- end_tag
13
- return
14
- end
11
+ return if block_delimiter == $2
15
12
  end
16
13
  @nodelist << token if not token.empty?
17
14
  end
@@ -54,7 +54,7 @@ module Liquid
54
54
 
55
55
  col += 1
56
56
 
57
- result << "<td class=\"col#{col}\">" << render_all(@nodelist, context) << '</td>'
57
+ result << "<td class=\"col#{col}\">" << super << '</td>'
58
58
 
59
59
  if col == cols and (index != length - 1)
60
60
  col = 0
@@ -51,6 +51,8 @@ module Liquid
51
51
  end
52
52
  end
53
53
 
54
+ attr_reader :profiler
55
+
54
56
  class << self
55
57
  # Sets how strict the parser should be.
56
58
  # :lax acts like liquid 2.5 and silently ignores malformed tags in most cases.
@@ -58,6 +60,12 @@ module Liquid
58
60
  # :strict will enforce correct syntax.
59
61
  attr_writer :error_mode
60
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
+
61
69
  def file_system
62
70
  @@file_system
63
71
  end
@@ -78,27 +86,39 @@ module Liquid
78
86
  @error_mode || :lax
79
87
  end
80
88
 
89
+ def taint_mode
90
+ @taint_mode || :lax
91
+ end
92
+
81
93
  # Pass a module with filter methods which should be available
82
94
  # to all liquid views. Good for registering the standard library
83
95
  def register_filter(mod)
84
96
  Strainer.global_filter(mod)
85
97
  end
86
98
 
99
+ def default_resource_limits
100
+ @default_resource_limits ||= {}
101
+ end
102
+
87
103
  # creates a new <tt>Template</tt> object from liquid source code
104
+ # To enable profiling, pass in <tt>profile: true</tt> as an option.
105
+ # See Liquid::Profiler for more information
88
106
  def parse(source, options = {})
89
107
  template = Template.new
90
108
  template.parse(source, options)
91
109
  end
92
110
  end
93
111
 
94
- # creates a new <tt>Template</tt> from an array of tokens. Use <tt>Template.parse</tt> instead
95
112
  def initialize
96
- @resource_limits = {}
113
+ @resource_limits = self.class.default_resource_limits.dup
97
114
  end
98
115
 
99
116
  # Parse source code.
100
117
  # Returns self for easy chaining
101
118
  def parse(source, options = {})
119
+ @options = options
120
+ @profiling = options[:profile]
121
+ @line_numbers = options[:line_numbers] || @profiling
102
122
  @root = Document.parse(tokenize(source), DEFAULT_OPTIONS.merge(options))
103
123
  @warnings = nil
104
124
  self
@@ -130,6 +150,9 @@ module Liquid
130
150
  # if you use the same filters over and over again consider registering them globally
131
151
  # with <tt>Template.register_filter</tt>
132
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
+ #
133
156
  # Following options can be passed:
134
157
  #
135
158
  # * <tt>filters</tt> : array with local filters
@@ -183,7 +206,9 @@ module Liquid
183
206
  begin
184
207
  # render the nodelist.
185
208
  # for performance reasons we get an array back here. join will make a string out of it.
186
- result = @root.render(context)
209
+ result = with_profiling do
210
+ @root.render(context)
211
+ end
187
212
  result.respond_to?(:join) ? result.join : result
188
213
  rescue Liquid::MemoryError => e
189
214
  context.handle_error(e)
@@ -203,7 +228,8 @@ module Liquid
203
228
  def tokenize(source)
204
229
  source = source.source if source.respond_to?(:source)
205
230
  return [] if source.to_s.empty?
206
- tokens = source.split(TemplateParser)
231
+
232
+ tokens = calculate_line_numbers(source.split(TemplateParser))
207
233
 
208
234
  # removes the rogue empty element at the beginning of the array
209
235
  tokens.shift if tokens[0] and tokens[0].empty?
@@ -211,5 +237,30 @@ module Liquid
211
237
  tokens
212
238
  end
213
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
214
265
  end
215
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
@@ -11,40 +11,41 @@ module Liquid
11
11
  # {{ user | link }}
12
12
  #
13
13
  class Variable
14
- FilterParser = /(?:#{FilterSeparator}|(?:\s*(?:#{QuotedFragment}|#{ArgumentSeparator})\s*)+)/o
14
+ FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o
15
15
  EasyParse = /\A *(\w+(?:\.\w+)*) *\z/
16
16
  attr_accessor :filters, :name, :warnings
17
+ attr_accessor :line_number
18
+ include ParserSwitching
17
19
 
18
20
  def initialize(markup, options = {})
19
21
  @markup = markup
20
22
  @name = nil
21
23
  @options = options || {}
22
24
 
23
- case @options[:error_mode] || Template.error_mode
24
- when :strict then strict_parse(markup)
25
- when :lax then lax_parse(markup)
26
- when :warn
27
- begin
28
- strict_parse(markup)
29
- rescue SyntaxError => e
30
- @warnings ||= []
31
- @warnings << e
32
- lax_parse(markup)
33
- end
34
- end
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}}}\""
35
34
  end
36
35
 
37
36
  def lax_parse(markup)
38
37
  @filters = []
39
- if markup =~ /\s*(#{QuotedFragment})(.*)/om
40
- @name = Regexp.last_match(1)
41
- if Regexp.last_match(2) =~ /#{FilterSeparator}\s*(.*)/om
42
- 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)
43
44
  filters.each do |f|
44
- if f =~ /\s*(\w+)/
45
- filtername = Regexp.last_match(1)
45
+ if f =~ /\w+/
46
+ filtername = Regexp.last_match(0)
46
47
  filterargs = f.scan(/(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o).flatten
47
- @filters << [filtername, filterargs]
48
+ @filters << parse_filter_expressions(filtername, filterargs)
48
49
  end
49
50
  end
50
51
  end
@@ -54,7 +55,7 @@ module Liquid
54
55
  def strict_parse(markup)
55
56
  # Very simple valid cases
56
57
  if markup =~ EasyParse
57
- @name = $1
58
+ @name = Expression.parse($1)
58
59
  @filters = []
59
60
  return
60
61
  end
@@ -62,16 +63,13 @@ module Liquid
62
63
  @filters = []
63
64
  p = Parser.new(markup)
64
65
  # Could be just filters with no input
65
- @name = p.look(:pipe) ? ''.freeze : p.expression
66
+ @name = p.look(:pipe) ? nil : Expression.parse(p.expression)
66
67
  while p.consume?(:pipe)
67
68
  filtername = p.consume(:id)
68
69
  filterargs = p.consume?(:colon) ? parse_filterargs(p) : []
69
- @filters << [filtername, filterargs]
70
+ @filters << parse_filter_expressions(filtername, filterargs)
70
71
  end
71
72
  p.consume(:end_of_string)
72
- rescue SyntaxError => e
73
- e.message << " in \"{{#{markup}}}\""
74
- raise e
75
73
  end
76
74
 
77
75
  def parse_filterargs(p)
@@ -85,22 +83,51 @@ module Liquid
85
83
  end
86
84
 
87
85
  def render(context)
88
- return ''.freeze if @name.nil?
89
- @filters.inject(context[@name]) do |output, filter|
90
- filterargs = []
91
- keyword_args = {}
92
- filter[1].to_a.each do |a|
93
- if matches = a.match(/\A#{TagAttributes}\z/o)
94
- keyword_args[matches[1]] = context[matches[2]]
95
- else
96
- filterargs << context[a]
97
- 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)
102
+ end
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)
98
115
  end
99
- filterargs << keyword_args unless keyword_args.empty?
100
- begin
101
- output = context.invoke(filter[0], output, *filterargs)
102
- rescue FilterNotFound
103
- raise FilterNotFound, "Error - filter '#{filter[0]}' in '#{@markup.strip}' could not be found."
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"
104
131
  end
105
132
  end
106
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