liquid 4.0.0 → 5.10.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 (117) hide show
  1. checksums.yaml +5 -5
  2. data/History.md +235 -2
  3. data/README.md +58 -8
  4. data/lib/liquid/block.rb +51 -20
  5. data/lib/liquid/block_body.rb +216 -82
  6. data/lib/liquid/condition.rb +83 -32
  7. data/lib/liquid/const.rb +8 -0
  8. data/lib/liquid/context.rb +130 -59
  9. data/lib/liquid/deprecations.rb +22 -0
  10. data/lib/liquid/document.rb +47 -9
  11. data/lib/liquid/drop.rb +8 -2
  12. data/lib/liquid/environment.rb +159 -0
  13. data/lib/liquid/errors.rb +23 -20
  14. data/lib/liquid/expression.rb +114 -31
  15. data/lib/liquid/extensions.rb +8 -0
  16. data/lib/liquid/file_system.rb +6 -4
  17. data/lib/liquid/forloop_drop.rb +51 -4
  18. data/lib/liquid/i18n.rb +5 -3
  19. data/lib/liquid/interrupts.rb +3 -1
  20. data/lib/liquid/lexer.rb +165 -39
  21. data/lib/liquid/locales/en.yml +16 -6
  22. data/lib/liquid/parse_context.rb +62 -7
  23. data/lib/liquid/parse_tree_visitor.rb +42 -0
  24. data/lib/liquid/parser.rb +31 -19
  25. data/lib/liquid/parser_switching.rb +42 -3
  26. data/lib/liquid/partial_cache.rb +33 -0
  27. data/lib/liquid/profiler/hooks.rb +26 -14
  28. data/lib/liquid/profiler.rb +67 -86
  29. data/lib/liquid/range_lookup.rb +26 -6
  30. data/lib/liquid/registers.rb +51 -0
  31. data/lib/liquid/resource_limits.rb +47 -8
  32. data/lib/liquid/snippet_drop.rb +22 -0
  33. data/lib/liquid/standardfilters.rb +813 -137
  34. data/lib/liquid/strainer_template.rb +62 -0
  35. data/lib/liquid/tablerowloop_drop.rb +64 -5
  36. data/lib/liquid/tag/disableable.rb +22 -0
  37. data/lib/liquid/tag/disabler.rb +13 -0
  38. data/lib/liquid/tag.rb +42 -6
  39. data/lib/liquid/tags/assign.rb +46 -18
  40. data/lib/liquid/tags/break.rb +15 -4
  41. data/lib/liquid/tags/capture.rb +26 -18
  42. data/lib/liquid/tags/case.rb +108 -32
  43. data/lib/liquid/tags/comment.rb +76 -4
  44. data/lib/liquid/tags/continue.rb +15 -13
  45. data/lib/liquid/tags/cycle.rb +117 -34
  46. data/lib/liquid/tags/decrement.rb +30 -23
  47. data/lib/liquid/tags/doc.rb +81 -0
  48. data/lib/liquid/tags/echo.rb +39 -0
  49. data/lib/liquid/tags/for.rb +109 -96
  50. data/lib/liquid/tags/if.rb +72 -41
  51. data/lib/liquid/tags/ifchanged.rb +10 -11
  52. data/lib/liquid/tags/include.rb +89 -63
  53. data/lib/liquid/tags/increment.rb +31 -20
  54. data/lib/liquid/tags/inline_comment.rb +28 -0
  55. data/lib/liquid/tags/raw.rb +25 -13
  56. data/lib/liquid/tags/render.rb +151 -0
  57. data/lib/liquid/tags/snippet.rb +45 -0
  58. data/lib/liquid/tags/table_row.rb +104 -21
  59. data/lib/liquid/tags/unless.rb +37 -20
  60. data/lib/liquid/tags.rb +51 -0
  61. data/lib/liquid/template.rb +90 -106
  62. data/lib/liquid/template_factory.rb +9 -0
  63. data/lib/liquid/tokenizer.rb +143 -13
  64. data/lib/liquid/usage.rb +8 -0
  65. data/lib/liquid/utils.rb +114 -5
  66. data/lib/liquid/variable.rb +119 -45
  67. data/lib/liquid/variable_lookup.rb +35 -13
  68. data/lib/liquid/version.rb +3 -1
  69. data/lib/liquid.rb +31 -18
  70. metadata +56 -107
  71. data/lib/liquid/strainer.rb +0 -66
  72. data/test/fixtures/en_locale.yml +0 -9
  73. data/test/integration/assign_test.rb +0 -48
  74. data/test/integration/blank_test.rb +0 -106
  75. data/test/integration/capture_test.rb +0 -50
  76. data/test/integration/context_test.rb +0 -32
  77. data/test/integration/document_test.rb +0 -19
  78. data/test/integration/drop_test.rb +0 -273
  79. data/test/integration/error_handling_test.rb +0 -260
  80. data/test/integration/filter_test.rb +0 -178
  81. data/test/integration/hash_ordering_test.rb +0 -23
  82. data/test/integration/output_test.rb +0 -123
  83. data/test/integration/parsing_quirks_test.rb +0 -118
  84. data/test/integration/render_profiling_test.rb +0 -154
  85. data/test/integration/security_test.rb +0 -66
  86. data/test/integration/standard_filter_test.rb +0 -535
  87. data/test/integration/tags/break_tag_test.rb +0 -15
  88. data/test/integration/tags/continue_tag_test.rb +0 -15
  89. data/test/integration/tags/for_tag_test.rb +0 -410
  90. data/test/integration/tags/if_else_tag_test.rb +0 -188
  91. data/test/integration/tags/include_tag_test.rb +0 -238
  92. data/test/integration/tags/increment_tag_test.rb +0 -23
  93. data/test/integration/tags/raw_tag_test.rb +0 -31
  94. data/test/integration/tags/standard_tag_test.rb +0 -296
  95. data/test/integration/tags/statements_test.rb +0 -111
  96. data/test/integration/tags/table_row_test.rb +0 -64
  97. data/test/integration/tags/unless_else_tag_test.rb +0 -26
  98. data/test/integration/template_test.rb +0 -323
  99. data/test/integration/trim_mode_test.rb +0 -525
  100. data/test/integration/variable_test.rb +0 -92
  101. data/test/test_helper.rb +0 -117
  102. data/test/unit/block_unit_test.rb +0 -58
  103. data/test/unit/condition_unit_test.rb +0 -158
  104. data/test/unit/context_unit_test.rb +0 -483
  105. data/test/unit/file_system_unit_test.rb +0 -35
  106. data/test/unit/i18n_unit_test.rb +0 -37
  107. data/test/unit/lexer_unit_test.rb +0 -51
  108. data/test/unit/parser_unit_test.rb +0 -82
  109. data/test/unit/regexp_unit_test.rb +0 -44
  110. data/test/unit/strainer_unit_test.rb +0 -148
  111. data/test/unit/tag_unit_test.rb +0 -21
  112. data/test/unit/tags/case_tag_unit_test.rb +0 -10
  113. data/test/unit/tags/for_tag_unit_test.rb +0 -13
  114. data/test/unit/tags/if_tag_unit_test.rb +0 -8
  115. data/test/unit/template_unit_test.rb +0 -78
  116. data/test/unit/tokenizer_unit_test.rb +0 -55
  117. data/test/unit/variable_unit_test.rb +0 -162
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ module Liquid
6
+ # StrainerTemplate is the computed class for the filters system.
7
+ # New filters are mixed into the strainer class which is then instantiated for each liquid template render run.
8
+ #
9
+ # The Strainer only allows method calls defined in filters given to it via StrainerFactory.add_global_filter,
10
+ # Context#add_filters or Template.register_filter
11
+ class StrainerTemplate
12
+ def initialize(context)
13
+ @context = context
14
+ end
15
+
16
+ class << self
17
+ def add_filter(filter)
18
+ return if include?(filter)
19
+
20
+ invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) }
21
+ if invokable_non_public_methods.any?
22
+ raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}"
23
+ end
24
+
25
+ include(filter)
26
+
27
+ filter_methods.merge(filter.public_instance_methods.map(&:to_s))
28
+ end
29
+
30
+ def invokable?(method)
31
+ filter_methods.include?(method.to_s)
32
+ end
33
+
34
+ def inherited(subclass)
35
+ super
36
+ subclass.instance_variable_set(:@filter_methods, @filter_methods.dup)
37
+ end
38
+
39
+ def filter_method_names
40
+ filter_methods.map(&:to_s).to_a
41
+ end
42
+
43
+ private
44
+
45
+ def filter_methods
46
+ @filter_methods ||= Set.new
47
+ end
48
+ end
49
+
50
+ def invoke(method, *args)
51
+ if self.class.invokable?(method)
52
+ send(method, *args)
53
+ elsif @context.strict_filters
54
+ raise Liquid::UndefinedFilter, "undefined filter #{method}"
55
+ else
56
+ args.first
57
+ end
58
+ rescue ::ArgumentError => e
59
+ raise Liquid::ArgumentError, e.message, e.backtrace
60
+ end
61
+ end
62
+ end
@@ -1,47 +1,106 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
4
+ # @liquid_public_docs
5
+ # @liquid_type object
6
+ # @liquid_name tablerowloop
7
+ # @liquid_summary
8
+ # Information about a parent [`tablerow` loop](/docs/api/liquid/tags/tablerow).
2
9
  class TablerowloopDrop < Drop
