liquid 3.0.6 → 4.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (95) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +89 -58
  3. data/{MIT-LICENSE → LICENSE} +0 -0
  4. data/lib/liquid.rb +7 -6
  5. data/lib/liquid/block.rb +31 -124
  6. data/lib/liquid/block_body.rb +54 -57
  7. data/lib/liquid/condition.rb +23 -22
  8. data/lib/liquid/context.rb +50 -42
  9. data/lib/liquid/document.rb +19 -9
  10. data/lib/liquid/drop.rb +12 -13
  11. data/lib/liquid/errors.rb +16 -17
  12. data/lib/liquid/expression.rb +15 -3
  13. data/lib/liquid/extensions.rb +7 -7
  14. data/lib/liquid/file_system.rb +3 -3
  15. data/lib/liquid/forloop_drop.rb +42 -0
  16. data/lib/liquid/i18n.rb +5 -5
  17. data/lib/liquid/interrupts.rb +1 -2
  18. data/lib/liquid/lexer.rb +6 -4
  19. data/lib/liquid/locales/en.yml +3 -1
  20. data/lib/liquid/parse_context.rb +37 -0
  21. data/lib/liquid/parser_switching.rb +4 -4
  22. data/lib/liquid/profiler.rb +18 -19
  23. data/lib/liquid/profiler/hooks.rb +7 -7
  24. data/lib/liquid/range_lookup.rb +16 -1
  25. data/lib/liquid/resource_limits.rb +23 -0
  26. data/lib/liquid/standardfilters.rb +101 -56
  27. data/lib/liquid/strainer.rb +4 -5
  28. data/lib/liquid/tablerowloop_drop.rb +62 -0
  29. data/lib/liquid/tag.rb +9 -8
  30. data/lib/liquid/tags/assign.rb +5 -4
  31. data/lib/liquid/tags/break.rb +0 -3
  32. data/lib/liquid/tags/capture.rb +1 -1
  33. data/lib/liquid/tags/case.rb +19 -12
  34. data/lib/liquid/tags/comment.rb +2 -2
  35. data/lib/liquid/tags/cycle.rb +6 -6
  36. data/lib/liquid/tags/decrement.rb +1 -4
  37. data/lib/liquid/tags/for.rb +93 -75
  38. data/lib/liquid/tags/if.rb +49 -44
  39. data/lib/liquid/tags/ifchanged.rb +0 -2
  40. data/lib/liquid/tags/include.rb +60 -52
  41. data/lib/liquid/tags/raw.rb +26 -4
  42. data/lib/liquid/tags/table_row.rb +12 -30
  43. data/lib/liquid/tags/unless.rb +3 -4
  44. data/lib/liquid/template.rb +23 -50
  45. data/lib/liquid/tokenizer.rb +31 -0
  46. data/lib/liquid/utils.rb +48 -8
  47. data/lib/liquid/variable.rb +46 -45
  48. data/lib/liquid/variable_lookup.rb +3 -3
  49. data/lib/liquid/version.rb +1 -1
  50. data/test/integration/assign_test.rb +8 -8
  51. data/test/integration/blank_test.rb +14 -14
  52. data/test/integration/context_test.rb +2 -2
  53. data/test/integration/document_test.rb +19 -0
  54. data/test/integration/drop_test.rb +42 -40
  55. data/test/integration/error_handling_test.rb +64 -45
  56. data/test/integration/filter_test.rb +60 -20
  57. data/test/integration/output_test.rb +26 -27
  58. data/test/integration/parsing_quirks_test.rb +15 -13
  59. data/test/integration/render_profiling_test.rb +20 -20
  60. data/test/integration/security_test.rb +5 -7
  61. data/test/integration/standard_filter_test.rb +119 -37
  62. data/test/integration/tags/break_tag_test.rb +1 -2
  63. data/test/integration/tags/continue_tag_test.rb +0 -1
  64. data/test/integration/tags/for_tag_test.rb +133 -98
  65. data/test/integration/tags/if_else_tag_test.rb +75 -77
  66. data/test/integration/tags/include_tag_test.rb +23 -30
  67. data/test/integration/tags/increment_tag_test.rb +10 -11
  68. data/test/integration/tags/raw_tag_test.rb +7 -1
  69. data/test/integration/tags/standard_tag_test.rb +121 -122
  70. data/test/integration/tags/statements_test.rb +3 -5
  71. data/test/integration/tags/table_row_test.rb +20 -19
  72. data/test/integration/tags/unless_else_tag_test.rb +6 -6
  73. data/test/integration/template_test.rb +91 -45
  74. data/test/integration/variable_test.rb +23 -13
  75. data/test/test_helper.rb +33 -5
  76. data/test/unit/block_unit_test.rb +6 -5
  77. data/test/unit/condition_unit_test.rb +82 -77
  78. data/test/unit/context_unit_test.rb +48 -57
  79. data/test/unit/file_system_unit_test.rb +3 -3
  80. data/test/unit/i18n_unit_test.rb +2 -2
  81. data/test/unit/lexer_unit_test.rb +11 -8
  82. data/test/unit/parser_unit_test.rb +2 -2
  83. data/test/unit/regexp_unit_test.rb +1 -1
  84. data/test/unit/strainer_unit_test.rb +13 -2
  85. data/test/unit/tag_unit_test.rb +7 -2
  86. data/test/unit/tags/case_tag_unit_test.rb +1 -1
  87. data/test/unit/tags/for_tag_unit_test.rb +2 -2
  88. data/test/unit/tags/if_tag_unit_test.rb +1 -1
  89. data/test/unit/template_unit_test.rb +6 -5
  90. data/test/unit/tokenizer_unit_test.rb +24 -7
  91. data/test/unit/variable_unit_test.rb +60 -43
  92. metadata +44 -41
  93. data/lib/liquid/module_ex.rb +0 -62
  94. data/lib/liquid/token.rb +0 -18
  95. data/test/unit/module_ex_unit_test.rb +0 -87
