liquid2 0.1.1 → 0.3.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.
@@ -7,6 +7,7 @@ require_relative "node"
7
7
  require_relative "nodes/comment"
8
8
  require_relative "nodes/output"
9
9
  require_relative "expressions/arguments"
10
+ require_relative "expressions/arithmetic"
10
11
  require_relative "expressions/array"
11
12
  require_relative "expressions/blank"
12
13
  require_relative "expressions/boolean"
@@ -233,7 +234,6 @@ module Liquid2
233
234
  left = parse_primary
234
235
  left = parse_array_literal(left) if current_kind == :token_comma
235
236
  filters = parse_filters if current_kind == :token_pipe
236
- filters ||= [] # : Array[Filter]
237
237
  expr = FilteredExpression.new(token, left, filters)
238
238
 
239
239
  if current_kind == :token_if
@@ -337,14 +337,26 @@ module Liquid2
337
337
 
338
338
  left = case kind
339
339
  when :token_true
340
- self.next
341
- looks_like_a_path ? parse_path : true
340
+ if looks_like_a_path
341
+ parse_path
342
+ else
343
+ self.next
344
+ true
345
+ end
342
346
  when :token_false
343
- self.next
344
- looks_like_a_path ? parse_path : false
347
+ if looks_like_a_path
348
+ parse_path
349
+ else
350
+ self.next
351
+ false
352
+ end
345
353
  when :token_nil
346
- self.next
347
- looks_like_a_path ? parse_path : nil
354
+ if looks_like_a_path
355
+ parse_path
356
+ else
357
+ self.next
358
+ nil
359
+ end
348
360
  when :token_int
349
361
  Liquid2.to_liquid_int(self.next[1])
350
362
  when :token_float
@@ -359,7 +371,7 @@ module Liquid2
359
371
  parse_path
360
372
  when :token_lparen
361
373
  parse_range_lambda_or_grouped_expression
362
- when :token_not
374
+ when :token_not, :token_plus, :token_minus
363
375
  parse_prefix_expression
364
376
  else
365
377
  unless looks_like_a_path && RESERVED_WORDS.include?(kind)
@@ -396,7 +408,7 @@ module Liquid2
396
408
  # @raises [LiquidTypeError].
397
409
  def parse_string
398
410
  node = parse_primary
399
- raise LiquidTypeError, "expected a string" unless node.is_a?(String)
411
+ raise LiquidTypeError, "expected a string literal" unless node.is_a?(String)
400
412
 
401
413
  node
402
414
  end
@@ -411,6 +423,23 @@ module Liquid2
411
423
  Identifier.new(token)
412
424
  end
413
425
 
426
+ # Parse a string literals or unquoted word.
427
+ def parse_name
428
+ case current_kind
429
+ when :token_word
430
+ parse_identifier.name
431
+ when :token_single_quote_string, :token_double_quote_string
432
+ node = parse_string_literal
433
+ unless node.is_a?(String)
434
+ raise LiquidSyntaxError.new("names can't be template strings", node.token)
435
+ end
436
+
437
+ node
438
+ else
439
+ raise LiquidSyntaxError.new("expected a string literal or unquoted word", current)
440
+ end
441
+ end
442
+
414
443
  # Parse comma separated expression.
415
444
  # Leading commas should be consumed by the caller.
416
445
  # @return [Array<Expression>]
@@ -427,7 +456,7 @@ module Liquid2
427
456
  args
428
457
  end
429
458
 
430
- # Parse comma name/value pairs.
459
+ # Parse comma separated name/value pairs.
431
460
  # Leading commas should be consumed by the caller, if allowed.
432
461
  # @return [Array<KeywordArgument>]
433
462
  def parse_keyword_arguments
@@ -438,8 +467,7 @@ module Liquid2
438
467
 
439
468
  word = eat(:token_word)
440
469
  eat_one_of(:token_assign, :token_colon)
