liquid2 0.1.1 → 0.3.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: 856722c8ce4f6b647fff6e6c5ce61661845f6f8a51f51a4fe6be13ac21fe0287
4
- data.tar.gz: 8da99a1c9e659519eac46600e1e9609cd6e57dc9ac88ad8589b564fbf7861eb5
3
+ metadata.gz: 42973b8aae08cf4321586ac4cdaf39ecab29e0a2a4b7f26aed278291e41b7645
4
+ data.tar.gz: 9c2bca82b7f589cfdccd4a2a5c091ac3cd32613e0c40354b6313d23a6c35bbec
5
5
  SHA512:
6
- metadata.gz: 40a9180a57d81c6461c056de6bf38e10ab6a95bb177acb59ad43aa0a92a80f44c3070c490afc67f30a12035427098fd78d26915b4336e587a1cd25188d49223a
7
- data.tar.gz: e2a572fde0fffb372f37fbe8ad02bb49acaf6d55e2c449f6c3502b49b07996843c4396bbde08135629195d39e10b305e8848b1f17aec75ad0690951dba41f8e9
6
+ metadata.gz: fbb3917b6b68ba37aaffbf158aac61d85a577ddf244b12fdad9eaa99e5ea54a0f94b255b3d21381514bb42bbb7c3543c247b7bba38255e95dacc195aedf2f10a
7
+ data.tar.gz: a61fd0b11ff0d4ed92bdcd1b8eca60b5997f6881caad131132256f561a0c039cdc33e758601b7dd9da0e02534f53c21a38da6c09e38bc947a4a93834a0413210
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
- ## [Unreleased]
1
+ ## [0.3.0] - 25-05-29
2
2
 