@@ -1,9 +1,7 @@
1
1
  module Liquid
2
2
  class Ifchanged < Block
3
-
4
3
  def render(context)
5
4
  context.stack do
6
-
7
5
  output = super
8
6
 
9
7
  if output != context.registers[:ifchanged]
@@ -1,5 +1,4 @@
1
1
  module Liquid
2
-
3
2
  # Include allows templates to relate with other templates
4
3
  #
5
4
  # Simply include another template:
@@ -22,12 +21,15 @@ module Liquid
22
21
 
23
22
  if markup =~ Syntax
24
23
 
25
- @template_name = $1
26
- @variable_name = $3
27
- @attributes = {}
24
+ template_name = $1
25
+ variable_name = $3
26
+
27
+ @variable_name_expr = variable_name ? Expression.parse(variable_name) : nil
28
+ @template_name_expr = Expression.parse(template_name)
29
+ @attributes = {}
28
30
 
29
31
  markup.scan(TagAttributes) do |key, value|
30
- @attributes[key] = value
32
+ @attributes[key] = Expression.parse(value)
31
33
  end
32
34
 
33
35
  else
@@ -35,69 +37,75 @@ module Liquid
35
37
  end
36
38
  end
37
39
 
38
- def parse(tokens)
40
+ def parse(_tokens)
39
41
  end
40
42
 
41
43
  def render(context)
42
- partial = load_cached_partial(context)
43
- variable = context[@variable_name || @template_name[1..-2]]
44
+ template_name = context.evaluate(@template_name_expr)
45
+ partial = load_cached_partial(template_name, context)
44
46
 
45
- context.stack do
46
- @attributes.each do |key, value|
47
- context[key] = context[value]
48
- end
47
+ context_variable_name = template_name.split('/'.freeze).last
49
48
 
50
- context_variable_name = @template_name[1..-2].split('/'.freeze).last
51
- if variable.is_a?(Array)
52
- variable.collect do |var|
53
- context[context_variable_name] = var
49
+ variable = if @variable_name_expr
50
+ context.evaluate(@variable_name_expr)
51
+ else
52
+ context.find_variable(template_name)
53
+ end
54
+
55
+ old_template_name = context.template_name
56
+ old_partial = context.partial
57
+ begin
58
+ context.template_name = template_name
59
+ context.partial = true
60
+ context.stack do
61
+ @attributes.each do |key, value|
62
+ context[key] = context.evaluate(value)
63
+ end
64
+
65
+ if variable.is_a?(Array)
66
+ variable.collect do |var|
67
+ context[context_variable_name] = var
68
+ partial.render(context)
69
+ end
70
+ else
71
+ context[context_variable_name] = variable
54
72
  partial.render(context)