441
- val = parse_primary
442
- args << KeywordArgument.new(word, word[1] || raise, val)
470
+ args << KeywordArgument.new(word, word[1] || raise, parse_primary)
443
471
 
444
472
  break unless current_kind == :token_comma
445
473
 
@@ -449,6 +477,68 @@ module Liquid2
449
477
  args
450
478
  end
451
479
 
480
+ # Parse comma separated parameter names with optional default expressions.
481
+ # Leading commas should be consumed by the caller, if allowed.
482
+ # @return [Hash[String, Parameter]]
483
+ def parse_parameters
484
+ args = {} # : Hash[String, Parameter]
485
+
486
+ loop do
487
+ break if TERMINATE_EXPRESSION.member?(current_kind)
488
+
489
+ word = eat(:token_word)
490
+ name = word[1] || raise
491
+
492
+ case current_kind
493
+ when :token_assign, :token_colon
494
+ @pos += 1
495
+ args[name] = Parameter.new(word, name, parse_primary)
496
+ @pos += 1 if current_kind == :token_comma
497
+ when :comma
498
+ args[name] = Parameter.new(word, name, :undefined)
499
+ @pos += 1
500
+ else
501
+ args[name] = Parameter.new(word, name, :undefined)
502
+ break
503
+ end
504
+ end
505
+
506
+ args
507
+ end
508
+
509
+ # Parse mixed positional and keyword arguments.
510
+ # Leading commas should be consumed by the caller, if allowed.
511
+ # @return [[Array[untyped], Array[KeywordArgument]]]
512
+ def parse_arguments
513
+ args = [] # : Array[untyped]
514
+ kwargs = [] # : Array[KeywordArgument]
515
+
516
+ loop do
517
+ break if TERMINATE_EXPRESSION.member?(current_kind)
518
+
519
+ case current_kind
520
+ when :token_word
521
+ if KEYWORD_ARGUMENT_DELIMITERS.include?(peek_kind)
522
+ token = self.next
523
+ @pos += 1 # = or :
524
+ kwargs << KeywordArgument.new(token, token[1] || raise, parse_primary)
525
+ else
526
+ # A positional argument
527
+ args << parse_primary
528
+ end
529
+ else
530
+ # A positional argument
531
+ args << parse_primary
532
+ end
533
+
534
+ break unless current_kind == :token_comma
535
+
536
+ @pos += 1
537
+ end
538
+
539
+ [args, kwargs]
540
+ end
541
+
452
542
  protected
453
543
 
454
544
  class Precedence
@@ -458,7 +548,10 @@ module Liquid2
458
548
  LOGICAL_AND = 4
459
549
  RELATIONAL = 5
460
550
  MEMBERSHIP = 6
461
- PREFIX = 7
551
+ ADD_SUB = 8
552
+ MUL_DIV = 9
553
+ POW = 10
554
+ PREFIX = 11
462
555
  end
463
556
 
464
557
  PRECEDENCES = {
@@ -474,7 +567,14 @@ module Liquid2
474
567
  token_ne: Precedence::RELATIONAL,
475
568
  token_lg: Precedence::RELATIONAL,
476
569
  token_le: Precedence::RELATIONAL,
477
- token_ge: Precedence::RELATIONAL
570
+ token_ge: Precedence::RELATIONAL,
571
+ token_plus: Precedence::ADD_SUB,
572
+ token_minus: Precedence::ADD_SUB,
573
+ token_times: Precedence::MUL_DIV,
574
+ token_divide: Precedence::MUL_DIV,
575
+ token_floor_div: Precedence::MUL_DIV,
576
+ token_mod: Precedence::MUL_DIV,
577
+ token_pow: Precedence::POW
478
578
  }.freeze
479
579
 
