liquid 4.0.3 → 5.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +89 -0
  3. data/README.md +10 -4
  4. data/lib/liquid/block.rb +31 -14
  5. data/lib/liquid/block_body.rb +169 -57
  6. data/lib/liquid/condition.rb +48 -21
  7. data/lib/liquid/context.rb +111 -52
  8. data/lib/liquid/document.rb +47 -9
  9. data/lib/liquid/drop.rb +4 -2
  10. data/lib/liquid/errors.rb +20 -18
  11. data/lib/liquid/expression.rb +28 -32
  12. data/lib/liquid/extensions.rb +2 -0
  13. data/lib/liquid/file_system.rb +6 -4
  14. data/lib/liquid/forloop_drop.rb +54 -4
  15. data/lib/liquid/i18n.rb +5 -3
  16. data/lib/liquid/interrupts.rb +3 -1
  17. data/lib/liquid/lexer.rb +30 -23
  18. data/lib/liquid/locales/en.yml +8 -5
  19. data/lib/liquid/parse_context.rb +20 -4
  20. data/lib/liquid/parse_tree_visitor.rb +2 -2
  21. data/lib/liquid/parser.rb +30 -18
  22. data/lib/liquid/parser_switching.rb +17 -3
  23. data/lib/liquid/partial_cache.rb +24 -0
  24. data/lib/liquid/profiler/hooks.rb +26 -14
  25. data/lib/liquid/profiler.rb +67 -86
  26. data/lib/liquid/range_lookup.rb +13 -3
  27. data/lib/liquid/registers.rb +51 -0
  28. data/lib/liquid/resource_limits.rb +47 -8
  29. data/lib/liquid/standardfilters.rb +551 -114
  30. data/lib/liquid/strainer_factory.rb +41 -0
  31. data/lib/liquid/strainer_template.rb +62 -0
  32. data/lib/liquid/tablerowloop_drop.rb +64 -5
  33. data/lib/liquid/tag/disableable.rb +22 -0
  34. data/lib/liquid/tag/disabler.rb +21 -0
  35. data/lib/liquid/tag.rb +28 -6
  36. data/lib/liquid/tags/assign.rb +36 -18
  37. data/lib/liquid/tags/break.rb +16 -3
  38. data/lib/liquid/tags/capture.rb +24 -18
  39. data/lib/liquid/tags/case.rb +61 -27
  40. data/lib/liquid/tags/comment.rb +18 -3
  41. data/lib/liquid/tags/continue.rb +16 -12
  42. data/lib/liquid/tags/cycle.rb +37 -25
  43. data/lib/liquid/tags/decrement.rb +22 -20
  44. data/lib/liquid/tags/echo.rb +41 -0
  45. data/lib/liquid/tags/for.rb +90 -87
  46. data/lib/liquid/tags/if.rb +50 -32
  47. data/lib/liquid/tags/ifchanged.rb +11 -10
  48. data/lib/liquid/tags/include.rb +49 -60
  49. data/lib/liquid/tags/increment.rb +23 -17
  50. data/lib/liquid/tags/inline_comment.rb +43 -0
  51. data/lib/liquid/tags/raw.rb +25 -11
  52. data/lib/liquid/tags/render.rb +109 -0
  53. data/lib/liquid/tags/table_row.rb +45 -19
  54. data/lib/liquid/tags/unless.rb +38 -19
  55. data/lib/liquid/template.rb +52 -72
  56. data/lib/liquid/template_factory.rb +9 -0
  57. data/lib/liquid/tokenizer.rb +18 -10
  58. data/lib/liquid/usage.rb +8 -0
  59. data/lib/liquid/utils.rb +13 -3
  60. data/lib/liquid/variable.rb +49 -44
  61. data/lib/liquid/variable_lookup.rb +18 -10
  62. data/lib/liquid/version.rb +2 -1
  63. data/lib/liquid.rb +18 -6
  64. metadata +20 -108
  65. data/lib/liquid/strainer.rb +0 -66
  66. data/lib/liquid/truffle.rb +0 -5
  67. data/test/fixtures/en_locale.yml +0 -9
  68. data/test/integration/assign_test.rb +0 -48
  69. data/test/integration/blank_test.rb +0 -106
  70. data/test/integration/block_test.rb +0 -12
  71. data/test/integration/capture_test.rb +0 -50
  72. data/test/integration/context_test.rb +0 -32
  73. data/test/integration/document_test.rb +0 -19
  74. data/test/integration/drop_test.rb +0 -273
  75. data/test/integration/error_handling_test.rb +0 -260
  76. data/test/integration/filter_test.rb +0 -178
  77. data/test/integration/hash_ordering_test.rb +0 -23
  78. data/test/integration/output_test.rb +0 -123
  79. data/test/integration/parse_tree_visitor_test.rb +0 -247
  80. data/test/integration/parsing_quirks_test.rb +0 -122
  81. data/test/integration/render_profiling_test.rb +0 -154
  82. data/test/integration/security_test.rb +0 -80
  83. data/test/integration/standard_filter_test.rb +0 -776
  84. data/test/integration/tags/break_tag_test.rb +0 -15
  85. data/test/integration/tags/continue_tag_test.rb +0 -15
  86. data/test/integration/tags/for_tag_test.rb +0 -410
  87. data/test/integration/tags/if_else_tag_test.rb +0 -188
  88. data/test/integration/tags/include_tag_test.rb +0 -253
  89. data/test/integration/tags/increment_tag_test.rb +0 -23
  90. data/test/integration/tags/raw_tag_test.rb +0 -31
  91. data/test/integration/tags/standard_tag_test.rb +0 -296
  92. data/test/integration/tags/statements_test.rb +0 -111
  93. data/test/integration/tags/table_row_test.rb +0 -64
  94. data/test/integration/tags/unless_else_tag_test.rb +0 -26
  95. data/test/integration/template_test.rb +0 -332
  96. data/test/integration/trim_mode_test.rb +0 -529
  97. data/test/integration/variable_test.rb +0 -96
  98. data/test/test_helper.rb +0 -116
  99. data/test/truffle/truffle_test.rb +0 -9
  100. data/test/unit/block_unit_test.rb +0 -58
  101. data/test/unit/condition_unit_test.rb +0 -166
  102. data/test/unit/context_unit_test.rb +0 -489
  103. data/test/unit/file_system_unit_test.rb +0 -35
  104. data/test/unit/i18n_unit_test.rb +0 -37
  105. data/test/unit/lexer_unit_test.rb +0 -51
  106. data/test/unit/parser_unit_test.rb +0 -82
  107. data/test/unit/regexp_unit_test.rb +0 -44
  108. data/test/unit/strainer_unit_test.rb +0 -164
  109. data/test/unit/tag_unit_test.rb +0 -21
  110. data/test/unit/tags/case_tag_unit_test.rb +0 -10
  111. data/test/unit/tags/for_tag_unit_test.rb +0 -13
  112. data/test/unit/tags/if_tag_unit_test.rb +0 -8
  113. data/test/unit/template_unit_test.rb +0 -78
  114. data/test/unit/tokenizer_unit_test.rb +0 -55
  115. data/test/unit/variable_unit_test.rb +0 -162
