liquid2 0.4.0 → 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 +7 -0
- data/README.md +65 -19
- data/docs/composite_literals.md +139 -0
- data/lib/liquid2/environment.rb +9 -1
- 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 +116 -8
- data/lib/liquid2/scanner.rb +68 -3
- data/lib/liquid2/version.rb +1 -1
- data/sig/liquid2.rbs +59 -2
- 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,10 @@
|
|
|
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
|
+
|
|
1
8
|
## [0.4.0] - 25-08-11
|
|
2
9
|
|
|
3
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).
|
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/environment.rb
CHANGED
|
@@ -412,7 +412,15 @@ module Liquid2
|
|
|
412
412
|
@re_markup_end_chars = /[#{Regexp.escape((@markup_out_end + @markup_tag_end).each_char.uniq.join)}]/
|
|
413
413
|
|
|
414
414
|
@re_up_to_markup_start = /(?=#{Regexp.escape(@markup_out_start)}|#{Regexp.escape(@markup_tag_start)}|#{Regexp.escape(@markup_comment_prefix)})/
|
|
415
|
-
|
|
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
|
+
|
|
416
424
|
@re_up_to_inline_comment_end = /(?=([+\-~])?#{Regexp.escape(@markup_tag_end)})/
|
|
417
425
|
@re_up_to_raw_end = /(?=(#{Regexp.escape(@markup_tag_start)}[+\-~]?\s*endraw\s*[+\-~]?#{Regexp.escape(@markup_tag_end)}))/
|
|
418
426
|
@re_block_comment_chunk = /(#{Regexp.escape(@markup_tag_start)}[+\-~]?\s*(comment|raw|endcomment|endraw)\s*[+\-~]?#{Regexp.escape(@markup_tag_end)})/
|
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"
|
|
@@ -237,7 +238,7 @@ module Liquid2
|
|
|
237
238
|
def parse_filtered_expression
|
|
238
239
|
token = current
|
|
239
240
|
left = parse_primary
|
|
240
|
-
left =
|
|
241
|
+
left = parse_implicit_array(left) if current_kind == :token_comma
|
|
241
242
|
filters = parse_filters if current_kind == :token_pipe
|
|
242
243
|
expr = FilteredExpression.new(token, left, filters)
|
|
243
244
|
|
|
@@ -262,7 +263,7 @@ module Liquid2
|
|
|
262
263
|
|
|
263
264
|
if current_kind == :token_comma
|
|
264
265
|
unless LOOP_KEYWORDS.member?(peek[1] || raise)
|
|
265
|
-
enum =
|
|
266
|
+
enum = parse_implicit_array(enum)
|
|
266
267
|
return LoopExpression.new(identifier.token, identifier, enum,
|
|
267
268
|
limit: limit, offset: offset, reversed: reversed, cols: cols)
|
|
268
269
|
end
|
|
@@ -372,8 +373,12 @@ module Liquid2
|
|
|
372
373
|
looks_like_a_path ? parse_path : Empty.new(self.next)
|
|
373
374
|
when :token_single_quote_string, :token_double_quote_string
|
|
374
375
|
parse_string_literal
|
|
375
|
-
when :token_word
|
|
376
|
+
when :token_word
|
|
376
377
|
parse_path
|
|
378
|
+
when :token_lbracket
|
|
379
|
+
parse_array_or_path
|
|
380
|
+
when :token_lbrace
|
|
381
|
+
parse_object_literal
|
|
377
382
|
when :token_lparen
|
|
378
383
|
parse_range_lambda_or_grouped_expression
|
|
379
384
|
when :token_not, :token_plus, :token_minus
|
|
@@ -429,6 +434,7 @@ module Liquid2
|
|
|
429
434
|
end
|
|
430
435
|
|
|
431
436
|
# Parse a string literals or unquoted word.
|
|
437
|
+
# @return [String]
|
|
432
438
|
def parse_name
|
|
433
439
|
case current_kind
|
|
434
440
|
when :token_word
|
|
@@ -787,10 +793,69 @@ module Liquid2
|
|
|
787
793
|
end
|
|
788
794
|
end
|
|
789
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
|
+
|
|
790
855
|
# Parse a comma separated list of expressions. Assumes the next token is a comma.
|
|
791
856
|
# @param left [Expression] The first item in the array.
|
|
792
857
|
# @return [ArrayLiteral]
|
|
793
|
-
def
|
|
858
|
+
def parse_implicit_array(left)
|
|
794
859
|
token = current
|
|
795
860
|
items = [left] # : Array[untyped]
|
|
796
861
|
|
|
@@ -807,19 +872,62 @@ module Liquid2
|
|
|
807
872
|
ArrayLiteral.new(left.respond_to?(:token) ? left.token : token, items)
|
|
808
873
|
end
|
|
809
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
|
+
|
|
810
915
|
# @return [Node]
|
|
811
916
|
def parse_range_lambda_or_grouped_expression
|
|
812
917
|
token = eat(:token_lparen)
|
|
813
918
|
expr = parse_primary
|
|
814
919
|
|
|
815
|
-
|
|
920
|
+
kind = current_kind
|
|
921
|
+
|
|
922
|
+
if kind == :token_double_dot
|
|
816
923
|
@pos += 1
|
|
817
924
|
stop = parse_primary
|
|
818
|
-
eat(:token_rparen)
|
|
925
|
+
eat(:token_rparen, "expected a closing parenthesis")
|
|
819
926
|
return RangeExpression.new(token, expr, stop)
|
|
820
927
|
end
|
|
821
928
|
|
|
822
|
-
|
|
929
|
+
# Probably a range expression with too many dots.
|
|
930
|
+
raise LiquidSyntaxError.new("too many dots", current) if kind == :token_spread
|
|
823
931
|
|
|
824
932
|
# An arrow function, but we've already consumed lparen and the first parameter.
|
|
825
933
|
return parse_partial_arrow_function(expr) if kind == :token_comma
|
|
@@ -978,7 +1086,7 @@ module Liquid2
|
|
|
978
1086
|
# A single parameter without parens
|
|
979
1087
|
params << parse_identifier
|
|
980
1088
|
when :token_lparen
|
|
981
|
-
# One or
|
|
1089
|
+
# One or more parameters separated by commas and surrounded by parentheses.
|
|
982
1090
|
self.next
|
|
983
1091
|
while current_kind != :token_rparen
|
|
984
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)
|
|
@@ -249,8 +254,10 @@ module Liquid2
|
|
|
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]
|
|
@@ -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
|
|
@@ -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
|
@@ -381,6 +381,8 @@ module Liquid2
|
|
|
381
381
|
# Scan a string literal surrounded by _quote_.
|
|
382
382
|
# Assumes the opening quote has already been consumed and emitted.
|
|
383
383
|
def scan_string: (("\"" | "'"), Symbol, Regexp) -> void
|
|
384
|
+
|
|
385
|
+
def scan_object_literal: () -> void
|
|
384
386
|
end
|
|
385
387
|
end
|
|
386
388
|
|
|
@@ -583,7 +585,15 @@ module Liquid2
|
|
|
583
585
|
# @return [Filter]
|
|
584
586
|
def parse_filter: () -> Filter
|
|
585
587
|
|
|
586
|
-
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
|
|
587
597
|
|
|
588
598
|
def parse_arrow_function: () -> untyped
|
|
589
599
|
|
|
@@ -1276,6 +1286,8 @@ module Liquid2
|
|
|
1276
1286
|
|
|
1277
1287
|
# @param token [[Symbol, String?, Integer]]
|
|
1278
1288
|
def initialize: ([Symbol, String?, Integer] token) -> void
|
|
1289
|
+
|
|
1290
|
+
def evaluate: (RenderContext context) -> untyped
|
|
1279
1291
|
|
|
1280
1292
|
def children: () -> Array[untyped]
|
|
1281
1293
|
|
|
@@ -1335,6 +1347,49 @@ module Liquid2
|
|
|
1335
1347
|
|
|
1336
1348
|
def evaluate: (RenderContext context) -> untyped
|
|
1337
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
|
|
1338
1393
|
end
|
|
1339
1394
|
|
|
1340
1395
|
module Liquid2
|
|
@@ -1762,7 +1817,7 @@ module Liquid2
|
|
|
1762
1817
|
# Cast _obj_ to a date and time. Return `nil` if casting fails.
|
|
1763
1818
|
def self.to_date: (untyped obj) -> untyped
|
|
1764
1819
|
|
|
1765
|
-
def self.fetch: (untyped obj, untyped key) -> untyped
|
|
1820
|
+
def self.fetch: (untyped obj, untyped key, ?untyped default) -> untyped
|
|
1766
1821
|
|
|
1767
1822
|
# Return the concatenation of items in _left_ separated by _sep_.
|
|
1768
1823
|
def self.join: (untyped left, ?::String sep) -> untyped
|
|
@@ -2130,6 +2185,8 @@ module Liquid2
|
|
|
2130
2185
|
|
|
2131
2186
|
@blank: bool
|
|
2132
2187
|
|
|
2188
|
+
@key: String | nil
|
|
2189
|
+
|
|
2133
2190
|
# @param parser [Parser]
|
|
2134
2191
|
# @return [CycleTag]
|
|
2135
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
|