3
- ## [0.1.0] - 2025-03-18
3
+ - Fixed static analysis of lambda expressions (arrow functions). Previously we were not including lambda parameters in the scope of the expression. See [#12](https://github.com/jg-rp/ruby-liquid2/issues/12).
4
+ - Fixed parsing of variable paths that start with `true`, `false`, `nil` or `null`. For example, `{{ true.foo }}`. See [#13](https://github.com/jg-rp/ruby-liquid2/issues/13).
5
+ - Added support for arithmetic infix operators `+`, `-`, `*`, `/`, `%` and `**`, and prefix operators `+` and `-`. These operators are disabled by default. Enable them by passing `arithmetic_operators: true` to a new `Liquid2::Environment`.
6
+
7
+ ## [0.2.0] - 25-05-12
8
+
9
+ - Fixed error context info when raising `UndefinedError` from `StrictUndefined`.
10
+ - Fixed parsing of compound expressions given as filter arguments.
11
+ - Fixed sorting of string representations of floats with the `sort_numeric` filter.
12
+ - Fixed the string representation of variable paths with bracket notation and nested paths.
13
+ - Added `Template#docs`, which returns an array of `DocTag` instances used in a template.
14
+ - Added implementations of the `{% macro %}` and `{% call %}` tags.
15
+ - Added `Template#macros`, which returns arrays of `{% macro %}` and `{% call %}` tags used in a template.
16
+ - Added template inheritance tags `{% extends %}` and `{% block %}`.
17
+ - Added block scoped variables with the `{% with %}` tag.
18
+
19
+ ## [0.1.1] - 2025-05-01
20
+
21
+ - Add `base64` dependency to gemspec.
22
+
23
+ ## [0.1.0] - 2025-05-01
4
24
 
5
25
  - Initial release
data/LICENSE_SHOPIFY.txt CHANGED
@@ -1,3 +1,11 @@
1
+ This project is not affiliated with Shopify, but we do reference
2
+ Shopify/liquid frequently and have used code from Shopify/liquid.
3
+ A copy of the Shopify/liquid license is included below.
4
+
5
+ See https://github.com/Shopify/liquid.
6
+
7
+ ---
8
+
1
9
  Copyright (c) 2005, 2006 Tobias Luetke
2
10
 
3
11
  Permission is hereby granted, free of charge, to any person obtaining
data/README.md CHANGED
@@ -4,6 +4,22 @@
4
4
  Liquid templates for Ruby, with some extra features.
5
5
  </p>
6
6
 
7
+ <p align="center">
8
+ <a href="https://github.com/jg-rp/ruby-liquid2/blob/main/LICENSE.txt">
9
+ <img alt="GitHub License" src="https://img.shields.io/github/license/jg-rp/ruby-liquid2?style=flat-square">
10
+ </a>
11
+ <a href="https://github.com/jg-rp/ruby-liquid2/actions">
12
+ <img src="https://img.shields.io/github/actions/workflow/status/jg-rp/ruby-liquid2/main.yml?branch=main&label=tests&style=flat-square" alt="Tests">
13
+ </a>
14
+ <br>
15
+ <a href="https://rubygems.org/gems/liquid2">
16
+ <img alt="Gem Version" src="https://img.shields.io/gem/v/liquid2?style=flat-square">
17
+ </a>
18
+ <a href="https://github.com/jg-rp/ruby-liquid2">
19
+ <img alt="Static Badge" src="https://img.shields.io/badge/Ruby-3.1%20%7C%203.2%20%7C%203.3%20%7C%203.4-CC342D?style=flat-square">
20
+ </a>
21
+ </p>
22
+
7
23
  ---
8
24
 
9
25
  **Table of Contents**
@@ -12,11 +28,28 @@ Liquid templates for Ruby, with some extra features.
12
28
  - [Example](#example)
13
29
  - [Links](#links)
14
30
  - [About](#about)
15
- - [Usage](#usage)
31
+ - [API](#api)
32
+ - [Development](#development)
16
33
 
17
34
  ## Install
18
35
 
19
- TODO
36
+ Add `'liquid2'` to your Gemfile:
37
+
38
+ ```
39
+ gem 'liquid2', '~> 0.3.0'
40
+ ```
41
+
42
+ Or
43
+
44
+ ```
45
+ gem install liquid2
46
+ ```
47
+
48
+ Or
49
+
50
+ ```
51
+ bundle add liquid2
52
+ ```
20
53
 
21
54
  ## Example
22
55
 
@@ -31,13 +64,13 @@ puts template.render("you" => "Liquid") # Hello, Liquid!
31
64
  ## Links
32
65
 
33
66
  - Change log: https://github.com/jg-rp/ruby-liquid2/blob/main/CHANGELOG.md
34
- - RubyGems: TODO
67
+ - RubyGems: https://rubygems.org/gems/liquid2
35
68
  - Source code: https://github.com/jg-rp/ruby-liquid2
36
69
  - Issue tracker: https://github.com/jg-rp/ruby-liquid2/issues
37
70
 
38
71
  ## About
39
72
 
40
- This project aims to be mostly compatible with [Shopify/liquid](https://github.com/Shopify/liquid), but with fewer quirks and some new features.
73
+ This project's template syntax aims to be mostly compatible with [Shopify/liquid](https://github.com/Shopify/liquid), but with fewer quirks and some new features. The [Ruby API](#api) is quite different.
41
74
 
42
75
  For those already familiar with Liquid, here's a quick description of the features added in Liquid2. Also see [test/test_compliance.rb](https://github.com/jg-rp/ruby-liquid2/blob/main/test/test_compliance.rb) for a list of [golden tests](https://github.com/jg-rp/golden-liquid) that we skip.
43
76
 
@@ -45,7 +78,7 @@ For those already familiar with Liquid, here's a quick description of the featur
45
78
 
46
79
  #### "Proper" string literal parsing
47
80
 
48
- String literals can contain markup delimiters (`{{`, `}}`, `{%`, `%}`, `{#` and `#}`) and c-like escape sequences without interfering with template parsing. Escape sequences follow JSON string syntax and semantics, with the addition of single quoted strings and the `\'` escape sequence.
81
+ String literals can contain markup delimiters (`{{`, `}}`, `{%`, `%}`, `{#` and `#}`) without interfering with template parsing, and c-like escape sequences. Escape sequences follow JSON string syntax and semantics, with the addition of single quoted strings and the `\'` escape sequence.
49
82
 
50
83
  ```liquid
51
84
  {% assign x = "Hi \uD83D\uDE00!" %}
@@ -117,14 +150,14 @@ In this example, `{% if not user %}` is equivalent to `{% unless user %}`, howev
117
150
 
118
151
  #### Inline conditional and relational expressions
119
152
 
120
- In most expressions where you'd normally provide a literal (string, integer, float, true, false, nil/null) or variable name/path (foo.bar[0]), you can now use an inline conditional or relational expression.
153
+ In most expressions where you'd normally provide a literal (string, integer, float, `true`, `false`, `nil`/`null`) or variable name/path (`foo.bar[0]`), you can now use an inline conditional or relational expression.
121
154
 
122
155
  See [Shopify/liquid #1922](https://github.com/Shopify/liquid/pull/1922) and [jg-rp/liquid #175](https://github.com/jg-rp/liquid/pull/175).
123
156
 
124
157
  These two templates are equivalent.
125
158
 
126
159
  ```liquid
127
- {{ user.name || "guest" }}
160
+ {{ user.name or "guest" }}
128
161
  ```
129
162
 
130
163
  ```liquid
@@ -152,7 +185,7 @@ Many built-in filters that operate on arrays now accept lambda expression argume
152
185
 
153
186
  #### Dedicated comment syntax
154
187
 
155
- Comments surrounded by `{#` and `#}` are enabled by default. Additional `#`'s can be added to comment out blocks of markup that already contain comments, as long as hashes are balanced.
188
+ Text surrounded by `{#` and `#}` are comments. Additional `#`'s can be added to comment out blocks of markup that already contain comments, as long as hashes are balanced.
156
189
 
157
190
  ```liquid2
158
191
  {## comment this out for now
@@ -186,21 +219,387 @@ Here we use `~` to remove the newline after the opening `for` tag, but preserve
186
219
  </ul>
187
220
  ```
188
221
 
222
+ #### Arithmetic operators
223
+
224
+ Arithmetic infix operators `+`, `-`, `*`, `/`, `%` and `**`, and prefix operators `+` and `-`, are an experimental feature and are disabled by default. Enable them by passing `arithmetic_operators: true` to a new [`Liquid2::Environment`](https://github.com/jg-rp/ruby-liquid2/blob/main/lib/liquid2/environment.rb).
225
+
189
226
  #### Scientific notation
190
227
 
191
228
  Integer and float literals can use scientific notation, like `1.2e3` or `1e-2`.
192
229
 
193
- ## Usage
230
+ #### Extra tags and filters
231
+
232
+ Liquid2 includes implementations of `{% extends %}` and `{% block %}` for template inheritance, `{% with %}` for block scoped variables and `{% macro %}` and `{% call %}` for defining parameterized blocks.
233
+
234
+ There's also built-in implementations of `sort_numeric` and `json` filters.
235
+
236
+ ## API
237
+
238
+ ### Liquid2.render
239
+
240
+ `self.render: (String source, ?Hash[String, untyped]? data) -> String`
241
+
242
+ Parse and render Liquid template _source_ using the default Liquid environment. If _data_ is given, hash keys will be available as template variables with their associated values.
243
+
244
+ ```ruby
245
+ require "liquid2"
246
+
247
+ puts Liquid2.render("Hello, {{ you }}!", "you" => "World") # Hello, World!
248
+ ```
249
+
250
+ This is a convenience method equivalent to `Liquid2::DEFAULT_ENVIRONMENT.parse(source).render(data)`.
251
+
252
+ ### Liquid2.parse
253
+
254
+ `self.parse: (String source, ?globals: Hash[String, untyped]?) -> Template`
255
+
256
+ Parse or "compile" Liquid template _source_ using the default Liquid environment. The resulting `Liquid2::Template` instance has a `render(data)` method, which can be called multiple times with different data.
257
+
258
+ ```ruby
259
+ require "liquid2"
260
+
261
+ template = Liquid2.parse("Hello, {{ you }}!")
262
+ puts template.render("you" => "World") # Hello, World!
263
+ puts template.render("you" => "Liquid") # Hello, Liquid!
264
+ ```
265
+
266
+ 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.
267
+
268
+ `Liquid2.render(source)` is a convenience method equivalent to `Liquid2::DEFAULT_ENVIRONMENT.parse(source)` or `Liquid2::Environment.new.parse(source)`.
269
+
270
+ ### Configure
271
+
272
+ Both `Liquid2.parse` and `Liquid2.render` are convenience methods that use the default `Liquid2::Environment`. Often you'll want to configure an environment, then load and render template from that.
273
+
274
+ ```ruby
275
+ require "liquid2"
276
+
277
+ env = Liquid2::Environment.new(loader: Liquid2::CachingFileSystemLoader.new("templates/"))
278
+ template = env.parse("Hello, {{ you }}!")
279
+ template.render("you" => "World") # Hello, World!
280
+ ```
281
+
282
+ Assuming you've configured a template loader, `Environment#get_template(name)`, the `{% render %}` tag and the `{% include %}` tag will use that `Liquid2::Loader` to find, read and parse templates. This example will look for templates in a relative folder on your file system called `templates`.
283
+
284
+ ```ruby
285
+ require "liquid2"
286
+
287
+ env = Liquid2::Environment.new(loader: Liquid2::CachingFileSystemLoader.new("templates/"))
288
+ template = env.get_template("index.liquid")
289
+ another_template = env.parse("{% render 'index.liquid' %}")
290
+ # ...
291
+ ```
292
+
293
+ We'd expect a `Liquid2::LiquidTemplateNotFoundError` if `index.liquid` does not exist in the folder `templates/`.
294
+
295
+ See [`environment.rb`](https://github.com/jg-rp/ruby-liquid2/blob/main/lib/liquid2/environment.rb) for all `Liquid2::Environment` options. Builtin template loaders are [`HashLoader`](https://github.com/jg-rp/ruby-liquid2/blob/main/lib/liquid2/loader.rb), [`FileSystemLoader`](https://github.com/jg-rp/ruby-liquid2/blob/main/lib/liquid2/loaders/file_system_loader.rb) and `CachingFileSystemLoader`. You are encouraged to implement your own template loaders to read template source text from a database or parse front matter, for example.
296
+
297
+ #### Tags and filters
298
+
299
+ All builtin tags and filters are registered with a new `Liquid2::Environment` by default. You can register or remove tags and/or filters using `Environment#register_filter`, `Environment#delete_filter`, `Environment#register_tag` and `Environment#delete_tag`, or override `Environment#setup_tags_and_filters` in an `Environment` subclass.
300
+
301
+ ```ruby
302
+ require "liquid2"
303
+
304
+ class MyEnv < Liquid2::Environment
305
+ def setup_tags_and_filters
306
+ super
307
+ delete_filter("base64_decode")
308
+ delete_filter("base64_encode")
309
+ delete_filter("base64_url_safe_decode")
310
+ delete_filter("base64_url_safe_encode")
311
+ register_tag("with", WithTag)
312
+ end
313
+ end
314
+
315
+ env = MyEnvironment.new
316
+ # ...
317
+ ```
318
+
319
+ See [`environment.rb`](https://github.com/jg-rp/ruby-liquid2/blob/main/lib/liquid2/environment.rb) for a list of builtin tags and filters, [`lib/liquid2/filters`](https://github.com/jg-rp/ruby-liquid2/tree/main/lib/liquid2/filters) for example filter implementations, and [`lib/liquid/nodes/tags`](https://github.com/jg-rp/ruby-liquid2/tree/main/lib/liquid2/nodes/tags) for example tag implementations.
320
+
321
+ #### Undefined
322
+
323
+ The default _undefined_ type is an instance of `Liquid2::Undefined`. It is silently ignored and, when rendered, produces an empty string. Passing `undefined: Liquid2::StrictUndefined` when initializing a `Liquid2::Environment` will cause all uses of an undefined template variable to raise a `Liquid2::UndefinedError`.
324
+
325
+ ```ruby
326
+ require "liquid2"
327
+
328
+ env = Liquid2::Environment.new(undefined: Liquid2::StrictUndefined)
329
+ template = env.parse("Hello, {{ nosuchthing }}!")
330
+ puts template.render
331
+ # -> "Hello, {{ nosuchthing }}!":1:10
332
+ # |
333
+ # 1 | Hello, {{ nosuchthing }}!
334
+ # | ^^^^^^^^^^^ "nosuchthing" is undefined
335
+ ```
336
+
337
+ By default, instances of `Liquid2::StrictUndefined` are considered falsy when tested for truthiness, without raising an error.
338
+
339
+ ```ruby
340
+ require "liquid2"
341
+
342
+ env = Liquid2::Environment.new(undefined: Liquid2::StrictUndefined)
343
+ template = env.parse("Hello, {{ nosuchthing or 'foo' }}!")
344
+ puts template.render # Hello, foo!
345
+ ```
346
+
347
+ Setting `falsy_undefined: false` when initializing a `Liquid2::Environment` will cause instances of `Liquid2::StrictUndefined` to raise an error when tested for truthiness.
348
+
349
+ There's also `Liquid2::StrictDefaultUndefined`, which behaves like `StrictUndefined` but plays nicely with the `default` filter.
350
+
351
+ ### Static analysis
352
+
353
+ Instances of `Liquid2::Template` include several methods for statically analyzing the template's syntax tree and reporting tag, filter and variable usage.
354
+
355
+ `Template#variables` returns an array of variables used in the template. Notice that we get the _root segment_ only, excluding segments that make up a path to a variable.
356
+
357
+ ```ruby
358
+ require "liquid2"
359
+
360
+ source = <<~LIQUID
361
+ Hello, {{ you }}!
362
+ {% assign x = 'foo' | upcase %}
363
+
364
+ {% for ch in x %}
365
+ - {{ ch }}
366
+ {% endfor %}
367
+
368
+ Goodbye, {{ you.first_name | capitalize }} {{ you.last_name }}
369
+ Goodbye, {{ you.first_name }} {{ you.last_name }}
370
+ LIQUID
371
+
372
+ template = Liquid2.parse(source)
373
+ p template.variables # ["you", "x", "ch"]
374
+ ```
375
+
376
+ `Template#variable_paths` is similar, but includes all segments for each variable/path.
377
+
378
+ ```ruby
379
+ # ... continued from above
380
+ p template.variable_paths # ["you", "you.first_name", "you.last_name", "x", "ch"]
381
+ ```
382
+
383
+ And `Template#variable_segments` does the same, but returns each variable/path as an array of segments instead of a string.
384
+
385
+ ```ruby
386
+ # ... continued from above
387
+ p template.variable_segments # [["you"], ["you", "first_name"], ["you", "last_name"], ["x"], ["ch"]]
388
+ ```
389
+
390
+ Sometimes you'll only be interested in variables that are not in scope from previous tags (like `assign` and `capture`) or temporary block scope variables (like `forloop`). We call such variables "global" and provide `Template#global_variables`, `Template#global_variable_paths` and `Template#global_variable_segments`.
391
+
392
+ ```ruby
393
+ # ... continued from above
394
+ p template.global_variables # ["you"]
395
+ ```
194
396
 
195
- TODO
397
+ `Template#tag_names` and `Template#filter_names` return an array of tag and filter names used in the template.
398
+
399
+ ```ruby
400
+ # ... continued from above
401
+ p template.filter_names # ["upcase", "capitalize"]
402
+ p template.tag_names # ["assign", "for"]
403
+ ```
404
+
405
+ `Template#macros` returns arrays of `{% macro %}` and `{% call %}` tags found in the template.
406
+
407
+ ```ruby
408
+ require "liquid2"
409
+
410
+ source = <<~LIQUID
411
+ {% macro foo, you %}Hello, {{ you }}!{% endmacro -%}
412
+ {% call foo, 'World' %}
413
+ {% call bar, 'Liquid' %}
414
+ LIQUID
415
+
416
+ template = Liquid2.parse(source)
417
+ macro_tags, call_tags = template.macros
418
+
419
+ p macro_tags.map(&:macro_name).uniq # ["foo"]
420
+ p call_tags.map(&:macro_name).uniq # ["foo", "bar"]
421
+ ```
422
+
423
+ Finally there's `Template#comments` and `Template#docs`, which return instances of comments nodes and `DocTag` nodes, respectively. Each node has a `token` attribute, including a start index, and a `text` attribute, which is the comment or doc text.
424
+
425
+ ```ruby
426
+ require "liquid2"
427
+
428
+ source = <<~LIQUID
429
+ {% doc %}
430
+ Some doc comment
431
+ {% enddoc %}
432
+ {% assign x = 42 %}
433
+
434
+ {# note y could be nil #}
435
+ {{ x | plus: y or 7 }}
436
+ LIQUID
437
+
438
+ template = Liquid2.parse(source)
439
+ p template.docs.map(&:text) # ["\n Some doc comment\n"]
440
+ p template.comments.map(&:text) # [" note y could be nil "]
441
+ ```
442
+
443
+ ### Drops
444
+
445
+ Our "drop" interface defines the methods used by Liquid2 when non-primitive values appear as variables in templates. Primitive types are strings, integers, floats, `true`, `false`, `nil`/`null`, hashes, arrays and the special `blank` and `empty` objects. All other objects are accessed using the drop interface.
446
+
447
+ #### To string
448
+
449
+ `to_s` is called when outputting a drop.
450
+
451
+ ```ruby
452
+ require "liquid2"
453
+
454
+ class MyDrop
455
+ def to_s
456
+ "Hi!"
457
+ end
458
+ end
459
+
460
+ puts Liquid2.render("{{ thing }}", "thing" => MyDrop.new) # Hi!
461
+ ```
462
+
463
+ #### Item fetching and method calling
464
+
465
+ We use `[](key)` and `key?(key)` both for fetching items from a collection-like object and/or calling methods. Notice that `baz` is not called in this example.
466
+
467
+ ```ruby
468
+ require "liquid2"
469
+
470
+ class MyDrop
471
+ INVOCABLE = Set["foo", "bar"]
472
+
473
+ def [](key)
474
+ send(key) if INVOCABLE.member?(key)
475
+ end
476
+
477
+ def key?(key)
478
+ INVOCABLE.member?(key)
479
+ end
480
+
481
+ def foo = 42
482
+ def bar = "Hello!"
483
+ def baz = "not public"
484
+ end
485
+
486
+ puts Liquid2.render("{{ thing.foo }} {{ thing.baz }}", "thing" => MyDrop.new) # 42
487
+ ```
488
+
489
+ #### Enumerating drops
490
+
491
+ Any `Enumerable` will work with the `{% for %}` tag and filters that operate on arrays.
492
+
493
+ ```ruby
494
+ require "liquid2"
495
+
496
+ class MyDrop
497
+ include Enumerable
498
+
499
+ def each
500
+ yield "foo"
501
+ yield "bar"
502
+ yield 42
503
+ end
504
+ end
505
+
506
+ puts Liquid2.render("{% for x in y %}{{ x }},{% endfor %}", "y" => MyDrop.new) # foo,bar,42,
507
+ ```
508
+
509
+ #### First, last and size
510
+
511
+ If an object responds to `first`, `last` or `size`, those methods will be called by the `first`, `last` and `size` filters, and the special `.first`, `.last` or `.size` attributes.
512
+
513
+ ```ruby
514
+ require "liquid2"
515
+
516
+ class MyDrop
517
+ def first
518
+ 42
519
+ end
520
+ end
521
+
522
+ puts Liquid2.render("{{ x.first }} {{ x | first }}", "x" => MyDrop.new) # 42 42
523
+ ```
524
+
525
+ #### Lazy slicing
526
+
527
+ If an object responds to `slice`, Liquid will call it with `start`, `length` and `reversed` arguments when evaluating it as a `for` loop target. `slice` takes priority over `is_a?(Enumerable)` and is intended for lazily loading items when slicing large collections.
528
+
529
+ ```ruby
530
+ require "liquid2"
531
+
532
+ class MyDrop
533
+ def initialize(*items)
534
+ @items = items
535
+ end
536
+
537
+ def slice(start, length, reversed)
538
+ # Pretend items are coming from a database or over a network.
539
+ array = @items.slice(start || 0, length || @items.length)
540
+ reversed ? array.reverse! : array
541
+ end
542
+ end
543
+
544
+ puts Liquid2.render("{% for x in y offset: 2, limit: 5 %}{{ x }},{% endfor %}", "y" => MyDrop.new) # 2,3,4,5,6,
545
+ ```
546
+
547
+ #### Truthiness, comparisons and path segments
548
+
549
+ If an object responds to `to_liquid(context)`, `to_liquid` will be called and the result used when testing drops for truthiness, comparing a drop to another value or when using the drop as a variable path segment.
550
+
551
+ ```ruby
552
+ require "liquid2"
553
+
554
+ class MyDrop
555
+ # @param context [RenderContext]
556
+ def to_liquid(_context)
557
+ "foo"
558
+ end
559
+ end
560
+
561
+ puts Liquid2.render("{% if x == 'foo' %}true{% else %}false{% endif %}", "x" => MyDrop.new) # true
562
+ ```
196
563
 
197
564
  ## Development
198
565
 
199
- TODO
566
+ The [golden liquid](https://github.com/jg-rp/golden-liquid) test suite is included in this repository as a Git [submodule](https://git-scm.com/book/en/v2/Git-Tools-Submodules). Clone this project and initialize the submodule with something like:
200
567
 
201
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
568
+ ```shell
569
+ $ git clone git@github.com:jg-rp/ruby-liquid2.git
570
+ $ cd ruby-liquid2
571
+ $ git submodule update --init
572
+ ```
202
573
 
203
- ## Contributing
574
+ We use [Bundler](https://bundler.io/) and [Rake](https://ruby.github.io/rake/). Install development dependencies with:
575
+
576
+ ```
577
+ bundle install
578
+ ```
579
+
580
+ Run tests with:
581
+
582
+ ```
583
+ bundle exec rake test
584
+ ```
585
+
586
+ Lint with:
587
+
588
+ ```
589
+ bundle exec rubocop
590
+ ```
591
+
592
+ And type check with:
593
+
594
+ ```
595
+ bundle exec steep
596
+ ```
597
+
598
+ Run one of the benchmarks with:
599
+
600
+ ```
601
+ bundle exec ruby performance/benchmark.rb
602
+ ```
204
603
 
205
604
  ### Profiling
206
605
 
@@ -212,6 +611,14 @@ Dump profile data with `bundle exec ruby performance/profile.rb`, then generate
212
611
  bundle exec stackprof --d3-flamegraph .stackprof-cpu-parse.dump > flamegraph-cpu-parse.html
213
612
  ```
214
613
 
614
+ #### Memory profile
615
+
616
+ Print memory usage to the terminal.
617
+
618
+ ```
619
+ bundle exec ruby performance/memory_profile.rb
620
+ ```
621
+
215
622
  ## License
216
623
 
217
624
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -29,8 +29,8 @@ module Liquid2
29
29
  # Per render contextual information. A new RenderContext is created automatically
30
30
  # every time `Template#render` is called.
31
31
  class RenderContext
32
- attr_reader :env, :template, :disabled_tags, :globals
33
- attr_accessor :interrupts, :tag_namespace
32
+ attr_reader :env, :disabled_tags, :globals
33
+ attr_accessor :interrupts, :tag_namespace, :template
34
34
 
35
35
  BUILT_IN = BuiltIn.new
36
36
 
@@ -71,7 +71,6 @@ module Liquid2
71
71
 
72
72
  # Namespaces are searched from right to left. When a RenderContext is extended, the
73
73
  # temporary namespace is pushed to the end of this queue.
74
- # TODO: exclude @globals if globals is empty
75
74
  @scope = ReadOnlyChainHash.new(@counters, BUILT_IN, @globals, @locals)
76
75
 
77
76
  # A namespace supporting stateful tags, such as `cycle` and `increment`.
@@ -171,10 +170,16 @@ module Liquid2
171
170
  template_ = @template
172
171
  @template = template if template
173
172
  @scope << namespace
174
- yield
175
- ensure
176
- @template = template_
177
- @scope.pop
173
+ begin
174
+ yield
175
+ rescue LiquidError => e
176
+ e.template_name = template.full_name if template && !e.template_name
177
+ e.source = template.source if template && !e.source
178
+ raise
179
+ ensure
180
+ @template = template_
181
+ @scope.pop
182
+ end
178
183
  end
179
184
 
180
185
  # Copy this render context and add _namespace_ to the new scope.
@@ -206,13 +211,17 @@ module Liquid2
206
211
  ReadOnlyChainHash.new(@globals, namespace)
207
212
  end
208
213
 
209
- self.class.new(template || @template,
210
- globals: scope,
211
- disabled_tags: disabled_tags,
212
- copy_depth: @copy_depth + 1,
213
- parent: self,
214
- loop_carry: loop_carry,
215
- local_namespace_carry: @assign_score)
214
+ context = self.class.new(template || @template,
215
+ globals: scope,
216
+ disabled_tags: disabled_tags,
217
+ copy_depth: @copy_depth + 1,
218
+ parent: self,
219
+ loop_carry: loop_carry,
220
+ local_namespace_carry: @assign_score)
221
+
222
+ # XXX: bit of a hack
223
+ context.tag_namespace[:extends] = @tag_namespace[:extends]
224
+ context
216
225
  end
217
226
 
218
227
  # Push a new namespace and forloop for the duration of a block.
@@ -222,10 +231,12 @@ module Liquid2
222
231
  raise_for_loop_limit(length: forloop.length)
223
232
  @loops << forloop
224
233
  @scope << namespace
225
- yield
226
- ensure
227
- @scope.pop
228
- @loops.pop
234
+ begin
235
+ yield
236
+ ensure
237
+ @scope.pop
238
+ @loops.pop
239
+ end
229
240
  end
230
241
 
231
242
  # Return the last ForLoop obj if one is available, or an instance of Undefined otherwise.