3
10
  def initialize(length, cols)
4
11
  @length = length
5
- @row = 1
6
- @col = 1
7
- @cols = cols
8
- @index = 0
12
+ @row = 1
13
+ @col = 1
14
+ @cols = cols
15
+ @index = 0
9
16
  end
10
17
 
11
- attr_reader :length, :col, :row
18
+ # @liquid_public_docs
19
+ # @liquid_summary
20
+ # The total number of iterations in the loop.
21
+ # @liquid_return [number]
22
+ attr_reader :length
23
+
24
+ # @liquid_public_docs
25
+ # @liquid_summary
26
+ # The 1-based index of the current column.
27
+ # @liquid_return [number]
28
+ attr_reader :col
29
+
30
+ # @liquid_public_docs
31
+ # @liquid_summary
32
+ # The 1-based index of current row.
33
+ # @liquid_return [number]
34
+ attr_reader :row
12
35
 
36
+ # @liquid_public_docs
37
+ # @liquid_summary
38
+ # The 1-based index of the current iteration.
39
+ # @liquid_return [number]
13
40
  def index
14
41
  @index + 1
15
42
  end
16
43
 
44
+ # @liquid_public_docs
45
+ # @liquid_summary
46
+ # The 0-based index of the current iteration.
47
+ # @liquid_return [number]
17
48
  def index0