@@ -1,18 +1,22 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
- # Continue tag to be used to break out of a for loop.
3
- #
4
- # == Basic Usage:
5
- # {% for item in collection %}
6
- # {% if item.condition %}
7
- # {% continue %}
8
- # {% endif %}
9
- # {% endfor %}
10
- #
4
+ # @liquid_public_docs
5
+ # @liquid_type tag
6
+ # @liquid_category iteration
7
+ # @liquid_name continue
8
+ # @liquid_summary
9
+ # Causes a [`for` loop](/api/liquid/tags#for) to skip to the next iteration.
10
+ # @liquid_syntax
11
+ # {% continue %}
11
12
  class Continue < Tag
12
- def interrupt
13
- ContinueInterrupt.new
13
+ INTERRUPT = ContinueInterrupt.new.freeze
14
+
15
+ def render_to_output_buffer(context, output)
16
+ context.push_interrupt(INTERRUPT)
17
+ output
14
18
  end
15
19
  end
16
20
 
17
- Template.register_tag('continue'.freeze, Continue)
21
+ Template.register_tag('continue', Continue)
18
22
  end
@@ -1,16 +1,19 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
- # Cycle is usually used within a loop to alternate between values, like colors or DOM classes.
3
- #
4
- # {% for item in items %}
5
- # <div class="{% cycle 'red', 'green', 'blue' %}"> {{ item }} </div>
6
- # {% end %}
7
- #
8
- # <div class="red"> Item one </div>
9
- # <div class="green"> Item two </div>
10
- # <div class="blue"> Item three </div>
11
- # <div class="red"> Item four </div>
12
- # <div class="green"> Item five</div>
4
+ # @liquid_public_docs
5
+ # @liquid_type tag
6
+ # @liquid_category iteration
7
+ # @liquid_name cycle
8
+ # @liquid_summary
9
+ # Loops through a group of strings and outputs them one at a time for each iteration of a [`for` loop](/api/liquid/tags#for).
10
+ # @liquid_description
11
+ # The `cycle` tag must be used inside a `for` loop.
13
12
  #
