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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +11 -0
- data/README.md +12 -2
- data/lib/liquid2/environment.rb +125 -10
- data/lib/liquid2/expressions/arithmetic.rb +123 -0
- data/lib/liquid2/expressions/lambda.rb +2 -0
- data/lib/liquid2/expressions/relational.rb +1 -1
- data/lib/liquid2/filters/slice.rb +40 -0
- data/lib/liquid2/nodes/tags/raw.rb +2 -1
- data/lib/liquid2/parser.rb +89 -25
- data/lib/liquid2/scanner.rb +98 -80
- data/lib/liquid2/undefined.rb +11 -1
- data/lib/liquid2/version.rb +1 -1
- data/performance/benchmark.rb +0 -6
- data/sig/liquid2.rbs +249 -28
- data.tar.gz.sig +0 -0
- metadata +2 -1
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3a68d0ef0f934b9b4fd68d99591e5b0faf9df0e4d408e35c4df1aa2b7b98f4a1
|
4
|
+
data.tar.gz: 41d881fe5f30b1f390e2c8297e36ca08f6eb70c1b70225f8418ba255f6297759
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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
|
-
|
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
|
|
data/lib/liquid2/environment.rb
CHANGED
@@ -38,14 +38,20 @@ require_relative "nodes/tags/with"
|
|
38
38
|
module Liquid2
|
39
39
|
# Template parsing and rendering configuration.
|
40
40
|
#
|
41
|
-
# A
|
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
|
-
@
|
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(
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
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
|
@@ -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)
|
data/lib/liquid2/parser.rb
CHANGED
@@ -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
|
-
|
340
|
-
|
341
|
+
if looks_like_a_path
|
342
|
+
parse_path
|
343
|
+
else
|
344
|
+
self.next
|
345
|
+
true
|
346
|
+
end
|
341
347
|
when :token_false
|
342
|
-
|
343
|
-
|
348
|
+
if looks_like_a_path
|
349
|
+
parse_path
|
350
|
+
else
|
351
|
+
self.next
|
352
|
+
false
|
353
|
+
end
|
344
354
|
when :token_nil
|
345
|
-
|
346
|
-
|
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
|
-
|
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
|
-
|
798
|
-
|
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
|
-
|
812
|
-
|
813
|
-
|
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
|
-
|
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
|
|