18
49
  @index
19
50
  end
20
51
 
52
+ # @liquid_public_docs
53
+ # @liquid_summary
54
+ # The 0-based index of the current column.
55
+ # @liquid_return [number]
21
56
  def col0
22
57
  @col - 1
23
58
  end
24
59
 
60
+ # @liquid_public_docs
61
+ # @liquid_summary
62
+ # The 1-based index of the current iteration, in reverse order.
63
+ # @liquid_return [number]
25
64
  def rindex
26
65
  @length - @index
27
66
  end
28
67
 
68
+ # @liquid_public_docs
69
+ # @liquid_summary
70
+ # The 0-based index of the current iteration, in reverse order.
71
+ # @liquid_return [number]
29
72
  def rindex0
30
73
  @length - @index - 1
31
74
  end
32
75
 
76
+ # @liquid_public_docs
77
+ # @liquid_summary
78
+ # Returns `true` if the current iteration is the first. Returns `false` if not.
79
+ # @liquid_return [boolean]
33
80
  def first
34
81
  @index == 0
35
82
  end
36
83
 
84
+ # @liquid_public_docs
85
+ # @liquid_summary
86
+ # Returns `true` if the current iteration is the last. Returns `false` if not.
87
+ # @liquid_return [boolean]
37
88
  def last
38
89
  @index == @length - 1
39
90
  end
40
91
 
92
+ # @liquid_public_docs
93
+ # @liquid_summary
94
+ # Returns `true` if the current column is the first in the row. Returns `false` if not.
95
+ # @liquid_return [boolean]
41
96
  def col_first
