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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 474a8dc5c84a97344741bc74d459cc904f1da7025af450c834fb8d416eb98518
4
- data.tar.gz: 2dda5436fd261f0208123baa7f7f917f2406531e0bf295db085528a8fa7a5baa
3
+ metadata.gz: 2e67530ac2094dea72ed04c893712aef94689789410799cc3c49145a71f07e0d
4
+ data.tar.gz: f979c17c7d0431c338935433080e2a63159c20b6feb109b36b43ccd06844b5e5
5
5
  SHA512:
6
- metadata.gz: 489dd5089b45f07779de0ee25ee4a8fa5b028a8721bf1423a6ca6884ebde6647d36e9e9450e650c2816c1a63a5b6f1554d8e2d8fa7712c106792a3dbeefe0205
7
- data.tar.gz: 7b5e904527625b34d0e177168abbd00198f393b303b9504ee68a37cda3a2f1ef7a407333b15dec96907271610ca715f0e08f49c695466f8f7ddff22a1056b6d4
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.4.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
- 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).
@@ -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
- @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
+
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)})/
@@ -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"
@@ -237,7 +238,7 @@ module Liquid2
237
238
  def parse_filtered_expression
238
239
  token = current
239
240
  left = parse_primary
240
- left = parse_array_literal(left) if current_kind == :token_comma
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 = parse_array_literal(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, :token_lbracket
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 parse_array_literal(left)
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
- if current_kind == :token_double_dot
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
- kind = current_kind
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 move parameters separated by commas and surrounded by parentheses.
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
@@ -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("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]
@@ -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 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.4.0"
4
+ VERSION = "0.5.0"
5
5
  end
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 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
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.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