liquid 3.0.6 → 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 (113) hide show
  1. checksums.yaml +5 -5
  2. data/History.md +243 -58
  3. data/README.md +43 -4
  4. data/lib/liquid/block.rb +57 -123
  5. data/lib/liquid/block_body.rb +217 -85
  6. data/lib/liquid/condition.rb +92 -45
  7. data/lib/liquid/context.rb +154 -89
  8. data/lib/liquid/document.rb +57 -9
  9. data/lib/liquid/drop.rb +20 -17
  10. data/lib/liquid/errors.rb +27 -29
  11. data/lib/liquid/expression.rb +32 -20
  12. data/lib/liquid/extensions.rb +21 -7
  13. data/lib/liquid/file_system.rb +17 -15
  14. data/lib/liquid/forloop_drop.rb +92 -0
  15. data/lib/liquid/i18n.rb +10 -8
  16. data/lib/liquid/interrupts.rb +4 -3
  17. data/lib/liquid/lexer.rb +37 -26
  18. data/lib/liquid/locales/en.yml +13 -6
  19. data/lib/liquid/parse_context.rb +54 -0
  20. data/lib/liquid/parse_tree_visitor.rb +42 -0
  21. data/lib/liquid/parser.rb +30 -18
  22. data/lib/liquid/parser_switching.rb +20 -6
  23. data/lib/liquid/partial_cache.rb +24 -0
  24. data/lib/liquid/profiler/hooks.rb +26 -14
  25. data/lib/liquid/profiler.rb +72 -92
  26. data/lib/liquid/range_lookup.rb +28 -3
  27. data/lib/liquid/registers.rb +51 -0
  28. data/lib/liquid/resource_limits.rb +62 -0
  29. data/lib/liquid/standardfilters.rb +715 -132
  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 +121 -0
  33. data/lib/liquid/tag/disableable.rb +22 -0
  34. data/lib/liquid/tag/disabler.rb +21 -0
  35. data/lib/liquid/tag.rb +35 -12
  36. data/lib/liquid/tags/assign.rb +57 -18
  37. data/lib/liquid/tags/break.rb +15 -5
  38. data/lib/liquid/tags/capture.rb +24 -18
  39. data/lib/liquid/tags/case.rb +79 -30
  40. data/lib/liquid/tags/comment.rb +19 -4
  41. data/lib/liquid/tags/continue.rb +16 -12
  42. data/lib/liquid/tags/cycle.rb +47 -27
  43. data/lib/liquid/tags/decrement.rb +23 -24
  44. data/lib/liquid/tags/echo.rb +41 -0
  45. data/lib/liquid/tags/for.rb +155 -124
  46. data/lib/liquid/tags/if.rb +97 -63
  47. data/lib/liquid/tags/ifchanged.rb +11 -12
  48. data/lib/liquid/tags/include.rb +82 -73
  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 +50 -8
  52. data/lib/liquid/tags/render.rb +109 -0
  53. data/lib/liquid/tags/table_row.rb +57 -41
  54. data/lib/liquid/tags/unless.rb +38 -20
  55. data/lib/liquid/template.rb +71 -103
  56. data/lib/liquid/template_factory.rb +9 -0
  57. data/lib/liquid/tokenizer.rb +39 -0
  58. data/lib/liquid/usage.rb +8 -0
  59. data/lib/liquid/utils.rb +63 -9
  60. data/lib/liquid/variable.rb +74 -56
  61. data/lib/liquid/variable_lookup.rb +31 -15
  62. data/lib/liquid/version.rb +3 -1
  63. data/lib/liquid.rb +27 -12
  64. metadata +30 -106
  65. data/lib/liquid/module_ex.rb +0 -62
  66. data/lib/liquid/strainer.rb +0 -59
  67. data/lib/liquid/token.rb +0 -18
  68. data/test/fixtures/en_locale.yml +0 -9
  69. data/test/integration/assign_test.rb +0 -48
  70. data/test/integration/blank_test.rb +0 -106
  71. data/test/integration/capture_test.rb +0 -50
  72. data/test/integration/context_test.rb +0 -32
  73. data/test/integration/drop_test.rb +0 -271
  74. data/test/integration/error_handling_test.rb +0 -207
  75. data/test/integration/filter_test.rb +0 -138
  76. data/test/integration/hash_ordering_test.rb +0 -23
  77. data/test/integration/output_test.rb +0 -124
  78. data/test/integration/parsing_quirks_test.rb +0 -116
  79. data/test/integration/render_profiling_test.rb +0 -154
  80. data/test/integration/security_test.rb +0 -64
  81. data/test/integration/standard_filter_test.rb +0 -396
  82. data/test/integration/tags/break_tag_test.rb +0 -16
  83. data/test/integration/tags/continue_tag_test.rb +0 -16
  84. data/test/integration/tags/for_tag_test.rb +0 -375
  85. data/test/integration/tags/if_else_tag_test.rb +0 -190
  86. data/test/integration/tags/include_tag_test.rb +0 -234
  87. data/test/integration/tags/increment_tag_test.rb +0 -24
  88. data/test/integration/tags/raw_tag_test.rb +0 -25
  89. data/test/integration/tags/standard_tag_test.rb +0 -297
  90. data/test/integration/tags/statements_test.rb +0 -113
  91. data/test/integration/tags/table_row_test.rb +0 -63
  92. data/test/integration/tags/unless_else_tag_test.rb +0 -26
  93. data/test/integration/template_test.rb +0 -182
  94. data/test/integration/variable_test.rb +0 -82
  95. data/test/test_helper.rb +0 -89
  96. data/test/unit/block_unit_test.rb +0 -55
  97. data/test/unit/condition_unit_test.rb +0 -149
  98. data/test/unit/context_unit_test.rb +0 -492
  99. data/test/unit/file_system_unit_test.rb +0 -35
  100. data/test/unit/i18n_unit_test.rb +0 -37
  101. data/test/unit/lexer_unit_test.rb +0 -48
  102. data/test/unit/module_ex_unit_test.rb +0 -87
  103. data/test/unit/parser_unit_test.rb +0 -82
  104. data/test/unit/regexp_unit_test.rb +0 -44
  105. data/test/unit/strainer_unit_test.rb +0 -69
  106. data/test/unit/tag_unit_test.rb +0 -16
  107. data/test/unit/tags/case_tag_unit_test.rb +0 -10
  108. data/test/unit/tags/for_tag_unit_test.rb +0 -13
  109. data/test/unit/tags/if_tag_unit_test.rb +0 -8
  110. data/test/unit/template_unit_test.rb +0 -69
  111. data/test/unit/tokenizer_unit_test.rb +0 -38
  112. data/test/unit/variable_unit_test.rb +0 -145
  113. /data/{MIT-LICENSE → LICENSE} +0 -0
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liquid
4
+ # StrainerFactory is the factory for the filters system.
5
+ module StrainerFactory
6
+ extend self
7
+
8
+ def add_global_filter(filter)
9
+ strainer_class_cache.clear
10
+ GlobalCache.add_filter(filter)
11
+ end
12
+
13
+ def create(context, filters = [])
14
+ strainer_from_cache(filters).new(context)
15
+ end
16
+
17
+ def global_filter_names
18
+ GlobalCache.filter_method_names
19
+ end
20
+
21
+ GlobalCache = Class.new(StrainerTemplate)
22
+
23
+ private
24
+
25
+ def strainer_from_cache(filters)
26
+ if filters.empty?
27
+ GlobalCache
28
+ else
29
+ strainer_class_cache[filters] ||= begin
30
+ klass = Class.new(GlobalCache)
31
+ filters.each { |f| klass.add_filter(f) }
32
+ klass
33
+ end
34
+ end
35
+ end
36
+
37
+ def strainer_class_cache
38
+ @strainer_class_cache ||= {}
39
+ end
40
+ end
41
+ end
@@ -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
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
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](/api/liquid/tags#tablerow).
9
+ class TablerowloopDrop < Drop
10
+ def initialize(length, cols)
11
+ @length = length
12
+ @row = 1
13
+ @col = 1
14
+ @cols = cols
15
+ @index = 0
16
+ end
17
+
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
35
+
36
+ # @liquid_public_docs
37
+ # @liquid_summary
38
+ # The 1-based index of the current iteration.
39
+ # @liquid_return [number]
40
+ def index
41
+ @index + 1
42
+ end
43
+
44
+ # @liquid_public_docs
45
+ # @liquid_summary
46
+ # The 0-based index of the current iteration.
47
+ # @liquid_return [number]
48
+ def index0
49
+ @index
50
+ end
51
+
52
+ # @liquid_public_docs
53
+ # @liquid_summary
54
+ # The 0-based index of the current column.
55
+ # @liquid_return [number]
56
+ def col0
57
+ @col - 1
58
+ end
59
+
60
+ # @liquid_public_docs
61
+ # @liquid_summary
62
+ # The 1-based index of the current iteration, in reverse order.
63
+ # @liquid_return [number]
64
+ def rindex
65
+ @length - @index
66
+ end
67
+
68
+ # @liquid_public_docs
69
+ # @liquid_summary
70
+ # The 0-based index of the current iteration, in reverse order.
71
+ # @liquid_return [number]
72
+ def rindex0
73
+ @length - @index - 1
74
+ end
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]
80
+ def first
81
+ @index == 0
82
+ end
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]
88
+ def last
89
+ @index == @length - 1
90
+ end
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]
96
+ def col_first
97
+ @col == 1
98
+ end
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]
104
+ def col_last
105
+ @col == @cols
106
+ end
107
+
108
+ protected
109
+
110
+ def increment!
111
+ @index += 1
112
+
113
+ if @col == @cols
114
+ @col = 1
115
+ @row += 1
116
+ else
117
+ @col += 1
118
+ end
119
+ end
120
+ end
121
+ 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,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liquid
4
+ class Tag
5
+ module Disabler
6
+ module ClassMethods
7
+ attr_reader :disabled_tags
8
+ end
9
+
10
+ def self.prepended(base)
11
+ base.extend(ClassMethods)
12
+ end
13
+
14
+ def render_to_output_buffer(context, output)
15
+ context.with_disabled_tags(self.class.disabled_tags) do
16
+ super
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
data/lib/liquid/tag.rb CHANGED
@@ -1,26 +1,35 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  class Tag
3
- attr_accessor :options, :line_number
4
- attr_reader :nodelist, :warnings
5
+ attr_reader :nodelist, :tag_name, :line_number, :parse_context
6
+ alias_method :options, :parse_context
5
7
  include ParserSwitching