42
97
  @col == 1
43
98
  end
44
99
 
100
+ # @liquid_public_docs
101
+ # @liquid_summary
102
+ # Returns `true` if the current column is the last in the row. Returns `false` if not.
103
+ # @liquid_return [boolean]
45
104
  def col_last
46
105
  @col == @cols
47
106
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liquid
4
+ class Tag
5
+ module Disableable
6
+ def render_to_output_buffer(context, output)
7
+ if context.tag_disabled?(tag_name)
8
+ output << disabled_error(context)
9
+ return
10
+ end
11
+ super
12
+ end
13
+
14
+ def disabled_error(context)
15
+ # raise then rescue the exception so that the Context#exception_renderer can re-raise it
16
+ raise DisabledError, "#{tag_name} #{parse_context[:locale].t('errors.disabled.tag')}"
17
+ rescue DisabledError => exc
18
+ context.handle_error(exc, line_number)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liquid
4
+ class Tag
5
+ module Disabler
6
+ def render_to_output_buffer(context, output)
7
+ context.with_disabled_tags(self.class.disabled_tags) do
8
+ super
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
data/lib/liquid/tag.rb CHANGED
@@ -1,3 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'liquid/tag/disabler'
4
+ require 'liquid/tag/disableable'
5
+
1
6
  module Liquid
2
7
  class Tag
3
8
  attr_reader :nodelist, :tag_name, :line_number, :parse_context
@@ -5,20 +10,32 @@ module Liquid
5
10
  include ParserSwitching
6
11
 
7
12
  class << self
8
- def parse(tag_name, markup, tokenizer, options)
9
- tag = new(tag_name, markup, options)
13
+ def parse(tag_name, markup, tokenizer, parse_context)
14
+ tag = new(tag_name, markup, parse_context)
10
15
  tag.parse(tokenizer)
11
16
  tag
12
17
  end
13
18
 
19
+ def disable_tags(*tag_names)
20
+ tag_names += disabled_tags
21
+ define_singleton_method(:disabled_tags) { tag_names }
22
+ prepend(Disabler)
23
+ end
24
+
14
25
  private :new
26
+
27
+ protected
28
+
29
+ def disabled_tags
30
+ []
31
+ end
15
32
  end
16
33
 
17
34
  def initialize(tag_name, markup, parse_context)
18
- @tag_name = tag_name
19
- @markup = markup
35
+ @tag_name = tag_name
36
+ @markup = markup
20
37
  @parse_context = parse_context
21
- @line_number = parse_context.line_number
38
+ @line_number = parse_context.line_number
22
39
  end
23
40
 
24
41
  def parse(_tokens)
@@ -33,11 +50,30 @@ module Liquid
33
50
  end
34
51
 
35
52
  def render(_context)
36
- ''.freeze
53
+ ''
54
+ end
55
+
56
+ # For backwards compatibility with custom tags. In a future release, the semantics
57
+ # of the `render_to_output_buffer` method will become the default and the `render`
58
+ # method will be removed.
59
+ def render_to_output_buffer(context, output)
60
+ render_result = render(context)
61
+ output << render_result if render_result
62
+ output
37
63
  end
38
64
 
39
65
  def blank?
40
66
  false
41
67
  end
68
+
69
+ private
70
+
71
+ def safe_parse_expression(parser)
72
+ parse_context.safe_parse_expression(parser)
73
+ end
74
+
75
+ def parse_expression(markup, safe: false)
76
+ parse_context.parse_expression(markup, safe: safe)
77
+ end
42
78
  end
43
79
  end
@@ -1,30 +1,47 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
- # Assign sets a variable in your template.
3
- #
4
- # {% assign foo = 'monkey' %}
5
- #
6
- # You can then use the variable later in the page.
7
- #
8
- # {{ foo }}
4
+ # @liquid_public_docs
5
+ # @liquid_type tag
6
+ # @liquid_category variable
7
+ # @liquid_name assign
8
+ # @liquid_summary
9
+ # Creates a new variable.
10
+ # @liquid_description
11
+ # You can create variables of any [basic type](/docs/api/liquid/basics#types), [object](/docs/api/liquid/objects), or object property.
9
12
  #