55
73
  end
56
- else
57
- context[context_variable_name] = variable
58
- partial.render(context)
59
74
  end
75
+ ensure
76
+ context.template_name = old_template_name
77
+ context.partial = old_partial
60
78
  end
61
79
  end
62
80
 
63
81
  private
64
- def load_cached_partial(context)
65
- cached_partials = context.registers[:cached_partials] || {}
66
- template_name = context[@template_name]
67
82
 
68
- if cached = cached_partials[template_name]
69
- return cached
70
- end
71
- source = read_template_from_file_system(context)
72
- partial = Liquid::Template.parse(source, pass_options)
73
- cached_partials[template_name] = partial
74
- context.registers[:cached_partials] = cached_partials
75
- partial
76
- end
83
+ alias_method :parse_context, :options
84
+ private :parse_context
77
85
 
78
- def read_template_from_file_system(context)
79
- file_system = context.registers[:file_system] || Liquid::Template.file_system
80
-
81
- # make read_template_file call backwards-compatible.
82
- case file_system.method(:read_template_file).arity
83
- when 1
84
- file_system.read_template_file(context[@template_name])
85
- when 2
86
- file_system.read_template_file(context[@template_name], context)
87
- else
88
- raise ArgumentError, "file_system.read_template_file expects two parameters: (template_name, context)"
89
- end
90
- end
86
+ def load_cached_partial(template_name, context)
87
+ cached_partials = context.registers[:cached_partials] || {}
91
88
 
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
89
+ if cached = cached_partials[template_name]
90
+ return cached
91
+ end
92
+ source = read_template_from_file_system(context)
93
+ begin
94
+ parse_context.partial = true
95
+ partial = Liquid::Template.parse(source, parse_context)
96
+ ensure
97
+ parse_context.partial = false
100
98
  end
99
+ cached_partials[template_name] = partial
100
+ context.registers[:cached_partials] = cached_partials
101
+ partial
102
+ end
103
+
104
+ def read_template_from_file_system(context)
105
+ file_system = context.registers[:file_system] || Liquid::Template.file_system
106
+
107
+ file_system.read_template_file(context.evaluate(@template_name_expr))
108
+ end
101
109
  end
102
110
 
103
111
  Template.register_tag('include'.freeze, Include)
@@ -1,17 +1,39 @@
1
1
  module Liquid
2
2
  class Raw < Block
3
+ Syntax = /\A\s*\z/
3
4
  FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
4
5
 
6
+ def initialize(tag_name, markup, parse_context)
7
+ super
8
+
9
+ unless markup =~ Syntax
10
+ raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_unexpected_args".freeze, tag: tag_name))
11
+ end
12
+ end
13
+
5
14
  def parse(tokens)
6
- @nodelist ||= []
7
- @nodelist.clear
15
+ @body = ''
8
16
  while token = tokens.shift
9
17
  if token =~ FullTokenPossiblyInvalid
10
- @nodelist << $1 if $1 != "".freeze
18
+ @body << $1 if $1 != "".freeze
11
19
  return if block_delimiter == $2
12
20
  end
13
- @nodelist << token if not token.empty?
21
+ @body << token unless token.empty?
14
22
  end
23
+
24
+ raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name))
25
+ end
26
+
27
+ def render(_context)
28
+ @body
29
+ end
30
+
31
+ def nodelist
32
+ [@body]
33
+ end
34
+
35
+ def blank?
36
+ @body.empty?
15
37
  end
16
38
  end
17
39
 
@@ -6,10 +6,10 @@ module Liquid
6
6
  super
7
7
  if markup =~ Syntax
8
8
  @variable_name = $1
9
- @collection_name = $2
9
+ @collection_name = Expression.parse($2)
10
10
  @attributes = {}
11
11
  markup.scan(TagAttributes) do |key, value|
12
- @attributes[key] = value
12
+ @attributes[key] = Expression.parse(value)
13
13
  end
14
14
  else
15
15
  raise SyntaxError.new(options[:locale].t("errors.syntax.table_row".freeze))
@@ -17,50 +17,32 @@ module Liquid
17
17
  end
18
18
 
19
19
  def render(context)
