liquid 4.0.3 → 5.4.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 (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