13
+ # > Caution:
14
+ # > Predefined Liquid objects can be overridden by variables with the same name.
15
+ # > To make sure that you can access all Liquid objects, make sure that your variable name doesn't match a predefined object's name.
16
+ # @liquid_syntax
17
+ # {% assign variable_name = value %}
18
+ # @liquid_syntax_keyword variable_name The name of the variable being created.
19
+ # @liquid_syntax_keyword value The value you want to assign to the variable.
10
20
  class Assign < Tag
11
21
  Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
12
22
 
13
- def initialize(tag_name, markup, options)
23
+ # @api private
24
+ def self.raise_syntax_error(parse_context)
25
+ raise Liquid::SyntaxError, parse_context.locale.t('errors.syntax.assign')
26
+ end
27
+
28
+ attr_reader :to, :from
29
+
30
+ def initialize(tag_name, markup, parse_context)
14
31
  super
15
32
  if markup =~ Syntax
16
- @to = $1
17
- @from = Variable.new($2, options)
33
+ @to = Regexp.last_match(1)
34
+ @from = Variable.new(Regexp.last_match(2), parse_context)
18
35
  else
19
- raise SyntaxError.new options[:locale].t("errors.syntax.assign".freeze)
36
+ self.class.raise_syntax_error(parse_context)
20
37
  end
21
38
  end
22
39
 
23
- def render(context)
40
+ def render_to_output_buffer(context, output)
24
41
  val = @from.render(context)
25
42
  context.scopes.last[@to] = val
26
- context.resource_limits.assign_score += assign_score_of(val)
27
- ''.freeze
43
+ context.resource_limits.increment_assign_score(assign_score_of(val))
44
+ output
28
45
  end
29
46
 
30
47
  def blank?
@@ -35,17 +52,28 @@ module Liquid
35
52
 
36
53
  def assign_score_of(val)
37
54
  if val.instance_of?(String)
38
- val.length
39
- elsif val.instance_of?(Array) || val.instance_of?(Hash)
55
+ val.bytesize
56
+ elsif val.instance_of?(Array)
40
57
  sum = 1
41
58
  # Uses #each to avoid extra allocations.
42
59
  val.each { |child| sum += assign_score_of(child) }
43
60
  sum
61
+ elsif val.instance_of?(Hash)
62
+ sum = 1
63
+ val.each do |key, entry_value|
64
+ sum += assign_score_of(key)
65
+ sum += assign_score_of(entry_value)
66
+ end
67
+ sum
44
68
  else
45
69
  1
46
70
  end
47
71
  end
48
- end
49
72
 
50
- Template.register_tag('assign'.freeze, Assign)
73
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
74
+ def children
75
+ [@node.from]
76
+ end
77
+ end
78
+ end
51
79
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  # Break tag to be used to break out of a for loop.
3
5
  #
@@ -8,11 +10,20 @@ module Liquid
8
10
  # {% endif %}
9
11
  # {% endfor %}
10
12
  #
13
+ # @liquid_public_docs
14
+ # @liquid_type tag
15
+ # @liquid_category iteration
16
+ # @liquid_name break
17
+ # @liquid_summary
18
+ # Stops a [`for` loop](/docs/api/liquid/tags/for) from iterating.
19
+ # @liquid_syntax
20
+ # {% break %}
11
21
  class Break < Tag
12
- def interrupt
13
- BreakInterrupt.new
22
+ INTERRUPT = BreakInterrupt.new.freeze
23
+
24
+ def render_to_output_buffer(context, output)
25
+ context.push_interrupt(INTERRUPT)
26
+ output
14
27
  end
15
28
  end
16
-
17
- Template.register_tag('break'.freeze, Break)
18
29
  end
@@ -1,38 +1,46 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
- # Capture stores the result of a block into a variable without rendering it inplace.
4
+ # @liquid_public_docs
5
+ # @liquid_type tag
6
+ # @liquid_category variable
7
+ # @liquid_name capture
8
+ # @liquid_summary
9
+ # Creates a new variable with a string value.
10
+ # @liquid_description
11
+ # You can create complex strings with Liquid logic and variables.
3
12
  #