6
8
 
7
9
  class << self
8
- def parse(tag_name, markup, tokens, options)
9
- tag = new(tag_name, markup, options)
10
- tag.parse(tokens)
10
+ def parse(tag_name, markup, tokenizer, parse_context)
11
+ tag = new(tag_name, markup, parse_context)
12
+ tag.parse(tokenizer)
11
13
  tag
12
14
  end
13
15
 
16
+ def disable_tags(*tag_names)
17
+ @disabled_tags ||= []
18
+ @disabled_tags.concat(tag_names)
19
+ prepend(Disabler)
20
+ end
21
+
14
22
  private :new
15
23
  end
16
24
 
17
- def initialize(tag_name, markup, options)
18
- @tag_name = tag_name
19
- @markup = markup
20
- @options = options
25
+ def initialize(tag_name, markup, parse_context)
26
+ @tag_name = tag_name
27
+ @markup = markup
28
+ @parse_context = parse_context
29
+ @line_number = parse_context.line_number
21
30
  end
22
31
 
23
- def parse(tokens)
32
+ def parse(_tokens)
24
33
  end
25
34
 
26
35
  def raw
@@ -31,12 +40,26 @@ module Liquid
31
40
  self.class.name.downcase
32
41
  end
33
42
 
34
- def render(context)
35
- ''.freeze
43
+ def render(_context)
44
+ ''
45
+ end
46
+
47
+ # For backwards compatibility with custom tags. In a future release, the semantics
48
+ # of the `render_to_output_buffer` method will become the default and the `render`
49
+ # method will be removed.
50
+ def render_to_output_buffer(context, output)
51
+ output << render(context)
52
+ output
36
53
  end
