liquid2 0.3.1 → 0.5.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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +13 -0
- data/README.md +65 -19
- data/docs/composite_literals.md +139 -0
- data/lib/liquid2/context.rb +4 -2
- data/lib/liquid2/environment.rb +32 -4
- data/lib/liquid2/expression.rb +5 -0
- data/lib/liquid2/expressions/array.rb +40 -1
- data/lib/liquid2/expressions/object.rb +54 -0
- data/lib/liquid2/filter.rb +2 -2
- data/lib/liquid2/nodes/tags/cycle.rb +4 -9
- data/lib/liquid2/parser.rb +121 -9
- data/lib/liquid2/scanner.rb +87 -22
- data/lib/liquid2/version.rb +1 -1
- data/sig/liquid2.rbs +74 -5
- data.tar.gz.sig +0 -0
- metadata +3 -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: 2e67530ac2094dea72ed04c893712aef94689789410799cc3c49145a71f07e0d
|
|
4
|
+
data.tar.gz: f979c17c7d0431c338935433080e2a63159c20b6feb109b36b43ccd06844b5e5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0ca528ffffe3a9c272d5e60c2895793fdd9cc0b789633b3b4e06a57e0c8613630cb64882f0d7e9e6a1dfa3a87d26d702635ab52ea988b4afa9bbd085a05ee55f
|
|
7
|
+
data.tar.gz: 36c6a606564ce5da8ddf448948878804e1df279b9e569535c99f72db48ab6024b24be4a006894df5e5163a8f1a403b1785bcc2506629183dce2d452424349779
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
## [0.5.0] - 25-22-10
|
|
2
|
+
|
|
3
|
+
- Improved array literal syntax. Arrays with square brackets are now allowed anywhere a value (literal or variable) is expected. See [#21](https://github.com/jg-rp/ruby-liquid2/issues/21) and `docs/composite_literals.md`.
|
|
4
|
+
- Added object (aka hash) literal syntax. Like arrays, object literals are allowed anywhere a value (literal or variable) is expected. See `docs/composite_literals.md`.
|
|
5
|
+
- Added the spread operator `...`. Like spread syntax in JavaScript, the spread operator is used to expand array elements inside array literals and merge objects (aka hashes) in object literals. See `docs/composite_literals.md`.
|
|
6
|
+
- Improved `{% cycle %}` tag implementation. We no longer evaluate all items for every call to `CycleTag#render`, just the next item in the cycle. We also cache cycle context keys if the key is known at parse time.
|
|
7
|
+
|
|
8
|
+
## [0.4.0] - 25-08-11
|
|
9
|
+
|
|
10
|
+
- Fixed a bug where the parser would raise a `Liquid2::LiquidSyntaxError` if environment arguments `markup_out_end` and `markup_tag_end` where identical. See [#23](https://github.com/jg-rp/ruby-liquid2/issues/23).
|
|
11
|
+
- Added `Liquid2::Environment.persistent_namespaces`. It is an array of symbols indicating which namespaces from `Liquid2::RenderContext.tag_namespaces` should be preserved when calling `Liquid2::RenderContext#copy`. This is important for some tags - like `{% extends %}` - that need to share state with partial templates rendered with `{% render %}`.
|
|
12
|
+
- Added the `auto_trim` argument to `Liquid2::Environment`. `auto_trim` can be `'-'`, `'~'` or `nil` (the default). When not `nil`, it sets the automatic whitespace trimming applied to the left side of template text when no explicit whitespace control is given. `+` is also available as whitespace control in tags, outputs statements and comments. A `+` will ensure no trimming is applied, even if `auto_trim` is set.
|
|
13
|
+
|
|
1
14
|
## [0.3.1] - 25-06-24
|
|
2
15
|
|
|
3
16
|
- Added support for custom markup delimiters. See [#16](https://github.com/jg-rp/ruby-liquid2/pull/16).
|
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.5.0'
|
|
40
40
|
```
|
|
41
41
|
|
|
42
42
|
Or
|
|
@@ -107,31 +107,77 @@ Hello, {{ you | capitalize }}!
|
|
|
107
107
|
|
|
108
108
|
#### Array literals
|
|
109
109
|
|
|
110
|
-
|
|
110
|
+
_CHANGED IN VERSION 0.5.0_
|
|
111
111
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
112
|
+
Array literals are allowed anywhere a value (literal or variable) is expected.
|
|
113
|
+
|
|
114
|
+
```liquid
|
|
115
|
+
{{ [1, 2, 3] }}
|
|
116
|
+
{{ ['a', 'b', 'c'] }}
|
|
117
|
+
|
|
118
|
+
{% assign some_array = [x, y, z] %}
|
|
119
|
+
|
|
120
|
+
{% for x in [1, 2, 3] %}
|
|
121
|
+
- {{ x }}
|
|
116
122
|
{% endfor %}
|
|
117
123
|
```
|
|
118
124
|
|
|
119
|
-
or,
|
|
125
|
+
Square brackets are optional on the left-hand side of a filtered expression or a for loop target, as long as there's at least two items.
|
|
120
126
|
|
|
121
|
-
```
|
|
122
|
-
{
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
127
|
+
```liquid
|
|
128
|
+
{{ 1, 2, 3 | join: '-' }}
|
|
129
|
+
|
|
130
|
+
{% for x in 1, 2, 3 %}
|
|
131
|
+
- {{ x }}
|
|
132
|
+
{% endfor %}
|
|
126
133
|
```
|
|
127
134
|
|
|
128
|
-
|
|
135
|
+
The spread operator `...` allows template authors to compose arrays immutably from existing arrays and enumerables.
|
|
129
136
|
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
137
|
+
```liquid
|
|
138
|
+
{% assign x = [1, 2, 3] %}
|
|
139
|
+
{% assign y = [...x, "a"] %}
|
|
140
|
+
{{ y | json }}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**Output**
|
|
144
|
+
|
|
145
|
+
```json
|
|
146
|
+
[1, 2, 3, "a"]
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
#### Object literals
|
|
150
|
+
|
|
151
|
+
_NEW IN VERSION 0.5.0_
|
|
152
|
+
|
|
153
|
+
Object (aka hash) literals are allowed anywhere a value (literal or variable) is expected. Keys must be strings or static identifiers. We do not support dynamic keys.
|
|
154
|
+
|
|
155
|
+
```liquid
|
|
156
|
+
{% assign point = {x: 10, y: 20} %}
|
|
157
|
+
{{ point.x }}
|
|
158
|
+
|
|
159
|
+
{{ {"foo": "bar", "baz": 42} | json }}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Object and array literals can be nested arbitrarily.
|
|
163
|
+
|
|
164
|
+
```liquid
|
|
165
|
+
{% assign profile = {
|
|
166
|
+
name: "Ada",
|
|
167
|
+
age: 42,
|
|
168
|
+
tags: ["engineer", "mathematician"]
|
|
169
|
+
} %}
|
|
170
|
+
|
|
171
|
+
{{ profile.tags[0] }}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
The spread operator `...` can also be used within object literals to merge key–value pairs from other objects or expressions.
|
|
175
|
+
|
|
176
|
+
```liquid
|
|
177
|
+
{% assign defaults = {a: 1, b: 2} %}
|
|
178
|
+
{% assign overrides = {b: 9, c: 3} %}
|
|
179
|
+
{% assign merged = {...defaults, ...overrides, d: 4} %}
|
|
180
|
+
{{ merged | json }}
|
|
135
181
|
```
|
|
136
182
|
|
|
137
183
|
#### Logical `not`
|
|
@@ -271,7 +317,7 @@ puts template.render("you" => "Liquid") # Hello, Liquid!
|
|
|
271
317
|
|
|
272
318
|
If the _globals_ keyword argument is given, that data will be _pinned_ to the template and will be available as template variables every time you call `Template#render`. Pinned data will be merged with data passed to `Template#render`, with `render` arguments taking priority over pinned data if there's a name conflict.
|
|
273
319
|
|
|
274
|
-
`Liquid2.
|
|
320
|
+
`Liquid2.parse(source)` is a convenience method equivalent to `Liquid2::DEFAULT_ENVIRONMENT.parse(source)` or `Liquid2::Environment.new.parse(source)`.
|
|
275
321
|
|
|
276
322
|
### Configure
|
|
277
323
|
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Composite literals in Liquid2
|
|
2
|
+
|
|
3
|
+
Liquid2, in its default configuration, will never mutate render context data. This behavior ensures that rendering the same template with the same inputs always yields identical results, and that data passed into the template cannot be changed accidentally or maliciously.
|
|
4
|
+
|
|
5
|
+
Some Liquid2 deployments may choose to support controlled data mutation for performance or integration reasons. In those configurations, custom filters and tags may mutate existing data structures. However, the default runtime always treats data as immutable, providing deterministic and side-effect-free rendering suitable for static publishing, caching, and sandboxed execution.
|
|
6
|
+
|
|
7
|
+
As such, there are no built-in `append`, `add`, `prepend` or `removed` filters for mutating arrays, nor filters for adding, removing or setting keys in objects (aka hashes or mappings). Instead, we introduce **array and object literals**, and the spread operator `...`.
|
|
8
|
+
|
|
9
|
+
## Array literals
|
|
10
|
+
|
|
11
|
+
```liquid
|
|
12
|
+
{{ [1, 2, 3] }}
|
|
13
|
+
{{ ['a', 'b', 'c'] }}
|
|
14
|
+
|
|
15
|
+
{% assign some_array = [x, y, z] %}
|
|
16
|
+
|
|
17
|
+
{% for x in [1, 2, 3] %}
|
|
18
|
+
- {{ x }}
|
|
19
|
+
{% endfor %}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Square brackets are optional on the left-hand side of a filtered expression or a for loop target, as long as there's at least two items.
|
|
23
|
+
|
|
24
|
+
```liquid
|
|
25
|
+
{{ 1, 2, 3 | join: '-' }}
|
|
26
|
+
|
|
27
|
+
{% for x in 1, 2, 3 %}
|
|
28
|
+
- {{ x }}
|
|
29
|
+
{% endfor %}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Empty arrays are OK. Single element arrays must include a trailing comma to differentiate them from bracketed variable/path notation.
|
|
33
|
+
|
|
34
|
+
```liquid
|
|
35
|
+
{% assign empty_array = [] %}
|
|
36
|
+
{% assign some_array = [x,] %}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Array literals can be arbitrarily nested.
|
|
40
|
+
|
|
41
|
+
```liquid
|
|
42
|
+
{% liquid
|
|
43
|
+
assign things = [["foo", 1], ["bar", 2]]
|
|
44
|
+
for item in things
|
|
45
|
+
echo "${item[0]}: ${item[1]}\\n"
|
|
46
|
+
endfor
|
|
47
|
+
%}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The standard `concat` filter can accept an array literal as its argument. It always returns a new array.
|
|
51
|
+
|
|
52
|
+
```liquid
|
|
53
|
+
{% assign default_colors = "red", "blue" %}
|
|
54
|
+
{% assign all_colors = default_colors | concat: ["green",] %}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
You can also map to arrays using the `map` filter and an arrow function argument.
|
|
58
|
+
|
|
59
|
+
**Data**
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"pages": [
|
|
64
|
+
{ "dir": "foo", "url": "example.com", "filename": "file1" },
|
|
65
|
+
{ "dir": "bar", "url": "thing.com", "filename": "file2" }
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```liquid
|
|
71
|
+
{% assign downloads = pages | map: p => [p.filename, "${p.url}/${p.dir}"] %}
|
|
72
|
+
{% for item in downloads %}
|
|
73
|
+
{{ item | join: ": " }}
|
|
74
|
+
{% endfor %}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The spread operator `...` allows template authors to compose arrays immutably from existing arrays and enumerables.
|
|
78
|
+
|
|
79
|
+
```liquid
|
|
80
|
+
{% assign x = [1, 2, 3] %}
|
|
81
|
+
{% assign y = [...x, "a"] %}
|
|
82
|
+
{{ y | json }}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Output**
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
[1, 2, 3, "a"]
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Object literals
|
|
92
|
+
|
|
93
|
+
Object literals (also known as hashes or mappings) define key–value pairs enclosed in curly braces `{}`. Keys must be static identifiers or string literals, and braces are always required.
|
|
94
|
+
|
|
95
|
+
```liquid
|
|
96
|
+
{% assign point = {x: 10, y: 20} %}
|
|
97
|
+
{{ point.x }}
|
|
98
|
+
|
|
99
|
+
{{ {"foo": "bar", "baz": 42} | json }}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Object literals can contain values of any type, including arrays, objects, and interpolated strings.
|
|
103
|
+
|
|
104
|
+
```liquid
|
|
105
|
+
{% assign profile = {
|
|
106
|
+
name: "Ada",
|
|
107
|
+
age: 42,
|
|
108
|
+
tags: ["engineer", "mathematician"]
|
|
109
|
+
} %}
|
|
110
|
+
|
|
111
|
+
{{ profile.name }}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Keys may be written as unquoted identifiers or quoted strings. Both forms are equivalent:
|
|
115
|
+
|
|
116
|
+
```liquid
|
|
117
|
+
{% assign a = {foo: 1, "bar": 2} %}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The spread operator `...` can also be used within object literals to merge key–value pairs from other objects or expressions. Each spread is evaluated in order, and later keys override earlier ones. The source objects themselves are never mutated.
|
|
121
|
+
|
|
122
|
+
```liquid
|
|
123
|
+
{% assign defaults = {a: 1, b: 2} %}
|
|
124
|
+
{% assign overrides = {b: 9, c: 3} %}
|
|
125
|
+
{% assign merged = {...defaults, ...overrides, d: 4} %}
|
|
126
|
+
{{ merged | json }}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**Output**
|
|
130
|
+
|
|
131
|
+
```json
|
|
132
|
+
{ "a": 1, "b": 9, "c": 3, "d": 4 }
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Spread values are evaluated as follows:
|
|
136
|
+
|
|
137
|
+
- **Hashes (objects)** are merged directly.
|
|
138
|
+
- **Objects responding to `to_h`** are converted and merged.
|
|
139
|
+
- **Other values** are ignored (treated as empty objects).
|
data/lib/liquid2/context.rb
CHANGED
|
@@ -219,8 +219,10 @@ module Liquid2
|
|
|
219
219
|
loop_carry: loop_carry,
|
|
220
220
|
local_namespace_carry: @assign_score)
|
|
221
221
|
|
|
222
|
-
|
|
223
|
-
|
|
222
|
+
@env.persistent_namespaces.each do |ns|
|
|
223
|
+
context.tag_namespace[ns] = @tag_namespace[ns] if @tag_namespace[ns]
|
|
224
|
+
end
|
|
225
|
+
|
|
224
226
|
context
|
|
225
227
|
end
|
|
226
228
|
|
data/lib/liquid2/environment.rb
CHANGED
|
@@ -51,8 +51,14 @@ module Liquid2
|
|
|
51
51
|
:re_double_quote_string_special, :re_single_quote_string_special, :re_markup_start,
|
|
52
52
|
:re_markup_end, :re_markup_end_chars, :re_up_to_markup_start, :re_punctuation,
|
|
53
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
|
|
55
|
-
|
|
54
|
+
:re_up_to_doc_end, :re_line_statement_comment, :persistent_namespaces,
|
|
55
|
+
:universal_markup_end
|
|
56
|
+
|
|
57
|
+
# @param arithmetic_operators [bool] When `true`, arithmetic operators `+`, `-`, `*`, `/`, `%`
|
|
58
|
+
# and `**` are enabled.
|
|
59
|
+
# @auto_trim ['-' | '~' | nil] Whitespace trimming to apply to the left of text when
|
|
60
|
+
# neither `-` or `~` is given for any tag, output statement or comment. The default is
|
|
61
|
+
# `nil`, which means no automatic whitespace trimming is applied.
|
|
56
62
|
# @param context_depth_limit [Integer] The maximum number of times a render context can
|
|
57
63
|
# be extended or copied before a `Liquid2::LiquidResourceLimitError`` is raised.
|
|
58
64
|
# @param globals [Hash[String, untyped]?] Variables that are available to all templates
|
|
@@ -91,6 +97,7 @@ module Liquid2
|
|
|
91
97
|
def initialize(
|
|
92
98
|
arithmetic_operators: false,
|
|
93
99
|
context_depth_limit: 30,
|
|
100
|
+
auto_trim: nil,
|
|
94
101
|
falsy_undefined: true,
|
|
95
102
|
globals: nil,
|
|
96
103
|
loader: nil,
|
|
@@ -117,6 +124,10 @@ module Liquid2
|
|
|
117
124
|
# keyword argument.
|
|
118
125
|
@filters = {}
|
|
119
126
|
|
|
127
|
+
# An array of symbols indicating which namespaces from RenderContext.tag_namespaces
|
|
128
|
+
# should be preserved when using RenderContext#copy.
|
|
129
|
+
@persistent_namespaces = [:extends]
|
|
130
|
+
|
|
120
131
|
# When `true`, arithmetic operators `+`, `-`, `*`, `/`, `%` and `**` are enabled.
|
|
121
132
|
# Defaults to `false`.
|
|
122
133
|
@arithmetic_operators = arithmetic_operators
|
|
@@ -125,6 +136,11 @@ module Liquid2
|
|
|
125
136
|
# a Liquid2::LiquidResourceLimitError is raised.
|
|
126
137
|
@context_depth_limit = context_depth_limit
|
|
127
138
|
|
|
139
|
+
# The default whitespace trimming applied to the left of text content when neither
|
|
140
|
+
# `-` or `~` is specified. Defaults to `nil`, which means no automatic whitespace
|
|
141
|
+
# trimming.
|
|
142
|
+
@auto_trim = auto_trim
|
|
143
|
+
|
|
128
144
|
# Variables that are available to all templates rendered from this environment.
|
|
129
145
|
@globals = globals
|
|
130
146
|
|
|
@@ -185,6 +201,10 @@ module Liquid2
|
|
|
185
201
|
# The string of characters that indicate the end of a Liquid tag.
|
|
186
202
|
@markup_tag_end = markup_tag_end
|
|
187
203
|
|
|
204
|
+
# Indicates if tag and output end delimiters are identical. This is used by the
|
|
205
|
+
# parser when parsing output statements.
|
|
206
|
+
@universal_markup_end = markup_tag_end == markup_out_end
|
|
207
|
+
|
|
188
208
|
# The string of characters that indicate the start of a Liquid comment. This should
|
|
189
209
|
# include a single trailing `#`. Additional, variable length hashes will be handled
|
|
190
210
|
# by the tokenizer. It is not possible to change comment syntax to not use `#`.
|
|
@@ -392,7 +412,15 @@ module Liquid2
|
|
|
392
412
|
@re_markup_end_chars = /[#{Regexp.escape((@markup_out_end + @markup_tag_end).each_char.uniq.join)}]/
|
|
393
413
|
|
|
394
414
|
@re_up_to_markup_start = /(?=#{Regexp.escape(@markup_out_start)}|#{Regexp.escape(@markup_tag_start)}|#{Regexp.escape(@markup_comment_prefix)})/
|
|
395
|
-
|
|
415
|
+
|
|
416
|
+
# Braces `{` and `}` are handled separately by the scanner, similar to how we handle
|
|
417
|
+
# `"` and `'`, so we don't confuse ending a nested object/hash literal with ending an
|
|
418
|
+
# output statement.
|
|
419
|
+
#
|
|
420
|
+
# In this example we need two `:token_rbrace` before `:token_output_end`.
|
|
421
|
+
# `{{ {"thing": {"foo": 1, "bar": 2}} }}`
|
|
422
|
+
@re_punctuation = %r{(?!#{@re_markup_end})(\?|\[|\]|\|{1,2}|\.{1,3}|,|:|\(|\)|[<>=!]+|[+\-%*/]+(?!#{@re_markup_end_chars}))}
|
|
423
|
+
|
|
396
424
|
@re_up_to_inline_comment_end = /(?=([+\-~])?#{Regexp.escape(@markup_tag_end)})/
|
|
397
425
|
@re_up_to_raw_end = /(?=(#{Regexp.escape(@markup_tag_start)}[+\-~]?\s*endraw\s*[+\-~]?#{Regexp.escape(@markup_tag_end)}))/
|
|
398
426
|
@re_block_comment_chunk = /(#{Regexp.escape(@markup_tag_start)}[+\-~]?\s*(comment|raw|endcomment|endraw)\s*[+\-~]?#{Regexp.escape(@markup_tag_end)})/
|
|
@@ -408,7 +436,7 @@ module Liquid2
|
|
|
408
436
|
|
|
409
437
|
# Trim _text_.
|
|
410
438
|
def trim(text, left_trim, right_trim)
|
|
411
|
-
case left_trim
|
|
439
|
+
case left_trim || @auto_trim
|
|
412
440
|
when "-"
|
|
413
441
|
text.lstrip!
|
|
414
442
|
when "~"
|
data/lib/liquid2/expression.rb
CHANGED
|
@@ -10,6 +10,11 @@ module Liquid2
|
|
|
10
10
|
@token = token
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
+
# @param context RenderContext
|
|
14
|
+
def evaluate(_context)
|
|
15
|
+
raise "all expressions must implement `evaluate: (RenderContext context) -> untyped`"
|
|
16
|
+
end
|
|
17
|
+
|
|
13
18
|
# Return children of this expression.
|
|
14
19
|
def children = []
|
|
15
20
|
|
|
@@ -5,16 +5,55 @@ require_relative "../expression"
|
|
|
5
5
|
module Liquid2
|
|
6
6
|
# An array literal.
|
|
7
7
|
class ArrayLiteral < Expression
|
|
8
|
+
# @param token [[Symbol, String?, Integer]]
|
|
8
9
|
# @param items [Array<Expression>]
|
|
9
10
|
def initialize(token, items)
|
|
10
11
|
super(token)
|
|
11
12
|
@items = items
|
|
12
13
|
end
|
|
13
14
|
|
|
15
|
+
# @param context [RenderContext]
|
|
16
|
+
# @return [untyped]
|
|
14
17
|
def evaluate(context)
|
|
15
|
-
|
|
18
|
+
result = [] # : Array[untyped]
|
|
19
|
+
@items.each do |item|
|
|
20
|
+
value = context.evaluate(item)
|
|
21
|
+
if item.is_a?(ArraySpread)
|
|
22
|
+
case value
|
|
23
|
+
when Array
|
|
24
|
+
result.concat(value)
|
|
25
|
+
when Hash, String
|
|
26
|
+
result << value
|
|
27
|
+
when Enumerable
|
|
28
|
+
result.concat(value.to_a)
|
|
29
|
+
else
|
|
30
|
+
if value.respond_to?(:each)
|
|
31
|
+
result.concat(value.each)
|
|
32
|
+
else
|
|
33
|
+
result << value
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
else
|
|
37
|
+
result << value
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
result
|
|
16
41
|
end
|
|
17
42
|
|
|
18
43
|
def children = @items
|
|
19
44
|
end
|
|
45
|
+
|
|
46
|
+
# Represents a spread element inside an array literal, e.g. ...expr
|
|
47
|
+
class ArraySpread < Expression
|
|
48
|
+
# @param token [[Symbol, String?, Integer]]
|
|
49
|
+
# @param expr [Expression]
|
|
50
|
+
def initialize(token, expr)
|
|
51
|
+
super(token)
|
|
52
|
+
@expr = expr
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def evaluate(context) = context.evaluate(@expr)
|
|
56
|
+
|
|
57
|
+
def children = [@expr]
|
|
58
|
+
end
|
|
20
59
|
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../expression"
|
|
4
|
+
|
|
5
|
+
module Liquid2
|
|
6
|
+
# JavaScript-style object/hash literal
|
|
7
|
+
class ObjectLiteral < Expression
|
|
8
|
+
# @param items [Array<ObjectLiteralItem>]
|
|
9
|
+
def initialize(token, items)
|
|
10
|
+
super(token)
|
|
11
|
+
@items = items
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def evaluate(context)
|
|
15
|
+
result = {} # : Hash[String,untyped]
|
|
16
|
+
@items.each do |item|
|
|
17
|
+
key, value = context.evaluate(item)
|
|
18
|
+
if item.spread
|
|
19
|
+
if value.is_a?(Hash)
|
|
20
|
+
result.merge!(value)
|
|
21
|
+
elsif value.respond_to?(:to_h)
|
|
22
|
+
result.merge!(value.to_h)
|
|
23
|
+
end
|
|
24
|
+
else
|
|
25
|
+
result[key] = value
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
result
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def children = @items
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# A key/value pair belonging to an object literal.
|
|
35
|
+
class ObjectLiteralItem < Expression
|
|
36
|
+
attr_reader :value, :name, :sym, :spread
|
|
37
|
+
|
|
38
|
+
# @param name [String]
|
|
39
|
+
# @param value [Expression]
|
|
40
|
+
def initialize(token, name, value, spread: false)
|
|
41
|
+
super(token)
|
|
42
|
+
@name = name
|
|
43
|
+
@sym = name.to_sym
|
|
44
|
+
@value = value
|
|
45
|
+
@spread = spread
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def evaluate(context)
|
|
49
|
+
[@name, context.evaluate(@value)]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def children = [@value]
|
|
53
|
+
end
|
|
54
|
+
end
|
data/lib/liquid2/filter.rb
CHANGED
|
@@ -15,8 +15,6 @@ module Liquid2
|
|
|
15
15
|
obj.flatten
|
|
16
16
|
when Hash, String
|
|
17
17
|
[obj]
|
|
18
|
-
# when String
|
|
19
|
-
# obj.each_char
|
|
20
18
|
when Enumerable
|
|
21
19
|
obj
|
|
22
20
|
else
|
|
@@ -86,6 +84,8 @@ module Liquid2
|
|
|
86
84
|
end
|
|
87
85
|
|
|
88
86
|
def self.fetch(obj, key, default = nil)
|
|
87
|
+
# TODO: benchmark without rescue
|
|
88
|
+
# TODO: try defensive respond_to?
|
|
89
89
|
obj[key]
|
|
90
90
|
rescue ArgumentError, TypeError, NoMethodError
|
|
91
91
|
default
|
|
@@ -39,19 +39,14 @@ module Liquid2
|
|
|
39
39
|
@items = items
|
|
40
40
|
@named = named
|
|
41
41
|
@blank = false
|
|
42
|
+
# Generate the cycle key here once if we don't have a potentially dynamic name.
|
|
43
|
+
@key = @items.to_s unless @named
|
|
42
44
|
end
|
|
43
45
|
|
|
44
46
|
def render(context, buffer)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
key = if @named
|
|
48
|
-
context.evaluate(@name).to_s
|
|
49
|
-
else
|
|
50
|
-
@items.to_s
|
|
51
|
-
end
|
|
52
|
-
|
|
47
|
+
key = @key || context.evaluate(@name).to_s
|
|
53
48
|
index = context.tag_namespace[:cycles][key]
|
|
54
|
-
buffer << Liquid2.to_output_s(
|
|
49
|
+
buffer << Liquid2.to_output_s(context.evaluate(@items[index]))
|
|
55
50
|
|
|
56
51
|
index += 1
|
|
57
52
|
index = 0 if index >= @items.length
|
data/lib/liquid2/parser.rb
CHANGED
|
@@ -16,6 +16,7 @@ require_relative "expressions/identifier"
|
|
|
16
16
|
require_relative "expressions/lambda"
|
|
17
17
|
require_relative "expressions/logical"
|
|
18
18
|
require_relative "expressions/loop"
|
|
19
|
+
require_relative "expressions/object"
|
|
19
20
|
require_relative "expressions/path"
|
|
20
21
|
require_relative "expressions/range"
|
|
21
22
|
require_relative "expressions/relational"
|
|
@@ -43,6 +44,10 @@ module Liquid2
|
|
|
43
44
|
@pos = 0
|
|
44
45
|
@eof = [:token_eof, nil, length - 1]
|
|
45
46
|
@whitespace_carry = nil
|
|
47
|
+
|
|
48
|
+
# If both tags and output statements share the same end delimiter, we expect
|
|
49
|
+
# `:token_tag_end` to close an output statement as the scanner scans for tags first.
|
|
50
|
+
@output_end = @env.universal_markup_end ? :token_tag_end : :token_output_end
|
|
46
51
|
end
|
|
47
52
|
|
|
48
53
|
# Return the current token without advancing the pointer.
|
|
@@ -233,7 +238,7 @@ module Liquid2
|
|
|
233
238
|
def parse_filtered_expression
|
|
234
239
|
token = current
|
|
235
240
|
left = parse_primary
|
|
236
|
-
left =
|
|
241
|
+
left = parse_implicit_array(left) if current_kind == :token_comma
|
|
237
242
|
filters = parse_filters if current_kind == :token_pipe
|
|
238
243
|
expr = FilteredExpression.new(token, left, filters)
|
|
239
244
|
|
|
@@ -258,7 +263,7 @@ module Liquid2
|
|
|
258
263
|
|
|
259
264
|
if current_kind == :token_comma
|
|
260
265
|
unless LOOP_KEYWORDS.member?(peek[1] || raise)
|
|
261
|
-
enum =
|
|
266
|
+
enum = parse_implicit_array(enum)
|
|
262
267
|
return LoopExpression.new(identifier.token, identifier, enum,
|
|
263
268
|
limit: limit, offset: offset, reversed: reversed, cols: cols)
|
|
264
269
|
end
|
|
@@ -368,8 +373,12 @@ module Liquid2
|
|
|
368
373
|
looks_like_a_path ? parse_path : Empty.new(self.next)
|
|
369
374
|
when :token_single_quote_string, :token_double_quote_string
|
|
370
375
|
parse_string_literal
|
|
371
|
-
when :token_word
|
|
376
|
+
when :token_word
|
|
372
377
|
parse_path
|
|
378
|
+
when :token_lbracket
|
|
379
|
+
parse_array_or_path
|
|
380
|
+
when :token_lbrace
|
|
381
|
+
parse_object_literal
|
|
373
382
|
when :token_lparen
|
|
374
383
|
parse_range_lambda_or_grouped_expression
|
|
375
384
|
when :token_not, :token_plus, :token_minus
|
|
@@ -425,6 +434,7 @@ module Liquid2
|
|
|
425
434
|
end
|
|
426
435
|
|
|
427
436
|
# Parse a string literals or unquoted word.
|
|
437
|
+
# @return [String]
|
|
428
438
|
def parse_name
|
|
429
439
|
case current_kind
|
|
430
440
|
when :token_word
|
|
@@ -691,7 +701,7 @@ module Liquid2
|
|
|
691
701
|
def parse_output
|
|
692
702
|
expr = parse_filtered_expression
|
|
693
703
|
carry_whitespace_control
|
|
694
|
-
eat(
|
|
704
|
+
eat(@output_end)
|
|
695
705
|
Output.new(expr.token, expr)
|
|
696
706
|
end
|
|
697
707
|
|
|
@@ -783,10 +793,69 @@ module Liquid2
|
|
|
783
793
|
end
|
|
784
794
|
end
|
|
785
795
|
|
|
796
|
+
# Parse an array literal delimited with `[` and `]` or a bracketed variable/path.
|
|
797
|
+
# @return [Node]
|
|
798
|
+
def parse_array_or_path
|
|
799
|
+
start_pos = @pos
|
|
800
|
+
token = eat(:token_lbracket)
|
|
801
|
+
|
|
802
|
+
if current_kind == :token_rbracket
|
|
803
|
+
# Empty array
|
|
804
|
+
@pos += 1
|
|
805
|
+
return ArrayLiteral.new(token, [])
|
|
806
|
+
end
|
|
807
|
+
|
|
808
|
+
if current_kind == :token_spread
|
|
809
|
+
# An array with a spread operator before the first item.
|
|
810
|
+
spread_token = self.next
|
|
811
|
+
return parse_partial_array(token, ArraySpread.new(spread_token, parse_primary))
|
|
812
|
+
end
|
|
813
|
+
|
|
814
|
+
first = parse_primary
|
|
815
|
+
|
|
816
|
+
if current_kind == :token_comma
|
|
817
|
+
# An array
|
|
818
|
+
parse_partial_array(token, first)
|
|
819
|
+
else
|
|
820
|
+
# backtrack
|
|
821
|
+
@pos = start_pos
|
|
822
|
+
parse_path
|
|
823
|
+
end
|
|
824
|
+
end
|
|
825
|
+
|
|
826
|
+
# Parse an array where we've already consumed the opening bracket and first item.
|
|
827
|
+
def parse_partial_array(token, first)
|
|
828
|
+
items = [first] # : Array[untyped]
|
|
829
|
+
|
|
830
|
+
loop do
|
|
831
|
+
if current_kind == :token_rbracket
|
|
832
|
+
@pos += 1
|
|
833
|
+
break
|
|
834
|
+
end
|
|
835
|
+
|
|
836
|
+
eat(:token_comma)
|
|
837
|
+
|
|
838
|
+
# Trailing commas are OK.
|
|
839
|
+
if current_kind == :token_rbracket
|
|
840
|
+
@pos += 1
|
|
841
|
+
break
|
|
842
|
+
end
|
|
843
|
+
|
|
844
|
+
if current_kind == :token_spread
|
|
845
|
+
spread_token = self.next
|
|
846
|
+
items << ArraySpread.new(spread_token, parse_primary)
|
|
847
|
+
else
|
|
848
|
+
items << parse_primary
|
|
849
|
+
end
|
|
850
|
+
end
|
|
851
|
+
|
|
852
|
+
ArrayLiteral.new(token, items)
|
|
853
|
+
end
|
|
854
|
+
|
|
786
855
|
# Parse a comma separated list of expressions. Assumes the next token is a comma.
|
|
787
856
|
# @param left [Expression] The first item in the array.
|
|
788
857
|
# @return [ArrayLiteral]
|
|
789
|
-
def
|
|
858
|
+
def parse_implicit_array(left)
|
|
790
859
|
token = current
|
|
791
860
|
items = [left] # : Array[untyped]
|
|
792
861
|
|
|
@@ -803,19 +872,62 @@ module Liquid2
|
|
|
803
872
|
ArrayLiteral.new(left.respond_to?(:token) ? left.token : token, items)
|
|
804
873
|
end
|
|
805
874
|
|
|
875
|
+
def parse_object_literal
|
|
876
|
+
token = eat(:token_lbrace)
|
|
877
|
+
|
|
878
|
+
if current_kind == :token_rbrace
|
|
879
|
+
# Empty object/hash
|
|
880
|
+
@pos += 1
|
|
881
|
+
return ObjectLiteral.new(token, [])
|
|
882
|
+
end
|
|
883
|
+
|
|
884
|
+
items = [parse_object_literal_item] # : Array[ObjectLiteralItem]
|
|
885
|
+
|
|
886
|
+
# Subsequent items must be preceded by a comma.
|
|
887
|
+
loop do
|
|
888
|
+
break if current_kind == :token_rbrace
|
|
889
|
+
|
|
890
|
+
eat(:token_comma, "expected a comma")
|
|
891
|
+
|
|
892
|
+
# Trailing commas are OK.
|
|
893
|
+
break if current_kind == :token_rbrace
|
|
894
|
+
|
|
895
|
+
items << parse_object_literal_item
|
|
896
|
+
end
|
|
897
|
+
|
|
898
|
+
eat(:token_rbrace, "expected a closing brace")
|
|
899
|
+
ObjectLiteral.new(token, items)
|
|
900
|
+
end
|
|
901
|
+
|
|
902
|
+
def parse_object_literal_item
|
|
903
|
+
token = current
|
|
904
|
+
if token[0] == :token_spread
|
|
905
|
+
@pos += 1
|
|
906
|
+
return ObjectLiteralItem.new(token, "", parse_primary, spread: true)
|
|
907
|
+
end
|
|
908
|
+
|
|
909
|
+
key = parse_name # We don't support dynamic keys
|
|
910
|
+
eat(:token_colon)
|
|
911
|
+
value = parse_primary
|
|
912
|
+
ObjectLiteralItem.new(token, key, value)
|
|
913
|
+
end
|
|
914
|
+
|
|
806
915
|
# @return [Node]
|
|
807
916
|
def parse_range_lambda_or_grouped_expression
|
|
808
917
|
token = eat(:token_lparen)
|
|
809
918
|
expr = parse_primary
|
|
810
919
|
|
|
811
|
-
|
|
920
|
+
kind = current_kind
|
|
921
|
+
|
|
922
|
+
if kind == :token_double_dot
|
|
812
923
|
@pos += 1
|
|
813
924
|
stop = parse_primary
|
|
814
|
-
eat(:token_rparen)
|
|
925
|
+
eat(:token_rparen, "expected a closing parenthesis")
|
|
815
926
|
return RangeExpression.new(token, expr, stop)
|
|
816
927
|
end
|
|
817
928
|
|
|
818
|
-
|
|
929
|
+
# Probably a range expression with too many dots.
|
|
930
|
+
raise LiquidSyntaxError.new("too many dots", current) if kind == :token_spread
|
|
819
931
|
|
|
820
932
|
# An arrow function, but we've already consumed lparen and the first parameter.
|
|
821
933
|
return parse_partial_arrow_function(expr) if kind == :token_comma
|
|
@@ -974,7 +1086,7 @@ module Liquid2
|
|
|
974
1086
|
# A single parameter without parens
|
|
975
1087
|
params << parse_identifier
|
|
976
1088
|
when :token_lparen
|
|
977
|
-
# One or
|
|
1089
|
+
# One or more parameters separated by commas and surrounded by parentheses.
|
|
978
1090
|
self.next
|
|
979
1091
|
while current_kind != :token_rparen
|
|
980
1092
|
params << parse_identifier
|
data/lib/liquid2/scanner.rb
CHANGED
|
@@ -38,6 +38,7 @@ module Liquid2
|
|
|
38
38
|
"||" => :token_double_pipe,
|
|
39
39
|
"." => :token_dot,
|
|
40
40
|
".." => :token_double_dot,
|
|
41
|
+
"..." => :token_spread,
|
|
41
42
|
"," => :token_comma,
|
|
42
43
|
":" => :token_colon,
|
|
43
44
|
"(" => :token_lparen,
|
|
@@ -222,6 +223,10 @@ module Liquid2
|
|
|
222
223
|
@start = @scanner.pos
|
|
223
224
|
else
|
|
224
225
|
case @scanner.get_byte
|
|
226
|
+
when "{"
|
|
227
|
+
@tokens << [:token_lbrace, nil, @start]
|
|
228
|
+
@start = @scanner.pos
|
|
229
|
+
scan_object_literal
|
|
225
230
|
when "'"
|
|
226
231
|
@start = @scanner.pos
|
|
227
232
|
scan_string("'", :token_single_quote_string, @re_single_quote_string_special)
|
|
@@ -240,17 +245,19 @@ module Liquid2
|
|
|
240
245
|
|
|
241
246
|
# Miro benchmarks show no performance gain using scan_byte and peek_byte over scan here.
|
|
242
247
|
case @scanner.scan(@re_markup_end)
|
|
243
|
-
when @s_out_end
|
|
244
|
-
@tokens << [:token_output_end, nil, @start]
|
|
245
248
|
when @s_tag_end
|
|
246
249
|
@tokens << [:token_tag_end, nil, @start]
|
|
250
|
+
when @s_out_end
|
|
251
|
+
@tokens << [:token_output_end, nil, @start]
|
|
247
252
|
else
|
|
248
253
|
# Unexpected token
|
|
249
254
|
return nil if @scanner.eos?
|
|
250
255
|
|
|
251
256
|
if (ch = @scanner.scan(@re_markup_end_chars))
|
|
252
|
-
raise LiquidSyntaxError.new(
|
|
253
|
-
|
|
257
|
+
raise LiquidSyntaxError.new(
|
|
258
|
+
"missing markup delimiter or unbalanced object literal detected",
|
|
259
|
+
[:token_unknown, ch, @start]
|
|
260
|
+
)
|
|
254
261
|
end
|
|
255
262
|
|
|
256
263
|
@tokens << [:token_unknown, @scanner.getch, @start]
|
|
@@ -306,10 +313,10 @@ module Liquid2
|
|
|
306
313
|
accept_whitespace_control
|
|
307
314
|
|
|
308
315
|
case @scanner.scan(@re_markup_end)
|
|
309
|
-
when @s_out_end
|
|
310
|
-
@tokens << [:token_output_end, nil, @start]
|
|
311
316
|
when @s_tag_end
|
|
312
317
|
@tokens << [:token_tag_end, nil, @start]
|
|
318
|
+
when @s_out_end
|
|
319
|
+
@tokens << [:token_output_end, nil, @start]
|
|
313
320
|
else
|
|
314
321
|
# Unexpected token
|
|
315
322
|
return nil if @scanner.eos?
|
|
@@ -326,12 +333,12 @@ module Liquid2
|
|
|
326
333
|
accept_whitespace_control
|
|
327
334
|
|
|
328
335
|
case @scanner.scan(@re_markup_end)
|
|
329
|
-
when @s_out_end
|
|
330
|
-
@tokens << [:token_output_end, nil, @start]
|
|
331
|
-
@start = @scanner.pos
|
|
332
336
|
when @s_tag_end
|
|
333
337
|
@tokens << [:token_tag_end, nil, @start]
|
|
334
338
|
@start = @scanner.pos
|
|
339
|
+
when @s_out_end
|
|
340
|
+
@tokens << [:token_output_end, nil, @start]
|
|
341
|
+
@start = @scanner.pos
|
|
335
342
|
end
|
|
336
343
|
|
|
337
344
|
if @scanner.skip_until(@re_up_to_raw_end)
|
|
@@ -347,12 +354,12 @@ module Liquid2
|
|
|
347
354
|
accept_whitespace_control
|
|
348
355
|
|
|
349
356
|
case @scanner.scan(@re_markup_end)
|
|
350
|
-
when @s_out_end
|
|
351
|
-
@tokens << [:token_output_end, nil, @start]
|
|
352
|
-
@start = @scanner.pos
|
|
353
357
|
when @s_tag_end
|
|
354
358
|
@tokens << [:token_tag_end, nil, @start]
|
|
355
359
|
@start = @scanner.pos
|
|
360
|
+
when @s_out_end
|
|
361
|
+
@tokens << [:token_output_end, nil, @start]
|
|
362
|
+
@start = @scanner.pos
|
|
356
363
|
end
|
|
357
364
|
|
|
358
365
|
comment_depth = 1
|
|
@@ -393,12 +400,12 @@ module Liquid2
|
|
|
393
400
|
accept_whitespace_control
|
|
394
401
|
|
|
395
402
|
case @scanner.scan(@re_markup_end)
|
|
396
|
-
when @s_out_end
|
|
397
|
-
@tokens << [:token_output_end, nil, @start]
|
|
398
|
-
@start = @scanner.pos
|
|
399
403
|
when @s_tag_end
|
|
400
404
|
@tokens << [:token_tag_end, nil, @start]
|
|
401
405
|
@start = @scanner.pos
|
|
406
|
+
when @s_out_end
|
|
407
|
+
@tokens << [:token_output_end, nil, @start]
|
|
408
|
+
@start = @scanner.pos
|
|
402
409
|
end
|
|
403
410
|
|
|
404
411
|
if @scanner.skip_until(@re_up_to_doc_end)
|
|
@@ -434,12 +441,12 @@ module Liquid2
|
|
|
434
441
|
else
|
|
435
442
|
accept_whitespace_control
|
|
436
443
|
case @scanner.scan(@re_markup_end)
|
|
437
|
-
when @s_out_end
|
|
438
|
-
@tokens << [:token_output_end, nil, @start]
|
|
439
|
-
@start = @scanner.pos
|
|
440
444
|
when @s_tag_end
|
|
441
445
|
@tokens << [:token_tag_end, nil, @start]
|
|
442
446
|
@start = @scanner.pos
|
|
447
|
+
when @s_out_end
|
|
448
|
+
@tokens << [:token_output_end, nil, @start]
|
|
449
|
+
@start = @scanner.pos
|
|
443
450
|
end
|
|
444
451
|
|
|
445
452
|
:lex_markup
|
|
@@ -450,6 +457,9 @@ module Liquid2
|
|
|
450
457
|
loop do
|
|
451
458
|
skip_line_trivia
|
|
452
459
|
|
|
460
|
+
# XXX: strings can contain line breaks
|
|
461
|
+
# XXX: object literals can contain line breaks
|
|
462
|
+
|
|
453
463
|
case @scanner.get_byte
|
|
454
464
|
when "'"
|
|
455
465
|
@start = @scanner.pos
|
|
@@ -457,6 +467,10 @@ module Liquid2
|
|
|
457
467
|
when "\""
|
|
458
468
|
@start = @scanner.pos
|
|
459
469
|
scan_string("\"", :token_double_quote_string, @re_double_quote_string_special)
|
|
470
|
+
when "{"
|
|
471
|
+
@tokens << [:token_lbrace, nil, @start]
|
|
472
|
+
@start = @scanner.pos
|
|
473
|
+
scan_object_literal
|
|
460
474
|
when nil
|
|
461
475
|
# End of scanner. Unclosed expression or string literal.
|
|
462
476
|
break
|
|
@@ -485,12 +499,12 @@ module Liquid2
|
|
|
485
499
|
@tokens << [:token_tag_end, nil, @start]
|
|
486
500
|
accept_whitespace_control
|
|
487
501
|
case @scanner.scan(@re_markup_end)
|
|
488
|
-
when @s_out_end
|
|
489
|
-
@tokens << [:token_output_end, nil, @start]
|
|
490
|
-
@start = @scanner.pos
|
|
491
502
|
when @s_tag_end
|
|
492
503
|
@tokens << [:token_tag_end, nil, @start]
|
|
493
504
|
@start = @scanner.pos
|
|
505
|
+
when @s_out_end
|
|
506
|
+
@tokens << [:token_output_end, nil, @start]
|
|
507
|
+
@start = @scanner.pos
|
|
494
508
|
end
|
|
495
509
|
|
|
496
510
|
return :lex_markup
|
|
@@ -499,7 +513,7 @@ module Liquid2
|
|
|
499
513
|
end
|
|
500
514
|
end
|
|
501
515
|
|
|
502
|
-
# Scan a string literal surrounded by
|
|
516
|
+
# Scan a string literal surrounded by `quote`.
|
|
503
517
|
# Assumes the opening quote has already been consumed and emitted.
|
|
504
518
|
def scan_string(quote, symbol, pattern)
|
|
505
519
|
start_of_string = @start - 1
|
|
@@ -551,6 +565,10 @@ module Liquid2
|
|
|
551
565
|
@start = @scanner.pos
|
|
552
566
|
scan_string("\"", :token_double_quote_string,
|
|
553
567
|
@re_double_quote_string_special)
|
|
568
|
+
when "{"
|
|
569
|
+
@tokens << [:token_lbrace, nil, @start]
|
|
570
|
+
@start = @scanner.pos
|
|
571
|
+
scan_object_literal
|
|
554
572
|
when "}"
|
|
555
573
|
@tokens << [:token_string_interpol_end, nil, @start]
|
|
556
574
|
@start = @scanner.pos
|
|
@@ -585,5 +603,52 @@ module Liquid2
|
|
|
585
603
|
end
|
|
586
604
|
end
|
|
587
605
|
end
|
|
606
|
+
|
|
607
|
+
# Scan an object/hash literal delimited by `{` and `}`.
|
|
608
|
+
# Assumes the opening `{` has been consumed and emitted.
|
|
609
|
+
def scan_object_literal
|
|
610
|
+
start_of_object = @start - 1
|
|
611
|
+
loop do
|
|
612
|
+
skip_trivia
|
|
613
|
+
if (value = @scanner.scan(@re_float))
|
|
614
|
+
@tokens << [:token_float, value, @start]
|
|
615
|
+
@start = @scanner.pos
|
|
616
|
+
elsif (value = @scanner.scan(@re_int))
|
|
617
|
+
@tokens << [:token_int, value, @start]
|
|
618
|
+
@start = @scanner.pos
|
|
619
|
+
elsif (value = @scanner.scan(@re_punctuation))
|
|
620
|
+
@tokens << [TOKEN_MAP[value] || :token_unknown, value, @start]
|
|
621
|
+
@start = @scanner.pos
|
|
622
|
+
elsif (value = @scanner.scan(@re_word))
|
|
623
|
+
@tokens << [TOKEN_MAP[value] || :token_word, value, @start]
|
|
624
|
+
@start = @scanner.pos
|
|
625
|
+
else
|
|
626
|
+
case @scanner.get_byte
|
|
627
|
+
when "{"
|
|
628
|
+
@tokens << [:token_lbrace, nil, @start]
|
|
629
|
+
@start = @scanner.pos
|
|
630
|
+
scan_object_literal
|
|
631
|
+
when "'"
|
|
632
|
+
@start = @scanner.pos
|
|
633
|
+
scan_string("'", :token_single_quote_string, @re_single_quote_string_special)
|
|
634
|
+
when "\""
|
|
635
|
+
@start = @scanner.pos
|
|
636
|
+
scan_string("\"", :token_double_quote_string,
|
|
637
|
+
@re_double_quote_string_special)
|
|
638
|
+
else
|
|
639
|
+
@scanner.pos -= 1
|
|
640
|
+
break
|
|
641
|
+
end
|
|
642
|
+
end
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
if @scanner.get_byte == "}"
|
|
646
|
+
@tokens << [:token_rbrace, nil, @start]
|
|
647
|
+
@start = @scanner.pos
|
|
648
|
+
else
|
|
649
|
+
raise LiquidSyntaxError.new("unclosed object literal",
|
|
650
|
+
[:token_lbrace, nil, start_of_object])
|
|
651
|
+
end
|
|
652
|
+
end
|
|
588
653
|
end
|
|
589
654
|
end
|
data/lib/liquid2/version.rb
CHANGED
data/sig/liquid2.rbs
CHANGED
|
@@ -62,6 +62,8 @@ module Liquid2
|
|
|
62
62
|
# keyword argument.
|
|
63
63
|
@filters: Hash[String, [_Filter, (Integer | nil)]]
|
|
64
64
|
|
|
65
|
+
@persistent_namespaces: Array[Symbol]
|
|
66
|
+
|
|
65
67
|
@local_namespace_limit: Integer?
|
|
66
68
|
|
|
67
69
|
@context_depth_limit: Integer
|
|
@@ -90,6 +92,8 @@ module Liquid2
|
|
|
90
92
|
|
|
91
93
|
@arithmetic_operators: bool
|
|
92
94
|
|
|
95
|
+
@auto_trim: '-' | '~' | nil
|
|
96
|
+
|
|
93
97
|
# The string of characters that indicate the start of a Liquid output statement.
|
|
94
98
|
@markup_out_start: String
|
|
95
99
|
|
|
@@ -102,6 +106,8 @@ module Liquid2
|
|
|
102
106
|
# The string of characters that indicate the end of a Liquid tag.
|
|
103
107
|
@markup_tag_end: String
|
|
104
108
|
|
|
109
|
+
@universal_markup_end: bool
|
|
110
|
+
|
|
105
111
|
# The string of characters that indicate the start of a Liquid comment. This should
|
|
106
112
|
# include a single trailing `#`. Additional, variable length hashes will be handled
|
|
107
113
|
# by the tokenizer. It is not possible to change comment syntax to not use `#`.
|
|
@@ -154,6 +160,8 @@ module Liquid2
|
|
|
154
160
|
|
|
155
161
|
attr_reader tags: Hash[String, _Tag]
|
|
156
162
|
|
|
163
|
+
attr_reader persistent_namespaces: Array[Symbol]
|
|
164
|
+
|
|
157
165
|
attr_reader local_namespace_limit: Integer?
|
|
158
166
|
|
|
159
167
|
attr_reader context_depth_limit: Integer
|
|
@@ -184,6 +192,8 @@ module Liquid2
|
|
|
184
192
|
|
|
185
193
|
attr_reader markup_tag_start: String
|
|
186
194
|
|
|
195
|
+
attr_reader universal_markup_end: bool
|
|
196
|
+
|
|
187
197
|
attr_reader re_tag_name: Regexp
|
|
188
198
|
|
|
189
199
|
attr_reader re_word: Regexp
|
|
@@ -216,7 +226,7 @@ module Liquid2
|
|
|
216
226
|
|
|
217
227
|
attr_reader re_line_statement_comment: Regexp
|
|
218
228
|
|
|
219
|
-
def initialize: (?arithmetic_operators: bool, ?context_depth_limit: ::Integer, ?falsy_undefined: bool, ?globals: untyped?, ?loader: TemplateLoader?, ?local_namespace_limit: Integer?, ?loop_iteration_limit: Integer?, ?markup_comment_prefix: ::String, ?markup_comment_suffix: ::String, ?markup_out_end: ::String, ?markup_out_start: ::String, ?markup_tag_end: ::String, ?markup_tag_start: ::String, ?output_stream_limit: Integer?, ?parser: singleton(Parser), ?scanner: singleton(Scanner), ?shorthand_indexes: bool, ?suppress_blank_control_flow_blocks: bool, ?undefined: singleton(Undefined)) -> void
|
|
229
|
+
def initialize: (?arithmetic_operators: bool, ?context_depth_limit: ::Integer, ?auto_trim: '-' | '~' | nil, ?falsy_undefined: bool, ?globals: untyped?, ?loader: TemplateLoader?, ?local_namespace_limit: Integer?, ?loop_iteration_limit: Integer?, ?markup_comment_prefix: ::String, ?markup_comment_suffix: ::String, ?markup_out_end: ::String, ?markup_out_start: ::String, ?markup_tag_end: ::String, ?markup_tag_start: ::String, ?output_stream_limit: Integer?, ?parser: singleton(Parser), ?scanner: singleton(Scanner), ?shorthand_indexes: bool, ?suppress_blank_control_flow_blocks: bool, ?undefined: singleton(Undefined)) -> void
|
|
220
230
|
|
|
221
231
|
# @param source [String] template source text.
|
|
222
232
|
# @return [Template]
|
|
@@ -371,6 +381,8 @@ module Liquid2
|
|
|
371
381
|
# Scan a string literal surrounded by _quote_.
|
|
372
382
|
# Assumes the opening quote has already been consumed and emitted.
|
|
373
383
|
def scan_string: (("\"" | "'"), Symbol, Regexp) -> void
|
|
384
|
+
|
|
385
|
+
def scan_object_literal: () -> void
|
|
374
386
|
end
|
|
375
387
|
end
|
|
376
388
|
|
|
@@ -387,6 +399,8 @@ module Liquid2
|
|
|
387
399
|
|
|
388
400
|
@whitespace_carry: String?
|
|
389
401
|
|
|
402
|
+
@output_end: Symbol
|
|
403
|
+
|
|
390
404
|
# Parse Liquid template text into a syntax tree.
|
|
391
405
|
# @param source [String]
|
|
392
406
|
# @return [Array[Node | String]]
|
|
@@ -512,13 +526,13 @@ module Liquid2
|
|
|
512
526
|
|
|
513
527
|
MEMBERSHIP: 6
|
|
514
528
|
|
|
515
|
-
PREFIX: 7
|
|
516
|
-
|
|
517
529
|
ADD_SUB: 8
|
|
518
530
|
|
|
519
531
|
MUL_DIV: 9
|
|
520
532
|
|
|
521
533
|
POW: 10
|
|
534
|
+
|
|
535
|
+
PREFIX: 11
|
|
522
536
|
end
|
|
523
537
|
|
|
524
538
|
PRECEDENCES: Hash[Symbol, Integer]
|
|
@@ -571,7 +585,15 @@ module Liquid2
|
|
|
571
585
|
# @return [Filter]
|
|
572
586
|
def parse_filter: () -> Filter
|
|
573
587
|
|
|
574
|
-
def
|
|
588
|
+
def parse_array_or_path: () -> (ArrayLiteral | Path)
|
|
589
|
+
|
|
590
|
+
def parse_partial_array: ([Symbol, String?, Integer] token, Expression first) -> ArrayLiteral
|
|
591
|
+
|
|
592
|
+
def parse_implicit_array: (untyped left) -> ArrayLiteral
|
|
593
|
+
|
|
594
|
+
def parse_object_literal: () -> ObjectLiteral
|
|
595
|
+
|
|
596
|
+
def parse_object_literal_item: () -> ObjectLiteralItem
|
|
575
597
|
|
|
576
598
|
def parse_arrow_function: () -> untyped
|
|
577
599
|
|
|
@@ -1264,6 +1286,8 @@ module Liquid2
|
|
|
1264
1286
|
|
|
1265
1287
|
# @param token [[Symbol, String?, Integer]]
|
|
1266
1288
|
def initialize: ([Symbol, String?, Integer] token) -> void
|
|
1289
|
+
|
|
1290
|
+
def evaluate: (RenderContext context) -> untyped
|
|
1267
1291
|
|
|
1268
1292
|
def children: () -> Array[untyped]
|
|
1269
1293
|
|
|
@@ -1323,6 +1347,49 @@ module Liquid2
|
|
|
1323
1347
|
|
|
1324
1348
|
def evaluate: (RenderContext context) -> untyped
|
|
1325
1349
|
end
|
|
1350
|
+
|
|
1351
|
+
class ArraySpread < Expression
|
|
1352
|
+
@expr: Expression
|
|
1353
|
+
|
|
1354
|
+
def initialize: ([Symbol, String?, Integer] token, Expression expr) -> void
|
|
1355
|
+
|
|
1356
|
+
def evaluate: (RenderContext context) -> untyped
|
|
1357
|
+
end
|
|
1358
|
+
end
|
|
1359
|
+
|
|
1360
|
+
module Liquid2
|
|
1361
|
+
class ObjectLiteral < Expression
|
|
1362
|
+
@items: Array[ObjectLiteralItem]
|
|
1363
|
+
|
|
1364
|
+
# @param items [Array<Expression>]
|
|
1365
|
+
def initialize: ([Symbol, String?, Integer] token, Array[ObjectLiteralItem] items) -> void
|
|
1366
|
+
|
|
1367
|
+
def evaluate: (RenderContext context) -> untyped
|
|
1368
|
+
end
|
|
1369
|
+
|
|
1370
|
+
class ObjectLiteralItem < Expression
|
|
1371
|
+
@name: String
|
|
1372
|
+
|
|
1373
|
+
@sym: Symbol
|
|
1374
|
+
|
|
1375
|
+
@value: untyped
|
|
1376
|
+
|
|
1377
|
+
@spread: bool
|
|
1378
|
+
|
|
1379
|
+
attr_reader name: String
|
|
1380
|
+
|
|
1381
|
+
attr_reader sym: Symbol
|
|
1382
|
+
|
|
1383
|
+
attr_reader value: untyped
|
|
1384
|
+
|
|
1385
|
+
attr_reader spread: bool
|
|
1386
|
+
|
|
1387
|
+
# @param name [Token]
|
|
1388
|
+
# @param value [Expression]
|
|
1389
|
+
def initialize: ([Symbol, String?, Integer] token, String name, untyped value, ?spread: bool) -> void
|
|
1390
|
+
|
|
1391
|
+
def evaluate: (RenderContext context) -> [String, untyped]
|
|
1392
|
+
end
|
|
1326
1393
|
end
|
|
1327
1394
|
|
|
1328
1395
|
module Liquid2
|
|
@@ -1750,7 +1817,7 @@ module Liquid2
|
|
|
1750
1817
|
# Cast _obj_ to a date and time. Return `nil` if casting fails.
|
|
1751
1818
|
def self.to_date: (untyped obj) -> untyped
|
|
1752
1819
|
|
|
1753
|
-
def self.fetch: (untyped obj, untyped key) -> untyped
|
|
1820
|
+
def self.fetch: (untyped obj, untyped key, ?untyped default) -> untyped
|
|
1754
1821
|
|
|
1755
1822
|
# Return the concatenation of items in _left_ separated by _sep_.
|
|
1756
1823
|
def self.join: (untyped left, ?::String sep) -> untyped
|
|
@@ -2118,6 +2185,8 @@ module Liquid2
|
|
|
2118
2185
|
|
|
2119
2186
|
@blank: bool
|
|
2120
2187
|
|
|
2188
|
+
@key: String | nil
|
|
2189
|
+
|
|
2121
2190
|
# @param parser [Parser]
|
|
2122
2191
|
# @return [CycleTag]
|
|
2123
2192
|
def self.parse: ([Symbol, String?, Integer] token, Parser parser) -> CycleTag
|
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: liquid2
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- James Prior
|
|
@@ -79,6 +79,7 @@ files:
|
|
|
79
79
|
- README.md
|
|
80
80
|
- Rakefile
|
|
81
81
|
- Steepfile
|
|
82
|
+
- docs/composite_literals.md
|
|
82
83
|
- lib/liquid2.rb
|
|
83
84
|
- lib/liquid2/context.rb
|
|
84
85
|
- lib/liquid2/environment.rb
|
|
@@ -94,6 +95,7 @@ files:
|
|
|
94
95
|
- lib/liquid2/expressions/lambda.rb
|
|
95
96
|
- lib/liquid2/expressions/logical.rb
|
|
96
97
|
- lib/liquid2/expressions/loop.rb
|
|
98
|
+
- lib/liquid2/expressions/object.rb
|
|
97
99
|
- lib/liquid2/expressions/path.rb
|
|
98
100
|
- lib/liquid2/expressions/range.rb
|
|
99
101
|
- lib/liquid2/expressions/relational.rb
|
metadata.gz.sig
CHANGED
|
Binary file
|