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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3a68d0ef0f934b9b4fd68d99591e5b0faf9df0e4d408e35c4df1aa2b7b98f4a1
4
- data.tar.gz: 41d881fe5f30b1f390e2c8297e36ca08f6eb70c1b70225f8418ba255f6297759
3
+ metadata.gz: 2e67530ac2094dea72ed04c893712aef94689789410799cc3c49145a71f07e0d
4
+ data.tar.gz: f979c17c7d0431c338935433080e2a63159c20b6feb109b36b43ccd06844b5e5
5
5
  SHA512:
6
- metadata.gz: 53ad1737b2ae742366a0fc26e038c971d18a4f500ce104faac21a94547ac61a9926a5683a5150539e1b968d144b2cb15aa93823db163cbaa07d785b2e9ed3c31
7
- data.tar.gz: 25e214ff840aacacb4ffed35160295d8fd7dd04ea301c62aec2a490d4c5d54ba72b73f9a7318b2bc0fb1f8c3ed7eea26a737ba5f8ea182b3a36e44996a8b06f4
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.3.1'
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
- Filtered expressions (those found in output statements, the `assign` tag and the `echo` tag) and `for` tag expressions support array literal syntax. We don't use the traditional `[item1, item2, ...]` syntax with square brackets because square brackets are already used for variables (`["some variable with spaces"]` is a valid variable).
110
+ _CHANGED IN VERSION 0.5.0_
111
111
 
112
- ```liquid2
113
- {% assign my_array = a, b, '42', false -%}
114
- {% for item in my_array -%}
115
- - {{ item }}
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, using a `{% liquid %}` tag:
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
- ```liquid2
122
- {% liquid
123
- for item in a, b, '42', false
124
- echo "- ${item}\n"
125
- endfor %}
127
+ ```liquid
128
+ {{ 1, 2, 3 | join: '-' }}
129
+
130
+ {% for x in 1, 2, 3 %}
131
+ - {{ x }}
132
+ {% endfor %}
126
133
  ```
127
134
 
128
- With `a` set to `"Hello"` and `b` set to `"World"`, both of the examples above produce the following output.
135
+ The spread operator `...` allows template authors to compose arrays immutably from existing arrays and enumerables.
129
136
 
130
- ```plain title="output"
131
- - Hello
132
- - World
133
- - 42
134
- - false
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.render(source)` is a convenience method equivalent to `Liquid2::DEFAULT_ENVIRONMENT.parse(source)` or `Liquid2::Environment.new.parse(source)`.
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).
@@ -219,8 +219,10 @@ module Liquid2
219
219
  loop_carry: loop_carry,
220
220
  local_namespace_carry: @assign_score)
221
221
 
222
- # XXX: bit of a hack
223
- context.tag_namespace[:extends] = @tag_namespace[:extends]
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
 
@@ -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
- @re_punctuation = %r{(?!#{@re_markup_end})(\?|\[|\]|\|{1,2}|\.{1,2}|,|:|\(|\)|[<>=!]+|[+\-%*/]+(?!#{@re_markup_end_chars}))}
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 "~"
@@ -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
- @items.map { |item| context.evaluate(item) }
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
@@ -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
- args = @items.map { |expr| context.evaluate(expr) }
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(args[index])
49
+ buffer << Liquid2.to_output_s(context.evaluate(@items[index]))
55
50
 
56
51
  index += 1
57
52
  index = 0 if index >= @items.length
@@ -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 = parse_array_literal(left) if current_kind == :token_comma
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 = parse_array_literal(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, :token_lbracket
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(:token_output_end)
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 parse_array_literal(left)
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
- if current_kind == :token_double_dot
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
- kind = current_kind
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 move parameters separated by commas and surrounded by parentheses.
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
@@ -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("missing markup delimiter detected",
253
- [:token_unknown, ch, @start])
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 single quotes.
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Liquid2
4
- VERSION = "0.3.1"
4
+ VERSION = "0.5.0"
5
5
  end
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 parse_array_literal: (untyped left) -> ArrayLiteral
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.3.1
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