20
- collection = context[@collection_name] or return ''.freeze
20
+ collection = context.evaluate(@collection_name) or return ''.freeze
21
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
22
+ from = @attributes.key?('offset'.freeze) ? context.evaluate(@attributes['offset'.freeze]).to_i : 0
23
+ to = @attributes.key?('limit'.freeze) ? from + context.evaluate(@attributes['limit'.freeze]).to_i : nil
24
24
 
25
25
  collection = Utils.slice_collection(collection, from, to)
26
26
 
27
27
  length = collection.length
28
28
 
29
- cols = context[@attributes['cols'.freeze]].to_i
30
-
31
- row = 1
32
- col = 0
29
+ cols = context.evaluate(@attributes['cols'.freeze]).to_i
33
30
 
34
31
  result = "<tr class=\"row1\">\n"
35
32
  context.stack do
33
+ tablerowloop = Liquid::TablerowloopDrop.new(length, cols)
34
+ context['tablerowloop'.freeze] = tablerowloop
36
35
 
37
36
  collection.each_with_index do |item, index|
38
37
  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
- 'rindex'.freeze => length - index,
46
- 'rindex0'.freeze => length - index - 1,
47
- 'first'.freeze => (index == 0),
48
- 'last'.freeze => (index == length - 1),
49
- 'col_first'.freeze => (col == 0),
50
- 'col_last'.freeze => (col == cols - 1)
51
- }
52
-
53
-
54
- col += 1
55
38
 
56
- result << "<td class=\"col#{col}\">" << super << '</td>'
39
+ result << "<td class=\"col#{tablerowloop.col}\">" << super << '</td>'
57
40
 
58
- if col == cols and (index != length - 1)
59
- col = 0
60
- row += 1
61
- result << "</tr>\n<tr class=\"row#{row}\">"
41
+ if tablerowloop.col_last && !tablerowloop.last
42
+ result << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">"
62
43
  end
63
44
 
45
+ tablerowloop.send(:increment!)
64
46
  end
65
47
  end
66
48
  result << "</tr>\n"
@@ -1,4 +1,4 @@
1
- require File.dirname(__FILE__) + '/if'
1
+ require_relative 'if'
2
2
 
3
3
  module Liquid
4
4
  # Unless is a conditional just like 'if' but works on the inverse logic.
@@ -8,17 +8,16 @@ module Liquid
8
8
  class Unless < If
9
9
  def render(context)
10
10
  context.stack do
11
-
12
11
  # First condition is interpreted backwards ( if not )
13
12
  first_block = @blocks.first
14
13
  unless first_block.evaluate(context)
15
- return render_all(first_block.attachment, context)
14
+ return first_block.attachment.render(context)
16
15
  end
17
16
 
18
17
  # After the first condition unless works just like if
19
18
  @blocks[1..-1].each do |block|
20
19
  if block.evaluate(context)
21
- return render_all(block.attachment, context)
20
+ return block.attachment.render(context)
22
21
  end
23
22
  end
24
23
 
@@ -1,5 +1,4 @@
1
1
  module Liquid
2
-
3
2
  # Templates are central to liquid.
4
3
  # Interpretating templates is a two step process. First you compile the
5
4
  # source code you got. During compile time some extensive error checking is performed.
@@ -14,11 +13,9 @@ module Liquid
14
13
  # template.render('user_name' => 'bob')
15
14
  #
16
15
  class Template
17
- DEFAULT_OPTIONS = {
18
- :locale => I18n.new
19
- }
16
+ attr_accessor :root
17
+ attr_reader :resource_limits, :warnings
20
18
 
21
- attr_accessor :root, :resource_limits
22
19
  @@file_system = BlankFileSystem.new
23
20
 
24
21
  class TagRegistry
@@ -28,7 +25,7 @@ module Liquid
28
25
  end
29
26
 
30
27
  def [](tag_name)
31
- return nil unless @tags.has_key?(tag_name)
28
+ return nil unless @tags.key?(tag_name)
32
29
  return @cache[tag_name] if Liquid.cache_classes
33
30
 
34
31
  lookup_class(@tags[tag_name]).tap { |o| @cache[tag_name] = o }
@@ -110,7 +107,7 @@ module Liquid
110
107
  end
111
108
 
112
109
  def initialize
113
- @resource_limits = self.class.default_resource_limits.dup
110
+ @resource_limits = ResourceLimits.new(self.class.default_resource_limits)
114
111
  end
115
112
 
116
113
  # Parse source code.
