liquid 3.0.0.rc1 → 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 (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