liquid2 0.1.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 (84) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +0 -0
  3. data/.rubocop.yml +46 -0
  4. data/.ruby-version +1 -0
  5. data/.vscode/settings.json +32 -0
  6. data/CHANGELOG.md +5 -0
  7. data/LICENSE.txt +21 -0
  8. data/LICENSE_SHOPIFY.txt +20 -0
  9. data/README.md +219 -0
  10. data/Rakefile +23 -0
  11. data/Steepfile +26 -0
  12. data/lib/liquid2/context.rb +297 -0
  13. data/lib/liquid2/environment.rb +287 -0
  14. data/lib/liquid2/errors.rb +79 -0
  15. data/lib/liquid2/expression.rb +20 -0
  16. data/lib/liquid2/expressions/arguments.rb +25 -0
  17. data/lib/liquid2/expressions/array.rb +20 -0
  18. data/lib/liquid2/expressions/blank.rb +41 -0
  19. data/lib/liquid2/expressions/boolean.rb +20 -0
  20. data/lib/liquid2/expressions/filtered.rb +136 -0
  21. data/lib/liquid2/expressions/identifier.rb +43 -0
  22. data/lib/liquid2/expressions/lambda.rb +53 -0
  23. data/lib/liquid2/expressions/logical.rb +71 -0
  24. data/lib/liquid2/expressions/loop.rb +79 -0
  25. data/lib/liquid2/expressions/path.rb +33 -0
  26. data/lib/liquid2/expressions/range.rb +28 -0
  27. data/lib/liquid2/expressions/relational.rb +119 -0
  28. data/lib/liquid2/expressions/template_string.rb +20 -0
  29. data/lib/liquid2/filter.rb +95 -0
  30. data/lib/liquid2/filters/array.rb +202 -0
  31. data/lib/liquid2/filters/date.rb +20 -0
  32. data/lib/liquid2/filters/default.rb +16 -0
  33. data/lib/liquid2/filters/json.rb +15 -0
  34. data/lib/liquid2/filters/math.rb +87 -0
  35. data/lib/liquid2/filters/size.rb +11 -0
  36. data/lib/liquid2/filters/slice.rb +17 -0
  37. data/lib/liquid2/filters/sort.rb +96 -0
  38. data/lib/liquid2/filters/string.rb +204 -0
  39. data/lib/liquid2/loader.rb +59 -0
  40. data/lib/liquid2/loaders/file_system_loader.rb +76 -0
  41. data/lib/liquid2/loaders/mixins.rb +52 -0
  42. data/lib/liquid2/node.rb +113 -0
  43. data/lib/liquid2/nodes/comment.rb +18 -0
  44. data/lib/liquid2/nodes/output.rb +24 -0
  45. data/lib/liquid2/nodes/tags/assign.rb +35 -0
  46. data/lib/liquid2/nodes/tags/block_comment.rb +26 -0
  47. data/lib/liquid2/nodes/tags/capture.rb +40 -0
  48. data/lib/liquid2/nodes/tags/case.rb +111 -0
  49. data/lib/liquid2/nodes/tags/cycle.rb +63 -0
  50. data/lib/liquid2/nodes/tags/decrement.rb +29 -0
  51. data/lib/liquid2/nodes/tags/doc.rb +24 -0
  52. data/lib/liquid2/nodes/tags/echo.rb +31 -0
  53. data/lib/liquid2/nodes/tags/extends.rb +3 -0
  54. data/lib/liquid2/nodes/tags/for.rb +155 -0
  55. data/lib/liquid2/nodes/tags/if.rb +84 -0
  56. data/lib/liquid2/nodes/tags/include.rb +123 -0
  57. data/lib/liquid2/nodes/tags/increment.rb +29 -0
  58. data/lib/liquid2/nodes/tags/inline_comment.rb +28 -0
  59. data/lib/liquid2/nodes/tags/liquid.rb +29 -0
  60. data/lib/liquid2/nodes/tags/macro.rb +3 -0
  61. data/lib/liquid2/nodes/tags/raw.rb +30 -0
  62. data/lib/liquid2/nodes/tags/render.rb +137 -0
  63. data/lib/liquid2/nodes/tags/tablerow.rb +143 -0
  64. data/lib/liquid2/nodes/tags/translate.rb +3 -0
  65. data/lib/liquid2/nodes/tags/unless.rb +23 -0
  66. data/lib/liquid2/nodes/tags/with.rb +3 -0
  67. data/lib/liquid2/parser.rb +917 -0
  68. data/lib/liquid2/scanner.rb +595 -0
  69. data/lib/liquid2/static_analysis.rb +301 -0
  70. data/lib/liquid2/tag.rb +22 -0
  71. data/lib/liquid2/template.rb +182 -0
  72. data/lib/liquid2/undefined.rb +131 -0
  73. data/lib/liquid2/utils/cache.rb +80 -0
  74. data/lib/liquid2/utils/chain_hash.rb +40 -0
  75. data/lib/liquid2/utils/unescape.rb +119 -0
  76. data/lib/liquid2/version.rb +5 -0
  77. data/lib/liquid2.rb +90 -0
  78. data/performance/benchmark.rb +73 -0
  79. data/performance/memory_profile.rb +62 -0
  80. data/performance/profile.rb +71 -0
  81. data/sig/liquid2.rbs +2348 -0
  82. data.tar.gz.sig +0 -0
  83. metadata +164 -0
  84. metadata.gz.sig +0 -0
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../expression"
4
+
5
+ module Liquid2
6
+ # A keyword argument with a name and a value.
7
+ class KeywordArgument < Expression
8
+ attr_reader :value, :name, :sym
9
+
10
+ # @param name [String]
11
+ # @param value [Expression]
12
+ def initialize(token, name, value)
13
+ super(token)
14
+ @name = name
15
+ @sym = name.to_sym
16
+ @value = value
17
+ end
18
+
19
+ def evaluate(context)
20
+ [@name, context.evaluate(@value)]
21
+ end
22
+
23
+ def children = [@value]
24
+ end
25
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../expression"
4
+
5
+ module Liquid2
6
+ # An array literal.
7
+ class ArrayLiteral < Expression
8
+ # @param items [Array<Expression>]
9
+ def initialize(token, items)
10
+ super(token)
11
+ @items = items
12
+ end
13
+
14
+ def evaluate(context)
15
+ @items.map { |item| context.evaluate(item) }
16
+ end
17
+
18
+ def children = @items
19
+ end
20
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../expression"
4
+
5
+ module Liquid2
6
+ # The special value _blank_.
7
+ class Blank < Expression
8
+ def evaluate(_context)
9
+ self
10
+ end
11
+
12
+ def ==(other)
13
+ return true if other.is_a?(String) && (other.empty? || other.match?(/\A\s+\Z/))
14
+
15
+ return other.empty? if other.respond_to?(:empty?)
16
+
17
+ other.is_a?(Blank)
18
+ end
19
+
20
+ alias eql? ==
21
+
22
+ def to_s = ""
23
+ end
24
+
25
+ # The special value _empty_.
26
+ class Empty < Expression
27
+ def evaluate(_context)
28
+ self
29
+ end
30
+
31
+ def ==(other)
32
+ return other.empty? if other.respond_to?(:empty?)
33
+
34
+ other.is_a?(Empty)
35
+ end
36
+
37
+ alias eql? ==
38
+
39
+ def to_s = ""
40
+ end
41
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../expression"
4
+
5
+ module Liquid2
6
+ # An expression that evaluates to true or false.
7
+ class BooleanExpression < Expression
8
+ # @param expr [Expression]
9
+ def initialize(token, expr)
10
+ super(token)
11
+ @expr = expr
12
+ end
13
+
14
+ def evaluate(context)
15
+ Liquid2.truthy?(context, context.evaluate(@expr))
16
+ end
17
+
18
+ def children = [@expr]
19
+ end
20
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../expression"
4
+
5
+ module Liquid2
6
+ # A primary expression with optional filters.
7
+ class FilteredExpression < Expression
8
+ attr_reader :filters
9
+
10
+ def initialize(token, left, filters)
11
+ super(token)
12
+ @left = left
13
+ @filters = filters
14
+ end
15
+
16
+ def evaluate(context)
17
+ left = context.evaluate(@left)
18
+ index = 0
19
+ while (filter = @filters[index])
20
+ left = filter.evaluate(left, context)
21
+ index += 1
22
+ end
23
+ left
24
+ end
25
+
26
+ def children =[@left, *@filters]
27
+ end
28
+
29
+ # An inline conditional expression.
30
+ class TernaryExpression < Expression
31
+ attr_reader :filters, :tail_filters
32
+
33
+ # @param left [FilteredExpression]
34
+ # @param condition [BooleanExpression]
35
+ # @param alternative [Expression | nil]
36
+ # @param filters [Array<Filter>]
37
+ # @param tail_filters [Array<Filter>]
38
+ def initialize(token, left, condition, alternative, filters, tail_filters)
39
+ super(token)
40
+ @left = left
41
+ @condition = condition
42
+ @alternative = alternative
43
+ @filters = filters
44
+ @tail_filters = tail_filters
45
+ end
46
+
47
+ def evaluate(context)
48
+ rv = nil
49
+
50
+ if @condition.evaluate(context)
51
+ rv = @left.evaluate(context)
52
+ elsif @alternative
53
+ rv = context.evaluate(@alternative)
54
+ index = 0
55
+ while (filter = @filters[index])
56
+ rv = filter.evaluate(rv, context)
57
+ index += 1
58
+ end
59
+ end
60
+
61
+ index = 0
62
+ while (filter = @tail_filters[index])
63
+ rv = filter.evaluate(rv, context)
64
+ index += 1
65
+ end
66
+ rv
67
+ end
68
+
69
+ def children
70
+ # @type var nodes: Array[untyped]
71
+ nodes = [@left, @condition]
72
+ nodes << @alternative if @alternative
73
+ nodes.concat(@filters) if @filters
74
+ nodes.concat(@tail_filters) if @tail_filters
75
+ nodes
76
+ end
77
+ end
78
+
79
+ # A Liquid filter with a name and array of arguments.
80
+ class Filter < Expression
81
+ attr_reader :name, :args
82
+
83
+ # @param name [String]
84
+ # @param args [Array[Expression]]
85
+ def initialize(token, name, args)
86
+ super(token)
87
+ @name = name
88
+ @args = args
89
+ end
90
+
91
+ def evaluate(left, context)
92
+ filter, with_context = context.env.filters[@name]
93
+ raise LiquidFilterNotFoundError.new("unknown filter #{@name.inspect}", @token) unless filter
94
+
95
+ positional_args, keyword_args = evaluate_args(context)
96
+ keyword_args[:context] = context if with_context
97
+
98
+ if keyword_args.empty?
99
+ filter.call(left, *positional_args) # steep:ignore
100
+ else
101
+ filter.call(left, *positional_args, **keyword_args) # steep:ignore
102
+ end
103
+ rescue ArgumentError, TypeError => e
104
+ raise LiquidArgumentError.new(e.message, @token)
105
+ end
106
+
107
+ def children = @args
108
+
109
+ private
110
+
111
+ # @param context [RenderContext]
112
+ # @return [positional arguments, keyword arguments] An array with two elements.
113
+ # The first is an array of evaluates positional arguments. The second is a hash
114
+ # of keyword names to evaluated keyword values.
115
+ def evaluate_args(context)
116
+ positional_args = [] # @type var positional_args: Array[untyped]
117
+ keyword_args = {} # @type var keyword_args: Hash[Symbol, untyped]
118
+
119
+ index = 0
120
+ loop do
121
+ # `@args[index]` could be `false` or `nil`
122
+ break if index >= @args.length
123
+
124
+ arg = @args[index]
125
+ index += 1
126
+ if arg.respond_to?(:sym)
127
+ keyword_args[arg.sym] = context.evaluate(arg.value)
128
+ else
129
+ positional_args << context.evaluate(arg)
130
+ end
131
+ end
132
+
133
+ [positional_args, keyword_args]
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../expression"
4
+
5
+ module Liquid2
6
+ # A single word identifier.
7
+ class Identifier < Expression
8
+ attr_reader :name
9
+
10
+ # Try to cast _expr_ to an Identifier.
11
+ # @param expr [Expression]
12
+ def self.from(expr, trailing_question: true)
13
+ # TODO: trailing question
14
+ # TODO: expr might not have a token if its not a path.
15
+ unless expr.is_a?(Path) && expr.segments.empty?
16
+ raise LiquidSyntaxError.new("expected an identifier, found #{expr}", expr.token)
17
+ end
18
+
19
+ val = expr.head
20
+
21
+ unless val.is_a?(String)
22
+ raise LiquidSyntaxError.new("expected an identifier, found #{val}", expr.token)
23
+ end
24
+
25
+ # TODO: optimize
26
+ unless val.to_s.match?(/[\u0080-\uFFFFa-zA-Z_][\u0080-\uFFFFa-zA-Z0-9_-]*/)
27
+ raise LiquidSyntaxError.new("invalid identifier", expr.token)
28
+ end
29
+
30
+ new(expr.token)
31
+ end
32
+
33
+ # @param token [[Symbol, String?, Integer]]
34
+ def initialize(token)
35
+ super
36
+ @name = token[1]
37
+ end
38
+
39
+ def evaluate(_context)
40
+ @name
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../expression"
4
+
5
+ module Liquid2
6
+ # A lambda expression aka arrow function
7
+ class Lambda < Expression
8
+ attr_reader :params, :expr
9
+
10
+ # @param params [Array<Identifier>]
11
+ # @param expr [Expression]
12
+ def initialize(token, params, expr)
13
+ super(token)
14
+ @params = params
15
+ @expr = expr
16
+ end
17
+
18
+ def evaluate(_context) = self
19
+
20
+ def children = [@expr]
21
+
22
+ # Apply this lambda function to elements from _enum_.
23
+ # @param context [RenderContext]
24
+ # @param enum [Enumerable<Object>]
25
+ # @return [Enumerable<Object>]
26
+ def map(context, enum)
27
+ scope = {} # : Hash[String, untyped]
28
+ rv = [] # : Array[untyped]
29
+
30
+ if @params.length == 1
31
+ param = @params.first.name
32
+ context.extend(scope) do
33
+ enum.each do |item|
34
+ scope[param] = item
35
+ rv << context.evaluate(@expr)
36
+ end
37
+ end
38
+ else
39
+ name_param = @params.first.name
40
+ index_param = @params[1].name
41
+ context.extend(scope) do
42
+ enum.each_with_index do |item, index|
43
+ scope[index_param] = index
44
+ scope[name_param] = item
45
+ rv << context.evaluate(@expr)
46
+ end
47
+ end
48
+ end
49
+
50
+ rv
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../expression"
4
+
5
+ module Liquid2
6
+ # A negated expression.
7
+ class LogicalNot < Expression
8
+ # @param expr [Expression]
9
+ def initialize(token, expr)
10
+ super(token)
11
+ @expr = expr
12
+ end
13
+
14
+ def evaluate(context)
15
+ !Liquid2.truthy?(context, context.evaluate(@expr))
16
+ end
17
+
18
+ def children = [@expr]
19
+ end
20
+
21
+ # Logical conjunction.
22
+ class LogicalAnd < Expression
23
+ # @param left [Expression]
24
+ # @param right [Expression]
25
+ def initialize(token, left, right)
26
+ super(token)
27
+ @left = left
28
+ @right = right
29
+ end
30
+
31
+ def evaluate(context)
32
+ left = context.evaluate(@left)
33
+ Liquid2.truthy?(context, left) ? context.evaluate(@right) : left
34
+ end
35
+
36
+ def children = [@left, @right]
37
+ end
38
+
39
+ # Logical disjunction.
40
+ class LogicalOr < Expression
41
+ # @param left [Expression]
42
+ # @param right [Expression]
43
+ def initialize(token, left, right)
44
+ super(token)
45
+ @left = left
46
+ @right = right
47
+ end
48
+
49
+ def evaluate(context)
50
+ left = context.evaluate(@left)
51
+ Liquid2.truthy?(context, left) ? left : context.evaluate(@right)
52
+ end
53
+
54
+ def children = [@left, @right]
55
+ end
56
+
57
+ # A logical expression with explicit parentheses.
58
+ class GroupedExpression < Expression
59
+ # @param expr [Expression]
60
+ def initialize(token, expr)
61
+ super(token)
62
+ @expr = expr
63
+ end
64
+
65
+ def evaluate(context)
66
+ context.evaluate(@expr)
67
+ end
68
+
69
+ def children = [@expr]
70
+ end
71
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../expression"
4
+
5
+ module Liquid2
6
+ # An expression used by the standard `for` and `tablerow` tags.
7
+ class LoopExpression < Expression
8
+ attr_reader :identifier, :enum, :limit, :offset, :reversed, :cols, :name
9
+
10
+ EMPTY_ENUM = [].freeze # steep:ignore
11
+
12
+ def initialize(token, identifier, enum, limit: nil, offset: nil, reversed: false, cols: nil)
13
+ super(token)
14
+ @identifier = identifier
15
+ @enum = enum
16
+ @limit = limit
17
+ @offset = offset
18
+ @reversed = reversed
19
+ @cols = cols
20
+ @name = "#{@identifier.name}-#{@enum}"
21
+ end
22
+
23
+ # @return [Array[untyped]]
24
+ def evaluate(context)
25
+ obj = context.evaluate(@enum)
26
+
27
+ # @type var array: Array[untyped]
28
+ array = if obj.is_a?(Array)
29
+ obj
30
+ elsif obj.is_a?(Hash) || obj.is_a?(Range)
31
+ # TODO: special big range slicing
32
+ obj.to_a
33
+ elsif obj.is_a?(String)
34
+ # TODO: optionally enable/disable string iteration
35
+ obj.each_char.to_a
36
+ elsif obj.respond_to?(:each)
37
+ # TODO: special lazy drop slicing
38
+ # #each and #slice is our enumerable drop interface
39
+ # TODO: or just #to_a
40
+ obj.each.to_a
41
+ else
42
+ EMPTY_ENUM
43
+ end
44
+
45
+ length = array.length
46
+
47
+ # No slicing required
48
+ if @offset.nil? && @limit.nil?
49
+ context.stop_index(@name, index: length)
50
+ return @reversed ? array.reverse : array
51
+ end
52
+
53
+ start = if @offset
54
+ offset = context.evaluate(@offset)
55
+ if offset == "continue"
56
+ context.stop_index(@name)
57
+ else
58
+ Liquid2.to_i(offset)
59
+ end
60
+ else
61
+ 0
62
+ end
63
+
64
+ stop = @limit ? Liquid2.to_i(context.evaluate(@limit)) + start : length
65
+ context.stop_index(@name, index: stop)
66
+
67
+ array = (stop ? array.slice(start...stop) : array.slice(start..)) || EMPTY_ENUM # steep:ignore
68
+ @reversed ? array.reverse! : array
69
+ end
70
+
71
+ def children
72
+ expressions = [@enum]
73
+ expressions << @limit if @limit
74
+ expressions << @offset if @offset
75
+ expressions << @cols if @cols
76
+ expressions
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../expression"
4
+
5
+ module Liquid2
6
+ # A path to some variable data.
7
+ # If the path has just one segment, it is often just called a "variable".
8
+ class Path < Expression
9
+ attr_reader :segments, :head
10
+
11
+ # @param segments [Array[String | Integer | Path]]
12
+ def initialize(token, segments)
13
+ super(token)
14
+ @segments = segments
15
+ @head = @segments.shift
16
+ end
17
+
18
+ def evaluate(context)
19
+ context.fetch(@head, @segments, node: self)
20
+ end
21
+
22
+ # TODO: fix and optimize (store it on the instance)
23
+ def to_s = "#{@head}.#{@segments.map(&:to_s).join(".")}"
24
+
25
+ def children
26
+ if @head.is_a?(Path)
27
+ [@head, *@segments.filter { |segment| segment.is_a?(Path) }]
28
+ else
29
+ @segments.filter { |segment| segment.is_a?(Path) }
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../expression"
4
+
5
+ module Liquid2
6
+ # A range expression.
7
+ class RangeExpression < Expression
8
+ # @param start [Expression]
9
+ # @param stop [Expression]
10
+ def initialize(token, start, stop)
11
+ super(token)
12
+ @start = start
13
+ @stop = stop
14
+ end
15
+
16
+ def evaluate(context)
17
+ from = Liquid2.to_liquid_int(context.evaluate(@start))
18
+ to = Liquid2.to_liquid_int(context.evaluate(@stop))
19
+ (from..to)
20
+ end
21
+
22
+ def to_s
23
+ "(#{@start}..#{@stop})"
24
+ end
25
+
26
+ def children = [@start, @stop]
27
+ end
28
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "blank"
4
+ require_relative "../expression"
5
+
6
+ module Liquid2 # :nodoc:
7
+ # Base for comparison expressions.
8
+ class ComparisonExpression < Expression
9
+ # @param left [Expression]
10
+ # @param right [Expression]
11
+ def initialize(token, left, right)
12
+ super(token)
13
+ @left = left
14
+ @right = right
15
+ end
16
+
17
+ def children = [@left, @right]
18
+
19
+ protected
20
+
21
+ def inner_evaluate(context)
22
+ left = context.evaluate(@left)
23
+ right = context.evaluate(@right)
24
+ left = left.to_liquid(context) if left.respond_to?(:to_liquid)
25
+ right = right.to_liquid(context) if right.respond_to?(:to_liquid)
26
+ [left, right]
27
+ end
28
+ end
29
+
30
+ # Infix ==
31
+ class Eq < ComparisonExpression
32
+ def evaluate(context)
33
+ Liquid2.eq?(*inner_evaluate(context))
34
+ end
35
+ end
36
+
37
+ # Infix != or <>
38
+ class Ne < ComparisonExpression
39
+ def evaluate(context)
40
+ !Liquid2.eq?(*inner_evaluate(context))
41
+ end
42
+ end
43
+
44
+ # Infix <=
45
+ class Le < ComparisonExpression
46
+ def evaluate(context)
47
+ left, right = inner_evaluate(context)
48
+ Liquid2.eq?(left, right) || Liquid2.lt?(left, right)
49
+ end
50
+ end
51
+
52
+ # Infix >=
53
+ class Ge < ComparisonExpression
54
+ def evaluate(context)
55
+ left, right = inner_evaluate(context)
56
+ Liquid2.eq?(left, right) || Liquid2.lt?(right, left)
57
+ end
58
+ end
59
+
60
+ # Infix <
61
+ class Lt < ComparisonExpression
62
+ def evaluate(context)
63
+ Liquid2.lt?(*inner_evaluate(context))
64
+ end
65
+ end
66
+
67
+ # Infix >
68
+ class Gt < ComparisonExpression
69
+ def evaluate(context)
70
+ left, right = inner_evaluate(context)
71
+ Liquid2.lt?(right, left)
72
+ end
73
+ end
74
+
75
+ # Infix `contains`
76
+ class Contains < ComparisonExpression
77
+ def evaluate(context)
78
+ left = context.evaluate(@left)
79
+ right = context.evaluate(@right)
80
+ Liquid2.contains?(left, right)
81
+ end
82
+ end
83
+
84
+ # Infix `in`
85
+ class In < ComparisonExpression
86
+ def evaluate(context)
87
+ left = context.evaluate(@left)
88
+ right = context.evaluate(@right)
89
+ Liquid2.contains?(right, left)
90
+ end
91
+ end
92
+
93
+ # Test _left_ and _right_ for Liquid equality.
94
+ def self.eq?(left, right)
95
+ left, right = right, left if right.is_a?(Empty) || right.is_a?(Blank)
96
+ left == right
97
+ rescue ::ArgumentError => e
98
+ raise Liquid2::LiquidArgumentError, e.message
99
+ end
100
+
101
+ # Return `true` if _left_ is considered less than _right_.
102
+ def self.lt?(left, right)
103
+ left < right
104
+ rescue ::ArgumentError => e
105
+ raise Liquid2::LiquidArgumentError, e.message
106
+ end
107
+
108
+ def self.contains?(left, right)
109
+ return false unless left.respond_to?(:include?)
110
+
111
+ if left.is_a?(String)
112
+ right.nil? || Liquid2.undefined?(right) ? false : left.include?(Liquid2.to_s(right))
113
+ else
114
+ left.include?(right)
115
+ end
116
+ rescue ::ArgumentError => e
117
+ raise Liquid2::LiquidArgumentError, e.message
118
+ end
119
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../expression"
4
+
5
+ module Liquid2
6
+ # Quoted string with interpolated expressions.
7
+ class TemplateString < Expression
8
+ # @param segments [Array<Expression>]
9
+ def initialize(token, segments)
10
+ super(token)
11
+ @segments = segments
12
+ end
13
+
14
+ def evaluate(context)
15
+ @segments.map { |expr| Liquid2.to_s(context.evaluate(expr)) }.join
16
+ end
17
+
18
+ def children = @segments
19
+ end
20
+ end