@@ -119,16 +116,12 @@ module Liquid
119
116
  @options = options
120
117
  @profiling = options[:profile]
121
118
  @line_numbers = options[:line_numbers] || @profiling
122
- @root = Document.parse(tokenize(source), DEFAULT_OPTIONS.merge(options))
123
- @warnings = nil
119
+ parse_context = options.is_a?(ParseContext) ? options : ParseContext.new(options)
120
+ @root = Document.parse(tokenize(source), parse_context)
121
+ @warnings = parse_context.warnings
124
122
  self
125
123
  end
126
124
 
127
- def warnings
128
- return [] unless @root
129
- @warnings ||= @root.warnings
130
- end
131
-
132
125
  def registers
133
126
  @registers ||= {}
134
127
  end
@@ -167,7 +160,7 @@ module Liquid
167
160
  c = args.shift
168
161
 
169
162
  if @rethrow_errors
170
- c.exception_handler = ->(e) { true }
163
+ c.exception_handler = ->(e) { raise }
171
164
  end
172
165
 
173
166
  c
@@ -186,27 +179,25 @@ module Liquid
186
179
  when Hash
187
180
  options = args.pop
188
181
 
189
- if options[:registers].is_a?(Hash)
190
- self.registers.merge!(options[:registers])
191
- end
182
+ registers.merge!(options[:registers]) if options[:registers].is_a?(Hash)
192
183
 
193
- if options[:filters]
194
- context.add_filters(options[:filters])
195
- end
184
+ context.add_filters(options[:filters]) if options[:filters]
196
185
 
197
- if options[:exception_handler]
198
- context.exception_handler = options[:exception_handler]
199
- end
200
- when Module
201
- context.add_filters(args.pop)
202
- when Array
186
+ context.global_filter = options[:global_filter] if options[:global_filter]
187
+
188
+ context.exception_handler = options[:exception_handler] if options[:exception_handler]
189
+
190
+ when Module, Array
203
191
  context.add_filters(args.pop)
204
192
  end
205
193
 
194
+ # Retrying a render resets resource usage
195
+ context.resource_limits.reset
196
+
206
197
  begin
207
198
  # render the nodelist.
208
199
  # for performance reasons we get an array back here. join will make a string out of it.
209
- result = with_profiling do
200
+ result = with_profiling(context) do
210
201
  @root.render(context)
211
202
  end
212
203
  result.respond_to?(:join) ? result.join : result
@@ -224,32 +215,14 @@ module Liquid
224
215
 
225
216
  private
226
217
 
227
- # Uses the <tt>Liquid::TemplateParser</tt> regexp to tokenize the passed source
228
218
  def tokenize(source)
229
- source = source.source if source.respond_to?(:source)
230
- return [] if source.to_s.empty?
231
-
232
- tokens = calculate_line_numbers(source.split(TemplateParser))
233
-
234
- # removes the rogue empty element at the beginning of the array
235
- tokens.shift if tokens[0] and tokens[0].empty?
236
-
237
- tokens
219
+ Tokenizer.new(source, @line_numbers)
238
220
  end
239
221
 
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
222
+ def with_profiling(context)
223
+ if @profiling && !context.partial
224
+ raise "Profiler not loaded, require 'liquid/profiler' first" unless defined?(Liquid::Profiler)
250
225
 
251
- def with_profiling
252
- if @profiling && !@options[:included]
253
226
  @profiler = Profiler.new
254
227
  @profiler.start
255
228
 
@@ -0,0 +1,31 @@
1
+ module Liquid
2
+ class Tokenizer
3
+ attr_reader :line_number
4
+
5
+ def initialize(source, line_numbers = false)
6
+ @source = source
7
+ @line_number = 1 if line_numbers
8
+ @tokens = tokenize
9
+ end
10
+
11
+ def shift
12
+ token = @tokens.shift
13
+ @line_number += token.count("\n") if @line_number && token
14
+ token
15
+ end
16
+
17
+ private
18
+
19
+ def tokenize
20
+ @source = @source.source if @source.respond_to?(:source)
21
+ return [] if @source.to_s.empty?
22
+
23
+ tokens = @source.split(TemplateParser)
24
+
25
+ # removes the rogue empty element at the beginning of the array
26
+ tokens.shift if tokens[0] && tokens[0].empty?
27
+
28
+ tokens
29
+ end
30
+ end
31
+ end