liquid 3.0.6 → 4.0.0.rc1

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 (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