liquid2 0.2.0 → 0.3.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 396a11c43cb73cebe443927f69b83210d9c1651d9168ad7412726f54470492a7
4
- data.tar.gz: ed127fb805a3862d3bde1db35dd88a713b714af3f65c51ba58d0f85fd09052c3
3
+ metadata.gz: 3a68d0ef0f934b9b4fd68d99591e5b0faf9df0e4d408e35c4df1aa2b7b98f4a1
4
+ data.tar.gz: 41d881fe5f30b1f390e2c8297e36ca08f6eb70c1b70225f8418ba255f6297759
5
5
  SHA512:
6
- metadata.gz: 5c72bdc10c87b94b3863826cd1e964be714de2277c1c4627480fe0a9f869de977ae9efeb55cbaec573a625e43a46a0758312c36e15697d97934b177c968c078f
7
- data.tar.gz: f2c6ec9e5d3d6d0fb86ba2455c0ec8134c8896243c6d8fab859ee3fbbd2c1bb17f2aaf949d591edf96eaf998d5562f7290e1ab9893502764ac9498ddb33aba97
6
+ metadata.gz: 53ad1737b2ae742366a0fc26e038c971d18a4f500ce104faac21a94547ac61a9926a5683a5150539e1b968d144b2cb15aa93823db163cbaa07d785b2e9ed3c31
7
+ data.tar.gz: 25e214ff840aacacb4ffed35160295d8fd7dd04ea301c62aec2a490d4c5d54ba72b73f9a7318b2bc0fb1f8c3ed7eea26a737ba5f8ea182b3a36e44996a8b06f4
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ ## [0.3.1] - 25-06-24
2
+
3
+ - Added support for custom markup delimiters. See [#16](https://github.com/jg-rp/ruby-liquid2/pull/16).
4
+ - Added the `range` filter. `range` is an array slicing filter that takes optional start and end indexes, and an optional step argument, any of which can be negative. See [#18](https://github.com/jg-rp/ruby-liquid2/pull/18).
5
+
6
+ ## [0.3.0] - 25-05-29
7
+
8
+ - Fixed static analysis of lambda expressions (arrow functions). Previously we were not including lambda parameters in the scope of the expression. See [#12](https://github.com/jg-rp/ruby-liquid2/issues/12).
9
+ - Fixed parsing of variable paths that start with `true`, `false`, `nil` or `null`. For example, `{{ true.foo }}`. See [#13](https://github.com/jg-rp/ruby-liquid2/issues/13).
10
+ - Added support for arithmetic infix operators `+`, `-`, `*`, `/`, `%` and `**`, and prefix operators `+` and `-`. These operators are disabled by default. Enable them by passing `arithmetic_operators: true` to a new `Liquid2::Environment`.
11
+
1
12
  ## [0.2.0] - 25-05-12
2
13
 
3
14
  - Fixed error context info when raising `UndefinedError` from `StrictUndefined`.
data/README.md CHANGED
@@ -36,7 +36,7 @@ Liquid templates for Ruby, with some extra features.
36
36
  Add `'liquid2'` to your Gemfile:
37
37
 
38
38
  ```
39
- gem 'liquid2', '~> 0.2.0'
39
+ gem 'liquid2', '~> 0.3.1'
40
40
  ```
41
41
 
42
42
  Or
@@ -219,6 +219,10 @@ Here we use `~` to remove the newline after the opening `for` tag, but preserve
219
219
  </ul>
220
220
  ```
221
221
 
222
+ #### Arithmetic operators
223
+
224
+ Arithmetic infix operators `+`, `-`, `*`, `/`, `%` and `**`, and prefix operators `+` and `-`, are an experimental feature and are disabled by default. Enable them by passing `arithmetic_operators: true` to a new [`Liquid2::Environment`](https://github.com/jg-rp/ruby-liquid2/blob/main/lib/liquid2/environment.rb).
225
+
222
226
  #### Scientific notation
223
227
 
224
228
  Integer and float literals can use scientific notation, like `1.2e3` or `1e-2`.
@@ -227,7 +231,13 @@ Integer and float literals can use scientific notation, like `1.2e3` or `1e-2`.
227
231
 
228
232
  Liquid2 includes implementations of `{% extends %}` and `{% block %}` for template inheritance, `{% with %}` for block scoped variables and `{% macro %}` and `{% call %}` for defining parameterized blocks.
229
233
 
230
- There's also built-in implementations of `sort_numeric` and `json` filters.
234
+ The following filters are included in Liquid2's default environment:
235
+
236
+ - `sort_numeric` - Sorts array elements by runs of digits found in their string representation.
237
+ - `json` - Outputs objects serialized in JSON format.
238
+ - `range`- An alternative to the standard `slice` filter that takes optional start and stop indexes, and an optional step, all of which can be negative.
239
+
240
+ See [Tags and filters](#tags-and-filters) for how to add, remove or alias tags and/or filters from your own Liquid2 environment.
231
241
 
232
242
  ## API
233
243
 
@@ -38,14 +38,20 @@ require_relative "nodes/tags/with"
38
38
  module Liquid2
39
39
  # Template parsing and rendering configuration.
40
40
  #
41
- # A Liquid::Environment is where you might register custom tags and filters,
41
+ # A Liquid2::Environment is where you might register custom tags and filters,
42
42
  # or store global context data that should be available to all templates.
43
43
  #
44
44
  # `Liquid2.parse(source)` is equivalent to `Liquid2::Environment.new.parse(source)`.
45
45
  class Environment
46
46
  attr_reader :tags, :local_namespace_limit, :context_depth_limit, :loop_iteration_limit,
47
47
  :output_stream_limit, :filters, :suppress_blank_control_flow_blocks,
48
- :shorthand_indexes, :falsy_undefined
48
+ :shorthand_indexes, :falsy_undefined, :arithmetic_operators, :markup_comment_prefix,
49
+ :markup_comment_suffix, :markup_out_end, :markup_out_start, :markup_tag_end,
50
+ :markup_tag_start, :re_tag_name, :re_word, :re_int, :re_float,
51
+ :re_double_quote_string_special, :re_single_quote_string_special, :re_markup_start,
52
+ :re_markup_end, :re_markup_end_chars, :re_up_to_markup_start, :re_punctuation,
53
+ :re_up_to_inline_comment_end, :re_up_to_raw_end, :re_block_comment_chunk,
54
+ :re_up_to_doc_end, :re_line_statement_comment
49
55
 
50
56
  # @param context_depth_limit [Integer] The maximum number of times a render context can
51
57
  # be extended or copied before a `Liquid2::LiquidResourceLimitError`` is raised.
@@ -59,8 +65,23 @@ module Liquid2
59
65
  # `Liquid2::LiquidResourceLimitError`` is raised.
60
66
  # @param loop_iteration_limit [Integer?] The maximum number of loop iterations allowed
61
67
  # before a `LiquidResourceLimitError` is raised.
68
+ # @param markup_comment_prefix [String] The string of characters that indicate the start of a
69
+ # Liquid comment. This should include a single trailing `#`. Additional, variable length
70
+ # hashes will be handled by the tokenizer. It is not possible to change comment syntax to not
71
+ # use `#`.
72
+ # @param markup_comment_suffix [String] The string of characters that indicate the end of a
73
+ # Liquid comment, excluding any hashes.
74
+ # @param markup_out_end [String] The string of characters that indicate the end of a Liquid
75
+ # output statement.
76
+ # @param markup_out_start [String] The string of characters that indicate the start of a Liquid
77
+ # output statement.
78
+ # @param markup_tag_end [String] The string of characters that indicate the end of a Liquid tag.
79
+ # @param markup_tag_start [String] The string of characters that indicate the start of a Liquid
80
+ # tag.
62
81
  # @param output_stream_limit [Integer?] The maximum number of bytes that can be written
63
82
  # to a template's output buffer before a `LiquidResourceLimitError` is raised.
83
+ # @param parser [singleton(Parser)] `Liquid2::Parser` or a subclass of it.
84
+ # @param scanner [singleton(Scanner)] `Liquid2::Scanner` or a subclass of it.
64
85
  # @param shorthand_indexes [bool] When `true`, allow shorthand dotted array indexes as
65
86
  # well as bracketed indexes in variable paths. Defaults to `false`.
66
87
  # @param suppress_blank_control_flow_blocks [bool] When `true`, suppress blank control
@@ -68,16 +89,25 @@ module Liquid2
68
89
  # @param undefined [singleton(Liquid2::Undefined)] A singleton returning an instance of
69
90
  # `Liquid2::Undefined`, which is used to represent template variables that don't exist.
70
91
  def initialize(
92
+ arithmetic_operators: false,
71
93
  context_depth_limit: 30,
94
+ falsy_undefined: true,
72
95
  globals: nil,
73
96
  loader: nil,
74
97
  local_namespace_limit: nil,
75
98
  loop_iteration_limit: nil,
99
+ markup_comment_prefix: "{#",
100
+ markup_comment_suffix: "}",
101
+ markup_out_end: "}}",
102
+ markup_out_start: "{{",
103
+ markup_tag_end: "%}",
104
+ markup_tag_start: "{%",
76
105
  output_stream_limit: nil,
106
+ parser: Parser,
107
+ scanner: Scanner,
77
108
  shorthand_indexes: false,
78
109
  suppress_blank_control_flow_blocks: true,
79
- undefined: Undefined,
80
- falsy_undefined: true
110
+ undefined: Undefined
81
111
  )
82
112
  # A mapping of tag names to objects responding to `parse(token, parser)`.
83
113
  @tags = {}
@@ -87,6 +117,10 @@ module Liquid2
87
117
  # keyword argument.
88
118
  @filters = {}
89
119
 
120
+ # When `true`, arithmetic operators `+`, `-`, `*`, `/`, `%` and `**` are enabled.
121
+ # Defaults to `false`.
122
+ @arithmetic_operators = arithmetic_operators
123
+
90
124
  # The maximum number of times a render context can be extended or copied before
91
125
  # a Liquid2::LiquidResourceLimitError is raised.
92
126
  @context_depth_limit = context_depth_limit
@@ -111,9 +145,17 @@ module Liquid2
111
145
  # before a `LiquidResourceLimitError` is raised.
112
146
  @output_stream_limit = output_stream_limit
113
147
 
148
+ # Liquid2::Scanner or a subclass of it. This is used to tokenize Liquid source
149
+ # text before parsing it.
150
+ @scanner = scanner
151
+
152
+ # Liquid2::Parser or a subclass of it. The parser takes tokens from the scanner
153
+ # and produces an abstract syntax tree.
154
+ @parser = parser
155
+
114
156
  # We reuse the same string scanner when parsing templates for improved performance.
115
157
  # TODO: Is this going to cause issues in multi threaded environments?
116
- @scanner = StringScanner.new("")
158
+ @string_scanner = StringScanner.new("")
117
159
 
118
160
  # When `true`, allow shorthand dotted array indexes as well as bracketed indexes
119
161
  # in variable paths. Defaults to `false`.
@@ -131,6 +173,31 @@ module Liquid2
131
173
  # raise an error when tested for truthiness.
132
174
  @falsy_undefined = falsy_undefined
133
175
 
176
+ # The string of characters that indicate the start of a Liquid output statement.
177
+ @markup_out_start = markup_out_start
178
+
179
+ # The string of characters that indicate the end of a Liquid output statement.
180
+ @markup_out_end = markup_out_end
181
+
182
+ # The string of characters that indicate the start of a Liquid tag.
183
+ @markup_tag_start = markup_tag_start
184
+
185
+ # The string of characters that indicate the end of a Liquid tag.
186
+ @markup_tag_end = markup_tag_end
187
+
188
+ # The string of characters that indicate the start of a Liquid comment. This should
189
+ # include a single trailing `#`. Additional, variable length hashes will be handled
190
+ # by the tokenizer. It is not possible to change comment syntax to not use `#`.
191
+ @markup_comment_prefix = markup_comment_prefix
192
+
193
+ # The string of characters that indicate the end of a Liquid comment, excluding any
194
+ # hashes.
195
+ @markup_comment_suffix = markup_comment_suffix
196
+
197
+ # You might need to override `setup_scanner` if you've specified custom markup
198
+ # delimiters and they conflict with standard punctuation.
199
+ setup_scanner
200
+
134
201
  # Override `setup_tags_and_filters` in environment subclasses to configure custom
135
202
  # tags and/or filters.
136
203
  setup_tags_and_filters
@@ -140,11 +207,13 @@ module Liquid2
140
207
  # @param source [String] template source text.
141
208
  # @return [Template]
142
209
  def parse(source, name: "", path: nil, up_to_date: nil, globals: nil, overlay: nil)
143
- Template.new(self,
144
- source,
145
- Parser.parse(self, source, scanner: @scanner),
146
- name: name, path: path, up_to_date: up_to_date,
147
- globals: make_globals(globals), overlay: overlay)
210
+ Template.new(
211
+ self,
212
+ source,
213
+ @parser.new(self, @scanner.tokenize(self, source, @string_scanner), source.length).parse,
214
+ name: name, path: path, up_to_date: up_to_date,
215
+ globals: make_globals(globals), overlay: overlay
216
+ )
148
217
  rescue LiquidError => e
149
218
  e.source = source unless e.source
150
219
  e.template_name = name unless e.template_name || name.empty?
@@ -257,6 +326,7 @@ module Liquid2
257
326
  register_filter("newline_to_br", Liquid2::Filters.method(:newline_to_br))
258
327
  register_filter("plus", Liquid2::Filters.method(:plus))
259
328
  register_filter("prepend", Liquid2::Filters.method(:prepend))
329
+ register_filter("range", Liquid2::Filters.method(:better_slice))
260
330
  register_filter("reject", Liquid2::Filters.method(:reject))
261
331
  register_filter("remove_first", Liquid2::Filters.method(:remove_first))
262
332
  register_filter("remove_last", Liquid2::Filters.method(:remove_last))
@@ -287,6 +357,51 @@ module Liquid2
287
357
  register_filter("where", Liquid2::Filters.method(:where))
288
358
  end
289
359
 
360
+ # Compile regular expressions for use by the tokenizer attached to this environment.
361
+ def setup_scanner
362
+ # A regex pattern matching Liquid tag names. Should include `#` for inline comments.
363
+ @re_tag_name = /(?:[a-z][a-z_0-9]*|#)/
364
+
365
+ # A regex pattern matching keywords and/or variable/path names. Replace this if
366
+ # you want to disable Unicode characters in identifiers, for example.
367
+ @re_word = /[\u0080-\uFFFFa-zA-Z_][\u0080-\uFFFFa-zA-Z0-9_-]*/
368
+
369
+ # Patterns matching literal integers and floats, possibly in scientific notation.
370
+ # You could simplify these to disable scientific notation.
371
+ @re_int = /-?\d+(?:[eE]\+?\d+)?/
372
+ @re_float = /((?:-?\d+\.\d+(?:[eE][+-]?\d+)?)|(-?\d+[eE]-\d+))/
373
+
374
+ # Patterns matching escape sequences, interpolation and end of string in string literals.
375
+ # You could remove `\$` from these to disable string interpolation.
376
+ @re_double_quote_string_special = /[\\"\$]/
377
+ @re_single_quote_string_special = /[\\'\$]/
378
+
379
+ # rubocop: disable Layout/LineLength
380
+
381
+ # A regex pattern matching the start of some Liquid markup. Could be the start of an
382
+ # output statement, tag or comment. Traditionally `{{`, `{%` and `{#`, respectively.
383
+ @re_markup_start = /#{Regexp.escape(@markup_out_start)}|#{Regexp.escape(@markup_tag_start)}|#{Regexp.escape(@markup_comment_prefix)}/
384
+
385
+ # A regex pattern matching the end of some Liquid markup. Could be the end of
386
+ # an output statement or tag. Traditionally `}}`, `%}`, respectively.
387
+ @re_markup_end = /#{Regexp.escape(@markup_out_end)}|#{Regexp.escape(@markup_tag_end)}/
388
+
389
+ # A regex pattern matching any one of the possible characters ending some Liquid
390
+ # markup. This is used to detect incomplete and malformed markup and provide
391
+ # helpful error messages.
392
+ @re_markup_end_chars = /[#{Regexp.escape((@markup_out_end + @markup_tag_end).each_char.uniq.join)}]/
393
+
394
+ @re_up_to_markup_start = /(?=#{Regexp.escape(@markup_out_start)}|#{Regexp.escape(@markup_tag_start)}|#{Regexp.escape(@markup_comment_prefix)})/
395
+ @re_punctuation = %r{(?!#{@re_markup_end})(\?|\[|\]|\|{1,2}|\.{1,2}|,|:|\(|\)|[<>=!]+|[+\-%*/]+(?!#{@re_markup_end_chars}))}
396
+ @re_up_to_inline_comment_end = /(?=([+\-~])?#{Regexp.escape(@markup_tag_end)})/
397
+ @re_up_to_raw_end = /(?=(#{Regexp.escape(@markup_tag_start)}[+\-~]?\s*endraw\s*[+\-~]?#{Regexp.escape(@markup_tag_end)}))/
398
+ @re_block_comment_chunk = /(#{Regexp.escape(@markup_tag_start)}[+\-~]?\s*(comment|raw|endcomment|endraw)\s*[+\-~]?#{Regexp.escape(@markup_tag_end)})/
399
+ @re_up_to_doc_end = /(?=(#{Regexp.escape(@markup_tag_start)}[+\-~]?\s*enddoc\s*[+\-~]?#{Regexp.escape(@markup_tag_end)}))/
400
+ @re_line_statement_comment = /(?=([\r\n]+|-?#{Regexp.escape(@markup_tag_end)}))/
401
+
402
+ # rubocop: enable Layout/LineLength
403
+ end
404
+
290
405
  def undefined(name, node: nil)
291
406
  @undefined.new(name, node: node)
292
407
  end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "blank"
4
+ require_relative "../expression"
5
+
6
+ module Liquid2 # :nodoc:
7
+ # Base class for all arithmetic expressions.
8
+ class ArithmeticExpression < 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
+ [Liquid2::Filters.to_decimal(left), Liquid2::Filters.to_decimal(right)]
27
+ end
28
+ end
29
+
30
+ # Infix addition
31
+ class Plus < ArithmeticExpression
32
+ def evaluate(context)
33
+ left, right = inner_evaluate(context)
34
+ left + right
35
+ end
36
+ end
37
+
38
+ # Infix subtraction
39
+ class Minus < ArithmeticExpression
40
+ def evaluate(context)
41
+ left, right = inner_evaluate(context)
42
+ left - right
43
+ end
44
+ end
45
+
46
+ # Infix multiplication
47
+ class Times < ArithmeticExpression
48
+ def evaluate(context)
49
+ left, right = inner_evaluate(context)
50
+ left * right
51
+ end
52
+ end
53
+
54
+ # Infix division
55
+ class Divide < ArithmeticExpression
56
+ def evaluate(context)
57
+ left, right = inner_evaluate(context)
58
+ left / right
59
+ rescue ZeroDivisionError => e
60
+ raise LiquidTypeError.new(e.message, nil)
61
+ end
62
+ end
63
+
64
+ # Infix modulo
65
+ class Modulo < ArithmeticExpression
66
+ def evaluate(context)
67
+ left, right = inner_evaluate(context)
68
+ left % right
69
+ rescue ZeroDivisionError => e
70
+ raise LiquidTypeError.new(e.message, nil)
71
+ end
72
+ end
73
+
74
+ # Infix exponentiation
75
+ class Pow < ArithmeticExpression
76
+ def evaluate(context)
77
+ left, right = inner_evaluate(context)
78
+ left**right
79
+ end
80
+ end
81
+
82
+ # Prefix negation
83
+ class Negative < Expression
84
+ # @param right [Expression]
85
+ def initialize(token, right)
86
+ super(token)
87
+ @right = right
88
+ end
89
+
90
+ def evaluate(context)
91
+ right = context.evaluate(@right)
92
+ value = Liquid2::Filters.to_decimal(right,
93
+ default: nil) || context.env.undefined(
94
+ "-(#{Liquid2.to_output_string(right)})",
95
+ node: self
96
+ )
97
+ value.send(:-@)
98
+ end
99
+
100
+ def children = [@right]
101
+ end
102
+
103
+ # Prefix positive
104
+ class Positive < Expression
105
+ # @param right [Expression]
106
+ def initialize(token, right)
107
+ super(token)
108
+ @right = right
109
+ end
110
+
111
+ def evaluate(context)
112
+ right = context.evaluate(@right)
113
+ value = Liquid2::Filters.to_decimal(right,
114
+ default: nil) || context.env.undefined(
115
+ "+(#{Liquid2.to_output_string(right)})",
116
+ node: self
117
+ )
118
+ value.send(:+@)
119
+ end
120
+
121
+ def children = [@right]
122
+ end
123
+ end
@@ -19,6 +19,8 @@ module Liquid2
19
19
 
20
20
  def children = [@expr]
21
21
 
22
+ def scope = @params
23
+
22
24
  # Apply this lambda function to elements from _enum_.
23
25
  # @param context [RenderContext]
24
26
  # @param enum [Enumerable<Object>]
@@ -4,7 +4,7 @@ require_relative "blank"
4
4
  require_relative "../expression"
5
5
 
6
6
  module Liquid2 # :nodoc:
7
- # Base for comparison expressions.
7
+ # Base class for all comparison expressions.
8
8
  class ComparisonExpression < Expression
9
9
  # @param left [Expression]
10
10
  # @param right [Expression]
@@ -13,5 +13,45 @@ module Liquid2
13
13
  Liquid2.to_s(left).slice(to_integer(start), to_integer(length)) || ""
14
14
  end
15
15
  end
16
+
17
+ def self.better_slice(
18
+ left,
19
+ start_ = :undefined, stop_ = :undefined, step_ = :undefined,
20
+ start: :undefined, stop: :undefined, step: :undefined
21
+ )
22
+ # Give priority to keyword arguments, default to nil if neither are given.
23
+ start = start_ == :undefined ? nil : start_ if start == :undefined
24
+ stop = stop_ == :undefined ? nil : stop_ if stop == :undefined
25
+ step = step_ == :undefined ? nil : step_ if step == :undefined
26
+
27
+ step = to_integer(step || 1)
28
+ length = left.length
29
+ return [] if length.zero? || step.zero?
30
+
31
+ start = to_integer(start) unless start.nil?
32
+ stop = to_integer(stop) unless stop.nil?
33
+
34
+ normalized_start = if start.nil?
35
+ step.negative? ? length - 1 : 0
36
+ elsif start&.negative?
37
+ [length + start, 0].max
38
+ else
39
+ [start, length - 1].min
40
+ end
41
+
42
+ normalized_stop = if stop.nil?
43
+ step.negative? ? -1 : length
44
+ elsif stop&.negative?
45
+ [length + stop, -1].max
46
+ else
47
+ [stop, length].min
48
+ end
49
+
50
+ # This does not work with Ruby 3.1
51
+ # left[(normalized_start...normalized_stop).step(step)]
52
+ #
53
+ # But this does.
54
+ (normalized_start...normalized_stop).step(step).map { |i| left[i] }
55
+ end
16
56
  end
17
57
  end
@@ -10,7 +10,8 @@ module Liquid2
10
10
  def self.parse(token, parser)
11
11
  parser.carry_whitespace_control
12
12
  parser.eat(:token_tag_end)
13
- # TODO: apply whitespace control to raw text
13
+ # TODO: apply whitespace control to raw text?
14
+ # Shopify/liquid does not apply whitespace control to raw content.
14
15
  raw = parser.eat(:token_raw)
15
16
  parser.eat_empty_tag("endraw")
16
17
  new(token, raw[1] || raise)
@@ -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"
@@ -24,11 +25,12 @@ module Liquid2
24
25
  # Liquid template parser.
25
26
  class Parser
26
27
  # Parse Liquid template text into a syntax tree.
28
+ # @param env [Environment]
27
29
  # @param source [String]
28
30
  # @return [Array[Node | String]]
29
31
  def self.parse(env, source, scanner: nil)
30
32
  new(env,
31
- Liquid2::Scanner.tokenize(source, scanner || StringScanner.new("")),
33
+ Liquid2::Scanner.tokenize(env, source, scanner || StringScanner.new("")),
32
34
  source.length).parse
33
35
  end
34
36
 
@@ -336,14 +338,26 @@ module Liquid2
336
338
 
337
339
  left = case kind
338
340
  when :token_true
339
- self.next
340
- looks_like_a_path ? parse_path : true
341
+ if looks_like_a_path
342
+ parse_path
343
+ else
344
+ self.next
345
+ true
346
+ end
341
347
  when :token_false
342
- self.next
343
- looks_like_a_path ? parse_path : false
348
+ if looks_like_a_path
349
+ parse_path
350
+ else
351
+ self.next
352
+ false
353
+ end
344
354
  when :token_nil
345
- self.next
346
- looks_like_a_path ? parse_path : nil
355
+ if looks_like_a_path
356
+ parse_path
357
+ else
358
+ self.next
359
+ nil
360
+ end
347
361
  when :token_int
348
362
  Liquid2.to_liquid_int(self.next[1])
349
363
  when :token_float
@@ -358,7 +372,7 @@ module Liquid2
358
372
  parse_path
359
373
  when :token_lparen
360
374
  parse_range_lambda_or_grouped_expression
361
- when :token_not
375
+ when :token_not, :token_plus, :token_minus
362
376
  parse_prefix_expression
363
377
  else
364
378
  unless looks_like_a_path && RESERVED_WORDS.include?(kind)
@@ -535,7 +549,10 @@ module Liquid2
535
549
  LOGICAL_AND = 4
536
550
  RELATIONAL = 5
537
551
  MEMBERSHIP = 6
538
- PREFIX = 7
552
+ ADD_SUB = 8
553
+ MUL_DIV = 9
554
+ POW = 10
555
+ PREFIX = 11
539
556
  end
540
557
 
541
558
  PRECEDENCES = {
@@ -551,7 +568,14 @@ module Liquid2
551
568
  token_ne: Precedence::RELATIONAL,
552
569
  token_lg: Precedence::RELATIONAL,
553
570
  token_le: Precedence::RELATIONAL,
554
- token_ge: Precedence::RELATIONAL
571
+ token_ge: Precedence::RELATIONAL,
572
+ token_plus: Precedence::ADD_SUB,
573
+ token_minus: Precedence::ADD_SUB,
574
+ token_times: Precedence::MUL_DIV,
575
+ token_divide: Precedence::MUL_DIV,
576
+ token_floor_div: Precedence::MUL_DIV,
577
+ token_mod: Precedence::MUL_DIV,
578
+ token_pow: Precedence::POW
555
579
  }.freeze
556
580
 
557
581
  BINARY_OPERATORS = Set[
@@ -565,7 +589,14 @@ module Liquid2
565
589
  :token_contains,
566
590
  :token_in,
567
591
  :token_and,
568
- :token_or
592
+ :token_or,
593
+ :token_plus,
594
+ :token_minus,
595
+ :token_times,
596
+ :token_divide,
597
+ :token_floor_div,
598
+ :token_mod,
599
+ :token_pow
569
600
  ]
570
601
 
571
602
  TERMINATE_EXPRESSION = Set[
@@ -794,23 +825,36 @@ module Liquid2
794
825
  return parse_partial_arrow_function(expr)
795
826
  end
796
827
 
797
- unless TERMINATE_GROUPED_EXPRESSION.member?(kind)
798
- unless BINARY_OPERATORS.member?(kind)
799
- raise LiquidSyntaxError.new("expected an infix operator, found #{kind}", current)
800
- end
801
-
802
- expr = parse_infix_expression(expr)
803
- end
804
-
805
- eat(:token_rparen)
806
- GroupedExpression.new(token, expr)
828
+ eat(:token_rparen, "unbalanced parentheses")
829
+ expr
807
830
  end
808
831
 
809
832
  # @return [Node]
810
833
  def parse_prefix_expression
811
- token = eat(:token_not)
812
- expr = parse_primary
813
- LogicalNot.new(token, expr)
834
+ case current_kind
835
+ when :token_not
836
+ token = self.next
837
+ expr = parse_primary(precedence: Precedence::PREFIX)
838
+ LogicalNot.new(token, expr)
839
+ when :token_plus
840
+ token = self.next
841
+ unless @env.arithmetic_operators
842
+ raise LiquidSyntaxError.new("unexpected prefix operator +",
843
+ token)
844
+ end
845
+
846
+ Positive.new(token, parse_primary(precedence: Precedence::PREFIX))
847
+ when :token_minus
848
+ token = self.next
849
+ unless @env.arithmetic_operators
850
+ raise LiquidSyntaxError.new("unexpected prefix operator -",
851
+ token)
852
+ end
853
+
854
+ Negative.new(token, parse_primary(precedence: Precedence::PREFIX))
855
+ else
856
+ raise LiquidSyntaxError.new("unexpected prefix operator #{current[1]}", current)
857
+ end
814
858
  end
815
859
 
816
860
  # @param left [Expression]
@@ -842,7 +886,27 @@ module Liquid2
842
886
  when :token_or
843
887
  LogicalOr.new(op_token, left, right)
844
888
  else
845
- raise LiquidSyntaxError.new("unexpected infix operator, #{op_token[1]}", op_token)
889
+ unless @env.arithmetic_operators
890
+ raise LiquidSyntaxError.new("unexpected infix operator, #{op_token[1]}",
891
+ op_token)
892
+ end
893
+
894
+ case op_token.first
895
+ when :token_plus
896
+ Plus.new(op_token, left, right)
897
+ when :token_minus
898
+ Minus.new(op_token, left, right)
899
+ when :token_times
900
+ Times.new(op_token, left, right)
901
+ when :token_divide
902
+ Divide.new(op_token, left, right)
903
+ when :token_mod
904
+ Modulo.new(op_token, left, right)
905
+ when :token_pow
906
+ Pow.new(op_token, left, right)
907
+ else
908
+ raise LiquidSyntaxError.new("unexpected infix operator, #{op_token[1]}", op_token)
909
+ end
846
910
  end
847
911
  end
848
912