13
+ # > Tip:
14
+ # > Use the `cycle` tag to output text in a predictable pattern. For example, to apply odd/even classes to rows in a table.
15
+ # @liquid_syntax
16
+ # {% cycle string, string, ... %}
14
17
  class Cycle < Tag
15
18
  SimpleSyntax = /\A#{QuotedFragment}+/o
16
19
  NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om
@@ -21,28 +24,37 @@ module Liquid
21
24
  super
22
25
  case markup
23
26
  when NamedSyntax
24
- @variables = variables_from_string($2)
25
- @name = Expression.parse($1)
27
+ @variables = variables_from_string(Regexp.last_match(2))
28
+ @name = parse_expression(Regexp.last_match(1))
26
29
  when SimpleSyntax
27
30
  @variables = variables_from_string(markup)
28
- @name = @variables.to_s
31
+ @name = @variables.to_s
29
32
  else
30
- raise SyntaxError.new(options[:locale].t("errors.syntax.cycle".freeze))
33
+ raise SyntaxError, options[:locale].t("errors.syntax.cycle")
31
34
  end
32
35
  end
33
36
 
34
- def render(context)
37
+ def render_to_output_buffer(context, output)
35
38
  context.registers[:cycle] ||= {}
36
39
 
37
- context.stack do
38
- key = context.evaluate(@name)
39
- iteration = context.registers[:cycle][key].to_i
40
- result = context.evaluate(@variables[iteration])
41
- iteration += 1
42
- iteration = 0 if iteration >= @variables.size
43
- context.registers[:cycle][key] = iteration
44
- result
40
+ key = context.evaluate(@name)
41
+ iteration = context.registers[:cycle][key].to_i
42
+
43
+ val = context.evaluate(@variables[iteration])
44
+
45
+ if val.is_a?(Array)
46
+ val = val.join
47
+ elsif !val.is_a?(String)
48
+ val = val.to_s
45
49
  end
50
+
51
+ output << val
52
+
53
+ iteration += 1
54
+ iteration = 0 if iteration >= @variables.size
55
+
56
+ context.registers[:cycle][key] = iteration
57
+ output
46
58
  end
47
59
 
48
60
  private
@@ -50,7 +62,7 @@ module Liquid
50
62
  def variables_from_string(markup)
51
63
  markup.split(',').collect do |var|