480
580
  BINARY_OPERATORS = Set[
@@ -488,7 +588,14 @@ module Liquid2
488
588
  :token_contains,
489
589
  :token_in,
490
590
  :token_and,
491
- :token_or
591
+ :token_or,
592
+ :token_plus,
593
+ :token_minus,
594
+ :token_times,
595
+ :token_divide,
596
+ :token_floor_div,
597
+ :token_mod,
598
+ :token_pow
492
599
  ]
493
600
 
494
601
  TERMINATE_EXPRESSION = Set[
@@ -726,14 +833,35 @@ module Liquid2
726
833
  end
727
834
 
728
835
  eat(:token_rparen)
729
- GroupedExpression.new(token, expr)
836
+ expr
730
837
  end
731
838
 
732
839
  # @return [Node]
733
840
  def parse_prefix_expression
734
- token = eat(:token_not)
735
- expr = parse_primary
736
- LogicalNot.new(token, expr)
841
+ case current_kind
842
+ when :token_not
843
+ token = self.next
844
+ expr = parse_primary(precedence: Precedence::PREFIX)
845
+ LogicalNot.new(token, expr)
846
+ when :token_plus
847
+ token = self.next
848
+ unless @env.arithmetic_operators
849
+ raise LiquidSyntaxError.new("unexpected prefix operator +",
850
+ token)
851
+ end
852
+
853
+ Positive.new(token, parse_primary(precedence: Precedence::PREFIX))
854
+ when :token_minus
855
+ token = self.next
856
+ unless @env.arithmetic_operators
857
+ raise LiquidSyntaxError.new("unexpected prefix operator -",
858
+ token)
859
+ end
860
+
861
+ Negative.new(token, parse_primary(precedence: Precedence::PREFIX))
862
+ else
863
+ raise LiquidSyntaxError.new("unexpected prefix operator #{current[1]}", current)
864
+ end
737
865
  end
738
866
 
739
867
  # @param left [Expression]
@@ -765,7 +893,27 @@ module Liquid2
765
893
  when :token_or
766
894
  LogicalOr.new(op_token, left, right)
767
895
  else
768
- raise LiquidSyntaxError.new("unexpected infix operator, #{op_token[1]}", op_token)
896
+ unless @env.arithmetic_operators
897
+ raise LiquidSyntaxError.new("unexpected infix operator, #{op_token[1]}",
898
+ op_token)
899
+ end
900
+
901
+ case op_token.first
902
+ when :token_plus
903
+ Plus.new(op_token, left, right)
904
+ when :token_minus
905
+ Minus.new(op_token, left, right)
906
+ when :token_times
907
+ Times.new(op_token, left, right)
908
+ when :token_divide
909
+ Divide.new(op_token, left, right)
910
+ when :token_mod
911
+ Modulo.new(op_token, left, right)
912
+ when :token_pow
913
+ Pow.new(op_token, left, right)
914
+ else
915
+ raise LiquidSyntaxError.new("unexpected infix operator, #{op_token[1]}", op_token)
916
+ end
769
917
  end
770
918
  end
771
919
 
@@ -783,7 +931,7 @@ module Liquid2
783
931
 
784
932
  unless current_kind == :token_colon || !TERMINATE_FILTER.member?(current_kind)
785
933
  # No arguments
786
- return Filter.new(name, name[1] || raise, []) # TODO: optimize
934
+ return Filter.new(name, name[1] || raise, nil)
787
935
  end
788
936
 
789
937
  @pos += 1 # token_colon
@@ -804,7 +952,7 @@ module Liquid2
804
952
  args << parse_arrow_function
805
953
  else
806
954
  # A positional argument that is a path.
807
- args << parse_path
955
+ args << parse_primary
808
956
  end
809
957
  when :token_lparen
810
958
  # A grouped expression or range or arrow function
@@ -12,13 +12,11 @@ module Liquid2
12
12
  class Scanner
13
13
  attr_reader :tokens
14
14
 
15
- RE_MARKUP_START = /\{[\{%#]/
16
- RE_WHITESPACE = /[ \n\r\t]+/
17
15
  RE_LINE_SPACE = /[ \t]+/
18
16
  RE_WORD = /[\u0080-\uFFFFa-zA-Z_][\u0080-\uFFFFa-zA-Z0-9_-]*/
19
17
  RE_INT = /-?\d+(?:[eE]\+?\d+)?/
20
18
  RE_FLOAT = /((?:-?\d+\.\d+(?:[eE][+-]?\d+)?)|(-?\d+[eE]-\d+))/
21
- RE_PUNCTUATION = /\?|\[|\]|\|{1,2}|\.{1,2}|,|:|\(|\)|[<>=!]+/
19
+ RE_PUNCTUATION = %r{\?|\[|\]|\|{1,2}|\.{1,2}|,|:|\(|\)|[<>=!]+|[+\-%*/]+(?![\}%])}
22
20
  RE_SINGLE_QUOTE_STRING_SPECIAL = /[\\'\$]/
23
21
  RE_DOUBLE_QUOTE_STRING_SPECIAL = /[\\"\$]/
24
22
 
@@ -60,7 +58,14 @@ module Liquid2
60
58
  ">=" => :token_ge,
61
59
  "==" => :token_eq,
62
60
  "!=" => :token_ne,
63
- "=>" => :token_arrow
61
+ "=>" => :token_arrow,
62
+ "+" => :token_plus,
63
+ "-" => :token_minus,
64
+ "%" => :token_mod,
65
+ "*" => :token_times,
66
+ "/" => :token_divide,
67
+ "//" => :token_floor_div,
68
+ "**" => :token_pow
64
69
  }.freeze
65
70
 
66
71
  def self.tokenize(source, scanner)
@@ -94,31 +99,19 @@ module Liquid2
94
99
  # @param value [String?]
95
100
  # @return void
96
101
  def emit(kind, value)
97
- # TODO: For debugging. Comment this out when benchmarking.
98
- raise "empty span (#{kind}, #{value})" if @scanner.pos == @start
99
-
100
102
  @tokens << [kind, value, @start]
101
103
  @start = @scanner.pos
102
104
  end
103
105
 
104
106
  def skip_trivia
105
- # TODO: For debugging. Comment this out when benchmarking.
106
- raise "must emit before skipping trivia" if @scanner.pos != @start
107
-
108
- @start = @scanner.pos if @scanner.skip(RE_WHITESPACE)
107
+ @start = @scanner.pos if @scanner.skip(/[ \n\r\t]+/)
109
108
  end
110
109
 
111
110
  def skip_line_trivia
112
- # TODO: For debugging. Comment this out when benchmarking.
113
- raise "must emit before skipping line trivia" if @scanner.pos != @start
114
-
115
111
  @start = @scanner.pos if @scanner.skip(RE_LINE_SPACE)
116
112
  end
117
113
 
118
114
  def accept_whitespace_control
119
- # TODO: For debugging. Comment this out when benchmarking.
120
- raise "must emit before accepting whitespace control" if @scanner.pos != @start
121
-
122
115
  ch = @scanner.peek(1)
123
116
 
124
117
  case ch
@@ -133,7 +126,7 @@ module Liquid2
133
126
  end
134
127
 
135
128
  def lex_markup
136
- case @scanner.scan(RE_MARKUP_START)
129
+ case @scanner.scan(/\{[\{%#]/)
137
130
  when "{#"
138
131
  :lex_comment
139
132
  when "{{"
@@ -197,37 +190,30 @@ module Liquid2
197
190
  end
198
191
 
199
192
  def lex_expression
200
- # TODO: For debugging. Comment this out when benchmarking.
201
- raise "must emit before accepting an expression token" if @scanner.pos != @start
202
-
203
193
  loop do
204
194
  skip_trivia
205
-
206
- case @scanner.get_byte
207
- when "'"
195
+ if (value = @scanner.scan(RE_FLOAT))
196
+ @tokens << [:token_float, value, @start]
208
197
  @start = @scanner.pos
209
- scan_string("'", :token_single_quote_string, RE_SINGLE_QUOTE_STRING_SPECIAL)
210
- when "\""
198
+ elsif (value = @scanner.scan(RE_INT))
199
+ @tokens << [:token_int, value, @start]
200
+ @start = @scanner.pos
201
+ elsif (value = @scanner.scan(RE_PUNCTUATION))
202
+ @tokens << [TOKEN_MAP[value] || :token_unknown, value, @start]
203
+ @start = @scanner.pos
204
+ elsif (value = @scanner.scan(RE_WORD))
205
+ @tokens << [TOKEN_MAP[value] || :token_word, value, @start]
211
206
  @start = @scanner.pos
212
- scan_string("\"", :token_double_quote_string, RE_DOUBLE_QUOTE_STRING_SPECIAL)
213
- when nil
214
- # End of scanner. Unclosed expression or string literal.
215
- break
216
207
  else
217
- @scanner.pos -= 1
218
- if (value = @scanner.scan(RE_FLOAT))
219
- @tokens << [:token_float, value, @start]
208
+ case @scanner.get_byte
209
+ when "'"
220
210
  @start = @scanner.pos
221
- elsif (value = @scanner.scan(RE_INT))
222
- @tokens << [:token_int, value, @start]
223
- @start = @scanner.pos
224
- elsif (value = @scanner.scan(RE_PUNCTUATION))
225
- @tokens << [TOKEN_MAP[value] || :token_unknown, value, @start]
226
- @start = @scanner.pos
227
- elsif (value = @scanner.scan(RE_WORD))
228
- @tokens << [TOKEN_MAP[value] || :token_word, value, @start]
211
+ scan_string("'", :token_single_quote_string, RE_SINGLE_QUOTE_STRING_SPECIAL)
212
+ when "\""
229
213
  @start = @scanner.pos
214
+ scan_string("\"", :token_double_quote_string, RE_DOUBLE_QUOTE_STRING_SPECIAL)
230
215
  else
216
+ @scanner.pos -= 1
231
217
  break
232
218
  end
233
219
  end
@@ -413,9 +399,6 @@ module Liquid2
413
399
  end
414
400
 
415
401
  def lex_line_statements
416
- # TODO: For debugging. Comment this out when benchmarking.
417
- raise "must emit before accepting an expression token" if @scanner.pos != @start
418
-
419
402
  skip_trivia # Leading newlines are OK
420
403
 
421
404
  if (tag_name = @scanner.scan(/(?:[a-z][a-z_0-9]*|#)/))
@@ -231,7 +231,7 @@ module Liquid2
231
231
  def self.extract_filters(expression, template_name)
232
232
  filters = [] # : Array[[String, Span]]
233
233
 
234
- if expression.is_a?(Liquid2::FilteredExpression)
234
+ if expression.is_a?(Liquid2::FilteredExpression) && !expression.filters.nil?
235
235
  expression.filters.each do |filter|
236
236
  filters << [filter.name, Span.new(template_name, filter.token.last)]
237
237
  end
@@ -43,7 +43,6 @@ module Liquid2
43
43
  end
44
44
 
45
45
  def render_with_context(context, buffer, partial: false, block_scope: false, namespace: nil)
46
- # TODO: don't extend if namespace is nil
47
46
  context.extend(namespace || {}) do
48
47
  index = 0
49
48
  while (node = @ast[index])
@@ -59,6 +58,8 @@ module Liquid2
59
58
 
60
59
  next unless (interrupt = context.interrupts.pop)
61
60
 
61
+ break if interrupt == :stop_render
62
+
62
63
  if !partial || block_scope
63
64
  raise LiquidSyntaxError.new("unexpected #{interrupt}",
64
65
  node.token) # steep:ignore
@@ -69,15 +70,14 @@ module Liquid2
69
70
  end
70
71
  end
71
72
  rescue LiquidError => e
72
- e.source = @source
73
- e.template_name = @name unless @name.empty?
73
+ e.source = context.template.source unless e.source
74
+ e.template_name = @name unless e.template_name || @name.empty?
74
75
  raise
75
76
  end
76
77
 
77
78
  # Merge template globals with another namespace.
78
79
  def make_globals(namespace)
79
- # TODO: optimize
80
- @globals.merge(@overlay || {}, namespace || {})
80
+ @globals.merge(@overlay, namespace || {})
81
81
  end
82
82
 
83
83
  # Return `false` if this template is stale and needs to be loaded again.
@@ -123,6 +123,53 @@ module Liquid2
123
123
  nodes
124
124
  end
125
125
 
126
+ # Return an array of `{% doc %}` nodes found in this template.
127
+ #
128
+ # Each instance of `Liquid2::DocTag` has a `token` and `text` attribute. Use
129
+ # `Template#docs.map(&:text)` to get an array of doc strings.
130
+ #
131
+ # @return [Array[DocTag]]
132
+ def docs
133
+ context = RenderContext.new(self)
134
+ nodes = [] # : Array[DocTag]
135
+
136
+ # @type var visit: ^(Node) -> void
137
+ visit = lambda do |node|
138
+ nodes << node if node.is_a?(DocTag)
139
+
140
+ node.children(context, include_partials: false).each do |child|
141
+ visit.call(child) if child.is_a?(Node)
142
+ end
143
+ end
144
+
145
+ @ast.each { |node| visit.call(node) if node.is_a?(Node) }
146
+
147
+ nodes
148
+ end
149
+
150
+ # Return arrays of `{% macro %}` and `{% call %}` tags found in this template.
151
+ # @param include_partials [bool]
152
+ # @return [Array[MacroTag], Array[CallTag]]
153
+ def macros(include_partials: false)
154
+ context = RenderContext.new(self)
155
+ macro_nodes = [] # : Array[MacroTag]
156
+ call_nodes = [] # : Array[CallTag]
157
+
158
+ # @type var visit: ^(Node) -> void
159
+ visit = lambda do |node|
160
+ macro_nodes << node if node.is_a?(MacroTag)
161
+ call_nodes << node if node.is_a?(CallTag)
162
+
163
+ node.children(context, include_partials: include_partials).each do |child|
164
+ visit.call(child) if child.is_a?(Node)
165
+ end
166
+ end
167
+
168
+ @ast.each { |node| visit.call(node) if node.is_a?(Node) }
169
+
170
+ [macro_nodes, call_nodes]
171
+ end
172
+
126
173
  # Return an array of variables used in this template, without path segments.
127
174
  # @param include_partials [bool]
128
175
  # @return [Array[String]]
@@ -5,6 +5,8 @@ require_relative "errors"
5
5
  module Liquid2
6
6
  # The default undefined type. Can be iterated over an indexed without error.
7
7
  class Undefined
8
+ include Enumerable
9
+
8
10
  attr_reader :force_default
9
11
 
10
12
  def initialize(name, node: nil)
@@ -26,6 +28,8 @@ module Liquid2
26
28
  def to_s = ""
27
29
  def to_i = 0
28
30
  def to_f = 0.0
31
+ def -@ = self
32
+ def +@ = self
29
33
  def each(...) = Enumerator.new {} # rubocop:disable Lint/EmptyBlock
30
34
  def each_with_index(...) = Enumerator.new {} # rubocop:disable Lint/EmptyBlock
31
35
  def join(...) = ""
@@ -37,87 +41,95 @@ module Liquid2
37
41
  class StrictUndefined < Undefined
38
42
  def initialize(name, node: nil)
39
43
  super
40
- @message = "#{name.inspect} is undefined"
44
+ @message = "#{@node.is_a?(Path) ? @node : name.inspect} is undefined"
41
45
  end
42
46
 
43
47
  def respond_to_missing? = true
44
48
 
45
49
  def method_missing(...)
46
- raise UndefinedError.new(@message, @node)
50
+ raise UndefinedError.new(@message, @node.token)
47
51
  end
48
52
 
49
53
  def [](...)
50
- raise UndefinedError.new(@message, @node)
54
+ raise UndefinedError.new(@message, @node.token)
51
55
  end
52
56
 
53
57
  def key?(...)
54
- raise UndefinedError.new(@message, @node)
58
+ raise UndefinedError.new(@message, @node.token)
55
59
  end
56
60
 
57
61
  def include?(...)
58
- raise UndefinedError.new(@message, @node)
62
+ raise UndefinedError.new(@message, @node.token)
59
63
  end
60
64
 
61
65
  def member?(...)
62
- raise UndefinedError.new(@message, @node)
66
+ raise UndefinedError.new(@message, @node.token)
63
67
  end
64
68
 
65
69
  def fetch(...)
66
- raise UndefinedError.new(@message, @node)
70
+ raise UndefinedError.new(@message, @node.token)
67
71
  end
68
72
 
69
73
  def !
70
- raise UndefinedError.new(@message, @node)
74
+ raise UndefinedError.new(@message, @node.token)
71
75
  end
72
76
 
73
77
  def ==(_other)
74
- raise UndefinedError.new(@message, @node)
78
+ raise UndefinedError.new(@message, @node.token)
75
79
  end
76
80
 
77
81
  def !=(_other)
78
- raise UndefinedError.new(@message, @node)
82
+ raise UndefinedError.new(@message, @node.token)
79
83
  end
80
84
 
81
85
  alias eql? ==
82
86
 
83
87
  def size
84
- raise UndefinedError.new(@message, @node)
88
+ raise UndefinedError.new(@message, @node.token)
85
89
  end
86
90
 
87
91
  def length
88
- raise UndefinedError.new(@message, @node)
92
+ raise UndefinedError.new(@message, @node.token)
89
93
  end
90
94
 
91
95
  def to_s
92
- raise UndefinedError.new(@message, @node)
96
+ raise UndefinedError.new(@message, @node.token)
93
97
  end
94
98
 
95
99
  def to_i
96
- raise UndefinedError.new(@message, @node)
100
+ raise UndefinedError.new(@message, @node.token)
97
101
  end
98
102
 
99
103
  def to_f
100
- raise UndefinedError.new(@message, @node)
104
+ raise UndefinedError.new(@message, @node.token)
105
+ end
106
+
107
+ def +@
108
+ self
109
+ end
110
+
111
+ def -@
112
+ self
101
113
  end
102
114
 
103
115
  def each(...)
104
- raise UndefinedError.new(@message, @node)
116
+ raise UndefinedError.new(@message, @node.token)
105
117
  end
106
118
 
107
119
  def each_with_index(...)
108
- raise UndefinedError.new(@message, @node)
120
+ raise UndefinedError.new(@message, @node.token)
109
121
  end
110
122
 
111
123
  def join(...)
112
- raise UndefinedError.new(@message, @node)
124
+ raise UndefinedError.new(@message, @node.token)
113
125
  end
114
126
 
115
127
  def to_liquid(_context)
116
- raise UndefinedError.new(@message, @node)
128
+ self
117
129
  end
118
130
 
119
131
  def poke
120
- raise UndefinedError.new(@message, @node)
132
+ raise UndefinedError.new(@message, @node.token)
121
133
  end
122
134
  end
123
135
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Liquid2
4
- VERSION = "0.1.1"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/liquid2.rb CHANGED
@@ -73,6 +73,8 @@ module Liquid2
73
73
  # @param obj [Object]
74
74
  # @return [bool]
75
75
  def self.truthy?(context, obj)
76
+ return false if context.env.falsy_undefined && undefined?(obj)
77
+
76
78
  obj = obj.to_liquid(context) if obj.respond_to?(:to_liquid)
77
79
  !!obj
78
80
  end