4
- # {% capture heading %}
5
- # Monkeys!
13
+ # > Caution:
14
+ # > Predefined Liquid objects can be overridden by variables with the same name.
15
+ # > To make sure that you can access all Liquid objects, make sure that your variable name doesn't match a predefined object's name.
16
+ # @liquid_syntax
17
+ # {% capture variable %}
18
+ # value
6
19
  # {% endcapture %}
7
- # ...
8
- # <h1>{{ heading }}</h1>
9
- #
10
- # Capture is useful for saving content for use later in your template, such as
11
- # in a sidebar or footer.
12
- #
20
+ # @liquid_syntax_keyword variable The name of the variable being created.
21
+ # @liquid_syntax_keyword value The value you want to assign to the variable.
13
22
  class Capture < Block
14
23
  Syntax = /(#{VariableSignature}+)/o
15
24
 
16
25
  def initialize(tag_name, markup, options)
17
26
  super
18
27
  if markup =~ Syntax
19
- @to = $1
28
+ @to = Regexp.last_match(1)
20
29
  else
21
- raise SyntaxError.new(options[:locale].t("errors.syntax.capture"))
30
+ raise SyntaxError, options[:locale].t("errors.syntax.capture")
22
31
  end
23
32
  end
24
33
 
25
- def render(context)
26
- output = super
27
- context.scopes.last[@to] = output
28
- context.resource_limits.assign_score += output.length
29
- ''.freeze
34
+ def render_to_output_buffer(context, output)
35
+ context.resource_limits.with_capture do
36
+ capture_output = render(context)
37
+ context.scopes.last[@to] = capture_output
38
+ end
39
+ output
30
40
  end
31
41
 
32
42
  def blank?
33
43
  true
34
44
  end
35
45
  end
36
-
37
- Template.register_tag('capture'.freeze, Capture)
38
46
  end
@@ -1,24 +1,50 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
4
+ # @liquid_public_docs
5
+ # @liquid_type tag
6
+ # @liquid_category conditional
7
+ # @liquid_name case
8
+ # @liquid_summary
9
+ # Renders a specific expression depending on the value of a specific variable.
10
+ # @liquid_syntax
11
+ # {% case variable %}
12
+ # {% when first_value %}
13
+ # first_expression
14
+ # {% when second_value %}
15
+ # second_expression
16
+ # {% else %}
17
+ # third_expression
18
+ # {% endcase %}
19
+ # @liquid_syntax_keyword variable The name of the variable you want to base your case statement on.
20
+ # @liquid_syntax_keyword first_value A specific value to check for.
21
+ # @liquid_syntax_keyword second_value A specific value to check for.
22
+ # @liquid_syntax_keyword first_expression An expression to be rendered when the variable's value matches `first_value`.
23
+ # @liquid_syntax_keyword second_expression An expression to be rendered when the variable's value matches `second_value`.
24
+ # @liquid_syntax_keyword third_expression An expression to be rendered when the variable's value has no match.
2
25
  class Case < Block
3
26
  Syntax = /(#{QuotedFragment})/o
4
27
  WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om
5
28
 
29
+ attr_reader :blocks, :left
30
+
6
31
  def initialize(tag_name, markup, options)
7
32
  super
8
33
  @blocks = []
9
-
10
- if markup =~ Syntax
11
- @left = Expression.parse($1)
12
- else
13
- raise SyntaxError.new(options[:locale].t("errors.syntax.case".freeze))
14
- end
34
+ parse_with_selected_parser(markup)
15
35
  end
16
36
 
17
37
  def parse(tokens)
18
- body = BlockBody.new
19
- while parse_body(body, tokens)
20
- body = @blocks.last.attachment
38
+ body = case_body = new_body
39
+ body = @blocks.last.attachment while parse_body(body, tokens)
40
+ @blocks.reverse_each do |condition|
41
+ body = condition.attachment
42
+ unless body.frozen?
43
+ body.remove_blank_strings if blank?
44
+ body.freeze
45
+ end
21
46
  end
47
+ case_body.freeze
22
48
  end
23
49
 
24
50
  def nodelist
@@ -27,45 +53,91 @@ module Liquid
27
53
 
28
54
  def unknown_tag(tag, markup, tokens)
29
55
  case tag
30
- when 'when'.freeze
56
+ when 'when'
31
57
  record_when_condition(markup)
32
- when 'else'.freeze
58
+ when 'else'
33
59
  record_else_condition(markup)
34
60
  else
35
61
  super
36
62
  end
37
63
  end
38
64
 
39
- def render(context)
40
- context.stack do
41
- execute_else_block = true
42
-
43
- output = ''
44
- @blocks.each do |block|
45
- if block.else?
46
- return block.attachment.render(context) if execute_else_block
47
- elsif block.evaluate(context)
48
- execute_else_block = false
49
- output << block.attachment.render(context)
50
- end
65
+ def render_to_output_buffer(context, output)
66
+ execute_else_block = true
67
+
68
+ @blocks.each do |block|
69
+ if block.else?
70
+ block.attachment.render_to_output_buffer(context, output) if execute_else_block
71
+ next
72
+ end
73
+
74
+ result = Liquid::Utils.to_liquid_value(
75
+ block.evaluate(context),
76
+ )
77
+
78
+ if result
79
+ execute_else_block = false
80
+ block.attachment.render_to_output_buffer(context, output)
51
81
  end
52
- output
53
82
  end
83
+
84
+ output
54
85
  end
55
86
 
56
87
  private
57
88
 
89
+ def rigid_parse(markup)
90
+ parser = @parse_context.new_parser(markup)
91
+ @left = safe_parse_expression(parser)
92
+ parser.consume(:end_of_string)
93
+ end
94
+
95
+ def strict_parse(markup)
96
+ lax_parse(markup)
97
+ end
98
+
99
+ def lax_parse(markup)
100
+ if markup =~ Syntax
101
+ @left = parse_expression(Regexp.last_match(1))
102
+ else
103
+ raise SyntaxError, options[:locale].t("errors.syntax.case")
104
+ end
105
+ end
106
+
58
107
  def record_when_condition(markup)
59
- body = BlockBody.new
108
+ body = new_body
109
+
110
+ if rigid_mode?
111
+ parse_rigid_when(markup, body)
112
+ else
113
+ parse_lax_when(markup, body)
114
+ end
115
+ end
116
+
117
+ def parse_rigid_when(markup, body)
118
+ parser = @parse_context.new_parser(markup)
119
+
120
+ loop do
121
+ expr = safe_parse_expression(parser)
122
+ block = Condition.new(@left, '==', expr)
123
+ block.attach(body)
124
+ @blocks << block
125
+
126
+ break unless parser.id?('or') || parser.consume?(:comma)
127
+ end
128
+
129
+ parser.consume(:end_of_string)
130
+ end
60
131
 
132
+ def parse_lax_when(markup, body)
61
133
  while markup
62
134
  unless markup =~ WhenSyntax
63
- raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_when".freeze))
135
+ raise SyntaxError, options[:locale].t("errors.syntax.case_invalid_when")
64
136
  end
65
137
 
66
- markup = $2
138
+ markup = Regexp.last_match(2)
67
139
 
68
- block = Condition.new(@left, '=='.freeze, Expression.parse($1))
140
+ block = Condition.new(@left, '==', Condition.parse_expression(parse_context, Regexp.last_match(1)))
69
141
  block.attach(body)
70
142
  @blocks << block
71
143
  end
@@ -73,14 +145,18 @@ module Liquid
73
145
 
74
146
  def record_else_condition(markup)
75
147
  unless markup.strip.empty?
76
- raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_else".freeze))
148
+ raise SyntaxError, options[:locale].t("errors.syntax.case_invalid_else")
77
149
  end
78
150
 
79
151
  block = ElseCondition.new
80
- block.attach(BlockBody.new)
152
+ block.attach(new_body)
81
153
  @blocks << block
82
154
  end
83
- end
84
155
 
85
- Template.register_tag('case'.freeze, Case)
156
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
157
+ def children
158
+ [@node.left] + @node.blocks
159
+ end
160
+ end
161
+ end
86
162
  end