52
64
  var =~ /\s*(#{QuotedFragment})\s*/o
53
- $1 ? Expression.parse($1) : nil
65
+ Regexp.last_match(1) ? parse_expression(Regexp.last_match(1)) : nil
54
66
  end.compact
55
67
  end
56
68
 
@@ -1,35 +1,37 @@
1
- module Liquid
2
- # decrement is used in a place where one needs to insert a counter
3
- # into a template, and needs the counter to survive across
4
- # multiple instantiations of the template.
5
- # NOTE: decrement is a pre-decrement, --i,
6
- # while increment is post: i++.
7
- #
8
- # (To achieve the survival, the application must keep the context)
9
- #
10
- # if the variable does not exist, it is created with value 0.
1
+ # frozen_string_literal: true
11
2
 
12
- # Hello: {% decrement variable %}
13
- #
14
- # gives you:
15
- #
16
- # Hello: -1
17
- # Hello: -2
18
- # Hello: -3
3
+ module Liquid
4
+ # @liquid_public_docs
5
+ # @liquid_type tag
6
+ # @liquid_category variable
7
+ # @liquid_name decrement
8
+ # @liquid_summary
9
+ # Creates a new variable, with a default value of -1, that's decreased by 1 with each subsequent call.
10
+ # @liquid_description
11
+ # Variables that are declared with `decrement` are unique to the [layout](/themes/architecture/layouts), [template](/themes/architecture/templates),
12
+ # or [section](/themes/architecture/sections) file that they're created in. However, the variable is shared across
13
+ # [snippets](/themes/architecture#snippets) included in the file.
19
14
  #
15
+ # Similarly, variables that are created with `decrement` are independent from those created with [`assign`](/api/liquid/tags#assign)
16
+ # and [`capture`](/api/liquid/tags#capture). However, `decrement` and [`increment`](/api/liquid/tags#increment) share
17
+ # variables.
18
+ # @liquid_syntax
19
+ # {% decrement variable_name %}
20
+ # @liquid_syntax_keyword variable_name The name of the variable being decremented.
20
21
  class Decrement < Tag
21
22
  def initialize(tag_name, markup, options)
22
23
  super
23
24
  @variable = markup.strip
24
25
  end
25
26
 
26
- def render(context)
27
+ def render_to_output_buffer(context, output)
27
28
  value = context.environments.first[@variable] ||= 0
28
29
  value -= 1
29
30
  context.environments.first[@variable] = value
30
- value.to_s
31
+ output << value.to_s
32
+ output
31
33
  end
32
34
  end
33
35
 
34
- Template.register_tag('decrement'.freeze, Decrement)
36
+ Template.register_tag('decrement', Decrement)
35
37
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liquid
4
+ # @liquid_public_docs
5
+ # @liquid_type tag
6
+ # @liquid_category syntax
7
+ # @liquid_name echo
8
+ # @liquid_summary
9
+ # Outputs an expression.
10
+ # @liquid_description
11
+ # Using the `echo` tag is the same as wrapping an expression in curly brackets (`{{` and `}}`). However, unlike the curly
12
+ # bracket method, you can use the `echo` tag inside [`liquid` tags](/api/liquid/tags#liquid).
13
+ #
14
+ # > Tip:
15
+ # > You can use [filters](/api/liquid/filters) on expressions inside `echo` tags.
16
+ # @liquid_syntax
17
+ # {% liquid
18
+ # echo expression
19
+ # %}
20
+ # @liquid_syntax_keyword expression The expression to be output.
21
+ class Echo < Tag
22
+ attr_reader :variable
23
+
24
+ def initialize(tag_name, markup, parse_context)
25
+ super
26
+ @variable = Variable.new(markup, parse_context)
27
+ end
28
+
29
+ def render(context)
30
+ @variable.render_to_output_buffer(context, +'')
31
+ end
32
+
33
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
34
+ def children
35
+ [@node.variable]
36
+ end
37
+ end
38
+ end
39
+
40
+ Template.register_tag('echo', Echo)
41
+ end
@@ -1,48 +1,29 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
- # "For" iterates over an array or collection.
3
- # Several useful variables are available to you within the loop.
4
- #
5
- # == Basic usage:
6
- # {% for item in collection %}
7
- # {{ forloop.index }}: {{ item.name }}
8
- # {% endfor %}
9
- #
10
- # == Advanced usage:
11
- # {% for item in collection %}
12
- # <div {% if forloop.first %}class="first"{% endif %}>
13
- # Item {{ forloop.index }}: {{ item.name }}
14
- # </div>
15
- # {% else %}
16
- # There is nothing in the collection.
17
- # {% endfor %}
18
- #
19
- # You can also define a limit and offset much like SQL. Remember
20
- # that offset starts at 0 for the first item.
21
- #
22
- # {% for item in collection limit:5 offset:10 %}
23
- # {{ item.name }}
24
- # {% end %}
25
- #
26
- # To reverse the for loop simply use {% for item in collection reversed %} (note that the flag's spelling is different to the filter `reverse`)
27
- #
28
- # == Available variables:
29
- #
30
- # forloop.name:: 'item-collection'
31
- # forloop.length:: Length of the loop
32
- # forloop.index:: The current item's position in the collection;
33
- # forloop.index starts at 1.
34
- # This is helpful for non-programmers who start believe
35
- # the first item in an array is 1, not 0.
36
- # forloop.index0:: The current item's position in the collection
37
- # where the first item is 0
38
- # forloop.rindex:: Number of items remaining in the loop
39
- # (length - index) where 1 is the last item.
40
- # forloop.rindex0:: Number of items remaining in the loop
41
- # where 0 is the last item.
42
- # forloop.first:: Returns true if the item is the first item.
43
- # forloop.last:: Returns true if the item is the last item.
44
- # forloop.parentloop:: Provides access to the parent loop, if present.
4
+ # @liquid_public_docs
5
+ # @liquid_type tag
6
+ # @liquid_category iteration
7
+ # @liquid_name for
8
+ # @liquid_summary
9
+ # Renders an expression for every item in an array.
10
+ # @liquid_description
11
+ # You can do a maximum of 50 iterations with a `for` loop. If you need to iterate over more than 50 items, then use the
12
+ # [`paginate` tag](/api/liquid/tags#paginate) to split the items over multiple pages.
45
13
  #
14
+ # > Tip:
15
+ # > Every `for` loop has an associated [`forloop` object](/api/liquid/objects#forloop) with information about the loop.
16
+ # @liquid_syntax
17
+ # {% for variable in array %}
18
+ # expression
19
+ # {% endfor %}
20
+ # @liquid_syntax_keyword variable The current item in the array.
21
+ # @liquid_syntax_keyword array The array to iterate over.
22
+ # @liquid_syntax_keyword expression The expression to render for each iteration.
23
+ # @liquid_optional_param limit [number] The number of iterations to perform.
24
+ # @liquid_optional_param offset [number] The 1-based index to start iterating at.
25
+ # @liquid_optional_param range [untyped] A custom numeric range to iterate over.
26
+ # @liquid_optional_param reversed [untyped] Iterate in reverse order.
46
27
  class For < Block
47
28
  Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
48
29
 
@@ -52,13 +33,20 @@ module Liquid
52
33
  super
53
34
  @from = @limit = nil
54
35
  parse_with_selected_parser(markup)
55
- @for_block = BlockBody.new
36
+ @for_block = new_body
56
37
  @else_block = nil
57
38
  end
58
39
 
59
40
  def parse(tokens)
60
- return unless parse_body(@for_block, tokens)
61
- parse_body(@else_block, tokens)
41
+ if parse_body(@for_block, tokens)
42
+ parse_body(@else_block, tokens)
43
+ end
44
+ if blank?
45
+ @else_block&.remove_blank_strings
46
+ @for_block.remove_blank_strings
47
+ end
48
+ @else_block&.freeze
49
+ @for_block.freeze
62
50
  end
63
51
 
64
52
  def nodelist
@@ -66,49 +54,53 @@ module Liquid
66
54
  end
67
55
 
68
56
  def unknown_tag(tag, markup, tokens)
69
- return super unless tag == 'else'.freeze
70
- @else_block = BlockBody.new
57
+ return super unless tag == 'else'
58
+ @else_block = new_body
71
59
  end
72
60
 
73
- def render(context)
61
+ def render_to_output_buffer(context, output)
74
62
  segment = collection_segment(context)
75
63
 
76
64
  if segment.empty?
77
- render_else(context)
65
+ render_else(context, output)
78
66
  else
79
- render_segment(context, segment)
67
+ render_segment(context, output, segment)
80
68
  end
69
+
70
+ output
81
71
  end
82
72
 
83
73
  protected
84
74
 
85
75
  def lax_parse(markup)
86
76
  if markup =~ Syntax
87
- @variable_name = $1
88
- collection_name = $2
89
- @reversed = !!$3
90
- @name = "#{@variable_name}-#{collection_name}"
91
- @collection_name = Expression.parse(collection_name)
77
+ @variable_name = Regexp.last_match(1)
78
+ collection_name = Regexp.last_match(2)
79
+ @reversed = !!Regexp.last_match(3)
80
+ @name = "#{@variable_name}-#{collection_name}"
81
+ @collection_name = parse_expression(collection_name)
92
82
  markup.scan(TagAttributes) do |key, value|
93
83
  set_attribute(key, value)
94
84
  end
95
85
  else
96
- raise SyntaxError.new(options[:locale].t("errors.syntax.for".freeze))
86
+ raise SyntaxError, options[:locale].t("errors.syntax.for")
97
87
  end
98
88
  end
99
89
 
100
90
  def strict_parse(markup)
101
91
  p = Parser.new(markup)
102
92
  @variable_name = p.consume(:id)
103
- raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_in".freeze)) unless p.id?('in'.freeze)
104
- collection_name = p.expression
105
- @name = "#{@variable_name}-#{collection_name}"
106
- @collection_name = Expression.parse(collection_name)
107
- @reversed = p.id?('reversed'.freeze)
93
+ raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in") unless p.id?('in')
94
+
95
+ collection_name = p.expression
96
+ @collection_name = parse_expression(collection_name)
97
+
98
+ @name = "#{@variable_name}-#{collection_name}"
99
+ @reversed = p.id?('reversed')
108
100
 
109
101
  while p.look(:id) && p.look(:colon, 1)
110
- unless attribute = p.id?('limit'.freeze) || p.id?('offset'.freeze)
111
- raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_attribute".freeze))
102
+ unless (attribute = p.id?('limit') || p.id?('offset'))
103
+ raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_attribute")
112
104
  end
113
105
  p.consume
114
106
  set_attribute(attribute, p.expression)
@@ -124,14 +116,23 @@ module Liquid
124
116
  from = if @from == :continue
125
117
  offsets[@name].to_i
126
118
  else
127
- context.evaluate(@from).to_i
119
+ from_value = context.evaluate(@from)
120
+ if from_value.nil?
121
+ 0
122
+ else
123
+ Utils.to_integer(from_value)
124
+ end
128
125
  end
129
126
 
130
127
  collection = context.evaluate(@collection_name)
131
128
  collection = collection.to_a if collection.is_a?(Range)
132
129
 
133
- limit = context.evaluate(@limit)
134
- to = limit ? limit.to_i + from : nil
130
+ limit_value = context.evaluate(@limit)
131
+ to = if limit_value.nil?
132
+ nil
133
+ else
134
+ Utils.to_integer(limit_value) + from
135
+ end
135
136
 
136
137
  segment = Utils.slice_collection(collection, from, to)
137
138
  segment.reverse! if @reversed
@@ -141,11 +142,9 @@ module Liquid
141
142
  segment
142
143
  end
143
144
 
144
- def render_segment(context, segment)
145
+ def render_segment(context, output, segment)
145
146
  for_stack = context.registers[:for_stack] ||= []
146
- length = segment.length
147
-
148
- result = ''
147
+ length = segment.length
149
148
 
150
149
  context.stack do
151
150
  loop_vars = Liquid::ForloopDrop.new(@name, length, for_stack[-1])
@@ -153,43 +152,47 @@ module Liquid
153
152
  for_stack.push(loop_vars)
154
153
 
155
154
  begin
156
- context['forloop'.freeze] = loop_vars
155
+ context['forloop'] = loop_vars
157
156
 
158
157
  segment.each do |item|
159
158
  context[@variable_name] = item
160
- result << @for_block.render(context)
159
+ @for_block.render_to_output_buffer(context, output)
161
160
  loop_vars.send(:increment!)
162
161
 
163
162
  # Handle any interrupts if they exist.
164
- if context.interrupt?
165
- interrupt = context.pop_interrupt
166
- break if interrupt.is_a? BreakInterrupt
167
- next if interrupt.is_a? ContinueInterrupt
168
- end
163
+ next unless context.interrupt?
164
+ interrupt = context.pop_interrupt
165
+ break if interrupt.is_a?(BreakInterrupt)
166
+ next if interrupt.is_a?(ContinueInterrupt)
169
167
  end
170
168
  ensure
171
169
  for_stack.pop
172
170
  end
173
171
  end
174
172
 
175
- result
173
+ output
176
174
  end
177
175
 
178
176
  def set_attribute(key, expr)
179
177
  case key
180
- when 'offset'.freeze
181
- @from = if expr == 'continue'.freeze
178
+ when 'offset'
179
+ @from = if expr == 'continue'
180
+ Usage.increment('for_offset_continue')
182
181
  :continue
183
182
  else
184
- Expression.parse(expr)
183
+ parse_expression(expr)
185
184
  end
186
- when 'limit'.freeze
187
- @limit = Expression.parse(expr)
185
+ when 'limit'
186
+ @limit = parse_expression(expr)
188
187
  end
189
188
  end
190
189
 
191
- def render_else(context)
192
- @else_block ? @else_block.render(context) : ''.freeze
190
+ def render_else(context, output)
191
+ if @else_block
192
+ @else_block.render_to_output_buffer(context, output)
193
+ else
194
+ output
195
+ end
193
196
  end
194
197
 
195
198
  class ParseTreeVisitor < Liquid::ParseTreeVisitor
@@ -199,5 +202,5 @@ module Liquid
199
202
  end
200
203
  end
201
204
 
202
- Template.register_tag('for'.freeze, For)
205
+ Template.register_tag('for', For)
203
206
  end
@@ -1,25 +1,29 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
- # If is the conditional block
3
- #
4
- # {% if user.admin %}
5
- # Admin user!
6
- # {% else %}
7
- # Not admin user
4
+ # @liquid_public_docs
5
+ # @liquid_type tag
6
+ # @liquid_category conditional
7
+ # @liquid_name if
8
+ # @liquid_summary
9
+ # Renders an expression if a specific condition is `true`.
10
+ # @liquid_syntax
11
+ # {% if condition %}
12
+ # expression
8
13
  # {% endif %}
9
- #
10
- # There are {% if count < 5 %} less {% else %} more {% endif %} items than you need.
11
- #
14
+ # @liquid_syntax_keyword condition The condition to evaluate.
15
+ # @liquid_syntax_keyword expression The expression to render if the condition is met.
12
16
  class If < Block
13
- Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o
17
+ Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o
14
18
  ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o
15
- BOOLEAN_OPERATORS = %w(and or).freeze
19
+ BOOLEAN_OPERATORS = %w(and or).freeze
16
20
 
17
21
  attr_reader :blocks
18
22
 
19
23
  def initialize(tag_name, markup, options)
20
24
  super
21
25
  @blocks = []
22
- push_block('if'.freeze, markup)
26
+ push_block('if', markup)
23
27
  end
24
28
 
25
29
  def nodelist
@@ -29,53 +33,67 @@ module Liquid
29
33
  def parse(tokens)
30
34
  while parse_body(@blocks.last.attachment, tokens)
31
35
  end
36
+ @blocks.reverse_each do |block|
37
+ block.attachment.remove_blank_strings if blank?
38
+ block.attachment.freeze
39
+ end
32
40
  end
33
41
 
42
+ ELSE_TAG_NAMES = ['elsif', 'else'].freeze
43
+ private_constant :ELSE_TAG_NAMES
44
+
34
45
  def unknown_tag(tag, markup, tokens)
35
- if ['elsif'.freeze, 'else'.freeze].include?(tag)
46
+ if ELSE_TAG_NAMES.include?(tag)
36
47
  push_block(tag, markup)
37
48
  else
38
49
  super
39
50
  end
40
51
  end
41
52
 
42
- def render(context)
43
- context.stack do
44
- @blocks.each do |block|
45
- if block.evaluate(context)
46
- return block.attachment.render(context)
47
- end
53
+ def render_to_output_buffer(context, output)
54
+ @blocks.each do |block|
55
+ result = Liquid::Utils.to_liquid_value(
56
+ block.evaluate(context)
57
+ )
58
+
59
+ if result
60
+ return block.attachment.render_to_output_buffer(context, output)
48
61
  end
49
- ''.freeze
50
62
  end
63
+
64
+ output
51
65
  end
52
66
 
53
67
  private
54
68
 
55
69
  def push_block(tag, markup)
56
- block = if tag == 'else'.freeze
70
+ block = if tag == 'else'
57
71
  ElseCondition.new
58
72
  else
59
73
  parse_with_selected_parser(markup)
60
74
  end
61
75
 
62
76
  @blocks.push(block)
63
- block.attach(BlockBody.new)
77
+ block.attach(new_body)
78
+ end
79
+
80
+ def parse_expression(markup)
81
+ Condition.parse_expression(parse_context, markup)
64
82
  end
65
83
 
66
84
  def lax_parse(markup)
67
85
  expressions = markup.scan(ExpressionsAndOperators)
68
- raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop =~ Syntax
86
+ raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop =~ Syntax
69
87
 
70
- condition = Condition.new(Expression.parse($1), $2, Expression.parse($3))
88
+ condition = Condition.new(parse_expression(Regexp.last_match(1)), Regexp.last_match(2), parse_expression(Regexp.last_match(3)))
71
89
 
72
90
  until expressions.empty?
73
91
  operator = expressions.pop.to_s.strip
74
92
 
75
- raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop.to_s =~ Syntax
93
+ raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop.to_s =~ Syntax
76
94
 
77
- new_condition = Condition.new(Expression.parse($1), $2, Expression.parse($3))
78
- raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless BOOLEAN_OPERATORS.include?(operator)
95
+ new_condition = Condition.new(parse_expression(Regexp.last_match(1)), Regexp.last_match(2), parse_expression(Regexp.last_match(3)))
96
+ raise SyntaxError, options[:locale].t("errors.syntax.if") unless BOOLEAN_OPERATORS.include?(operator)
79
97
  new_condition.send(operator, condition)
80
98
  condition = new_condition
81
99
  end
@@ -93,7 +111,7 @@ module Liquid
93
111
  def parse_binary_comparisons(p)
94
112
  condition = parse_comparison(p)
95
113
  first_condition = condition
96
- while op = (p.id?('and'.freeze) || p.id?('or'.freeze))
114
+ while (op = (p.id?('and') || p.id?('or')))
97
115
  child_condition = parse_comparison(p)
98
116
  condition.send(op, child_condition)
99
117
  condition = child_condition
@@ -102,9 +120,9 @@ module Liquid
102
120
  end
103
121
 
104
122
  def parse_comparison(p)
105
- a = Expression.parse(p.expression)
106
- if op = p.consume?(:comparison)
107
- b = Expression.parse(p.expression)
123
+ a = parse_expression(p.expression)
124
+ if (op = p.consume?(:comparison))
125
+ b = parse_expression(p.expression)
108
126
  Condition.new(a, op, b)
109
127
  else
110
128
  Condition.new(a)
@@ -118,5 +136,5 @@ module Liquid
118
136
  end
119
137
  end
120
138
 
121
- Template.register_tag('if'.freeze, If)
139
+ Template.register_tag('if', If)
122
140
  end