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.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data/.rubocop.yml +46 -0
- data/.ruby-version +1 -0
- data/.vscode/settings.json +32 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/LICENSE_SHOPIFY.txt +20 -0
- data/README.md +219 -0
- data/Rakefile +23 -0
- data/Steepfile +26 -0
- data/lib/liquid2/context.rb +297 -0
- data/lib/liquid2/environment.rb +287 -0
- data/lib/liquid2/errors.rb +79 -0
- data/lib/liquid2/expression.rb +20 -0
- data/lib/liquid2/expressions/arguments.rb +25 -0
- data/lib/liquid2/expressions/array.rb +20 -0
- data/lib/liquid2/expressions/blank.rb +41 -0
- data/lib/liquid2/expressions/boolean.rb +20 -0
- data/lib/liquid2/expressions/filtered.rb +136 -0
- data/lib/liquid2/expressions/identifier.rb +43 -0
- data/lib/liquid2/expressions/lambda.rb +53 -0
- data/lib/liquid2/expressions/logical.rb +71 -0
- data/lib/liquid2/expressions/loop.rb +79 -0
- data/lib/liquid2/expressions/path.rb +33 -0
- data/lib/liquid2/expressions/range.rb +28 -0
- data/lib/liquid2/expressions/relational.rb +119 -0
- data/lib/liquid2/expressions/template_string.rb +20 -0
- data/lib/liquid2/filter.rb +95 -0
- data/lib/liquid2/filters/array.rb +202 -0
- data/lib/liquid2/filters/date.rb +20 -0
- data/lib/liquid2/filters/default.rb +16 -0
- data/lib/liquid2/filters/json.rb +15 -0
- data/lib/liquid2/filters/math.rb +87 -0
- data/lib/liquid2/filters/size.rb +11 -0
- data/lib/liquid2/filters/slice.rb +17 -0
- data/lib/liquid2/filters/sort.rb +96 -0
- data/lib/liquid2/filters/string.rb +204 -0
- data/lib/liquid2/loader.rb +59 -0
- data/lib/liquid2/loaders/file_system_loader.rb +76 -0
- data/lib/liquid2/loaders/mixins.rb +52 -0
- data/lib/liquid2/node.rb +113 -0
- data/lib/liquid2/nodes/comment.rb +18 -0
- data/lib/liquid2/nodes/output.rb +24 -0
- data/lib/liquid2/nodes/tags/assign.rb +35 -0
- data/lib/liquid2/nodes/tags/block_comment.rb +26 -0
- data/lib/liquid2/nodes/tags/capture.rb +40 -0
- data/lib/liquid2/nodes/tags/case.rb +111 -0
- data/lib/liquid2/nodes/tags/cycle.rb +63 -0
- data/lib/liquid2/nodes/tags/decrement.rb +29 -0
- data/lib/liquid2/nodes/tags/doc.rb +24 -0
- data/lib/liquid2/nodes/tags/echo.rb +31 -0
- data/lib/liquid2/nodes/tags/extends.rb +3 -0
- data/lib/liquid2/nodes/tags/for.rb +155 -0
- data/lib/liquid2/nodes/tags/if.rb +84 -0
- data/lib/liquid2/nodes/tags/include.rb +123 -0
- data/lib/liquid2/nodes/tags/increment.rb +29 -0
- data/lib/liquid2/nodes/tags/inline_comment.rb +28 -0
- data/lib/liquid2/nodes/tags/liquid.rb +29 -0
- data/lib/liquid2/nodes/tags/macro.rb +3 -0
- data/lib/liquid2/nodes/tags/raw.rb +30 -0
- data/lib/liquid2/nodes/tags/render.rb +137 -0
- data/lib/liquid2/nodes/tags/tablerow.rb +143 -0
- data/lib/liquid2/nodes/tags/translate.rb +3 -0
- data/lib/liquid2/nodes/tags/unless.rb +23 -0
- data/lib/liquid2/nodes/tags/with.rb +3 -0
- data/lib/liquid2/parser.rb +917 -0
- data/lib/liquid2/scanner.rb +595 -0
- data/lib/liquid2/static_analysis.rb +301 -0
- data/lib/liquid2/tag.rb +22 -0
- data/lib/liquid2/template.rb +182 -0
- data/lib/liquid2/undefined.rb +131 -0
- data/lib/liquid2/utils/cache.rb +80 -0
- data/lib/liquid2/utils/chain_hash.rb +40 -0
- data/lib/liquid2/utils/unescape.rb +119 -0
- data/lib/liquid2/version.rb +5 -0
- data/lib/liquid2.rb +90 -0
- data/performance/benchmark.rb +73 -0
- data/performance/memory_profile.rb +62 -0
- data/performance/profile.rb +71 -0
- data/sig/liquid2.rbs +2348 -0
- data.tar.gz.sig +0 -0
- metadata +164 -0
- 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
|