37
54
 
38
55
  def blank?
39
56
  false
40
57
  end
58
+
59
+ private
60
+
61
+ def parse_expression(markup)
62
+ parse_context.parse_expression(markup)
63
+ end
41
64
  end
42
65
  end
@@ -1,38 +1,77 @@
1
- module Liquid
1
+ # frozen_string_literal: true
2
2
 
3
- # Assign sets a variable in your template.
4
- #
5
- # {% assign foo = 'monkey' %}
6
- #
7
- # You can then use the variable later in the page.
8
- #
9
- # {{ foo }}
10
- #
3
+ module Liquid
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](/api/liquid/basics#types), [object](/api/liquid/objects), or object property.
12
+ # @liquid_syntax
13
+ # {% assign variable_name = value %}
14
+ # @liquid_syntax_keyword variable_name The name of the variable being created.
15
+ # @liquid_syntax_keyword value The value you want to assign to the variable.
11
16
  class Assign < Tag
12
17
  Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
13
18
 
14
- def initialize(tag_name, markup, options)
19
+ # @api private
20
+ def self.raise_syntax_error(parse_context)
21
+ raise Liquid::SyntaxError, parse_context.locale.t('errors.syntax.assign')
22
+ end
23
+
24
+ attr_reader :to, :from
25
+
26
+ def initialize(tag_name, markup, parse_context)
15
27
  super
16
28
  if markup =~ Syntax
17
- @to = $1
18
- @from = Variable.new($2,options)
19
- @from.line_number = line_number
29
+ @to = Regexp.last_match(1)
30
+ @from = Variable.new(Regexp.last_match(2), parse_context)
20
31
  else
21
- raise SyntaxError.new options[:locale].t("errors.syntax.assign".freeze)
32
+ self.class.raise_syntax_error(parse_context)
22
33
  end
23
34
  end
24
35
 
25
- def render(context)
36
+ def render_to_output_buffer(context, output)
26
37
  val = @from.render(context)
27
38
  context.scopes.last[@to] = val
28
- context.increment_used_resources(:assign_score_current, val)
29
- ''.freeze
39
+ context.resource_limits.increment_assign_score(assign_score_of(val))
40
+ output
30
41
  end
31
42
 
32
43
  def blank?
33
44
  true
34
45
  end
46
+
47
+ private
48
+
49
+ def assign_score_of(val)
50
+ if val.instance_of?(String)
51
+ val.bytesize
52
+ elsif val.instance_of?(Array)
53
+ sum = 1
54
+ # Uses #each to avoid extra allocations.
55
+ val.each { |child| sum += assign_score_of(child) }
56
+ sum
57
+ elsif val.instance_of?(Hash)
58
+ sum = 1
59
+ val.each do |key, entry_value|
60
+ sum += assign_score_of(key)
61
+ sum += assign_score_of(entry_value)
62
+ end
63
+ sum
64
+ else
65
+ 1
66
+ end
67
+ end
68
+
69
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
70
+ def children
71
+ [@node.from]
72
+ end
73
+ end
35
74
  end
36
75
 
37
- Template.register_tag('assign'.freeze, Assign)
76
+ Template.register_tag('assign', Assign)
38
77
  end
@@ -1,5 +1,6 @@
1
- module Liquid
1
+ # frozen_string_literal: true
2
2
 
3
+ module Liquid
3
4
  # Break tag to be used to break out of a for loop.
4
5
  #
5
6
  # == Basic Usage:
@@ -9,13 +10,22 @@ module Liquid
9
10
  # {% endif %}
10
11
  # {% endfor %}
11
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](/api/liquid/tags#for) from iterating.
19
+ # @liquid_syntax
20
+ # {% break %}
12
21
  class Break < Tag
22
+ INTERRUPT = BreakInterrupt.new.freeze
13
23
 
14
- def interrupt
15
- BreakInterrupt.new
24
+ def render_to_output_buffer(context, output)
25
+ context.push_interrupt(INTERRUPT)
26
+ output
16
27
  end
17
-
18
28
  end
19
29
 
20
- Template.register_tag('break'.freeze, Break)
30
+ Template.register_tag('break', Break)
21
31
  end
@@ -1,32 +1,38 @@
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.
3
- #
4
- # {% capture heading %}
5
- # Monkeys!
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.
12
+ # @liquid_syntax
13
+ # {% capture variable %}
14
+ # value
6
15
  # {% 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
- #
16
+ # @liquid_syntax_keyword variable The name of the variable being created.
17
+ # @liquid_syntax_keyword value The value you want to assign to the variable.
13
18
  class Capture < Block
14
19
  Syntax = /(#{VariableSignature}+)/o
15
20
 
16
21
  def initialize(tag_name, markup, options)
17
22
  super
18
23
  if markup =~ Syntax
19
- @to = $1
24
+ @to = Regexp.last_match(1)
20
25
  else
21
- raise SyntaxError.new(options[:locale].t("errors.syntax.capture"))
26
+ raise SyntaxError, options[:locale].t("errors.syntax.capture")
22
27
  end
23
28
  end
24
29
 
25
- def render(context)
26
- output = super
27
- context.scopes.last[@to] = output
28
- context.increment_used_resources(:assign_score_current, output)
29
- ''.freeze
30
+ def render_to_output_buffer(context, output)
31
+ context.resource_limits.with_capture do
32
+ capture_output = render(context)
33
+ context.scopes.last[@to] = capture_output
34
+ end
35
+ output
30
36
  end
31
37
 
32
38
  def blank?
@@ -34,5 +40,5 @@ module Liquid
34
40
  end
35
41
  end
36
42
 
37
- Template.register_tag('capture'.freeze, Capture)
43
+ Template.register_tag('capture', Capture)
38
44
  end
@@ -1,79 +1,128 @@
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
34
 
10
35
  if markup =~ Syntax
11
- @left = $1
36
+ @left = parse_expression(Regexp.last_match(1))
12
37
  else
13
- raise SyntaxError.new(options[:locale].t("errors.syntax.case".freeze))
38
+ raise SyntaxError, options[:locale].t("errors.syntax.case")
39
+ end
40
+ end
41
+
42
+ def parse(tokens)
43
+ body = case_body = new_body
44
+ body = @blocks.last.attachment while parse_body(body, tokens)
45
+ @blocks.reverse_each do |condition|
46
+ body = condition.attachment
47
+ unless body.frozen?
48
+ body.remove_blank_strings if blank?
49
+ body.freeze
50
+ end
14
51
  end
52
+ case_body.freeze
15
53
  end
16
54
 
17
55
  def nodelist
18
- @blocks.flat_map(&:attachment)
56
+ @blocks.map(&:attachment)
19
57
  end
20
58
 
21
59
  def unknown_tag(tag, markup, tokens)
22
- @nodelist = []
23
60
  case tag
24
- when 'when'.freeze
61
+ when 'when'
25
62
  record_when_condition(markup)
26
- when 'else'.freeze
63
+ when 'else'
27
64
  record_else_condition(markup)
28
65
  else
29
66
  super
30
67
  end
31
68
  end
32
69
 
33
- def render(context)
34
- context.stack do
35
- execute_else_block = true
36
-
37
- output = ''
38
- @blocks.each do |block|
39
- if block.else?
40
- return render_all(block.attachment, context) if execute_else_block
41
- elsif block.evaluate(context)
42
- execute_else_block = false
43
- output << render_all(block.attachment, context)
44
- end
70
+ def render_to_output_buffer(context, output)
71
+ execute_else_block = true
72
+
73
+ @blocks.each do |block|
74
+ if block.else?
75
+ block.attachment.render_to_output_buffer(context, output) if execute_else_block
76
+ next
77
+ end
78
+
79
+ result = Liquid::Utils.to_liquid_value(
80
+ block.evaluate(context)
81
+ )
82
+
83
+ if result
84
+ execute_else_block = false
85
+ block.attachment.render_to_output_buffer(context, output)
45
86
  end
46
- output
47
87
  end
88
+
89
+ output
48
90
  end
49
91
 
50
92
  private
51
93
 
52
94
  def record_when_condition(markup)
95
+ body = new_body
96
+
53
97
  while markup
54
- # Create a new nodelist and assign it to the new block
55
- if not markup =~ WhenSyntax
56
- raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_when".freeze))
98
+ unless markup =~ WhenSyntax
99
+ raise SyntaxError, options[:locale].t("errors.syntax.case_invalid_when")
57
100
  end
58
101
 
59
- markup = $2
102
+ markup = Regexp.last_match(2)
60
103
 
61
- block = Condition.new(@left, '=='.freeze, $1)
62
- block.attach(@nodelist)
63
- @blocks.push(block)
104
+ block = Condition.new(@left, '==', Condition.parse_expression(parse_context, Regexp.last_match(1)))
105
+ block.attach(body)
106
+ @blocks << block
64
107
  end
65
108
  end
66
109
 
67
110
  def record_else_condition(markup)
68
- if not markup.strip.empty?
69
- raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_else".freeze))
111
+ unless markup.strip.empty?
112
+ raise SyntaxError, options[:locale].t("errors.syntax.case_invalid_else")
70
113
  end
71
114
 
72
115
  block = ElseCondition.new
73
- block.attach(@nodelist)
116
+ block.attach(new_body)
74
117
  @blocks << block
75
118
  end
119
+
120
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
121
+ def children
122
+ [@node.left] + @node.blocks
123
+ end
124
+ end
76
125
  end
77
126
 
78
- Template.register_tag('case'.freeze, Case)
127
+ Template.register_tag('case', Case)
79
128
  end