liquid2 0.1.1 → 0.2.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: 396a11c43cb73cebe443927f69b83210d9c1651d9168ad7412726f54470492a7
4
+ data.tar.gz: ed127fb805a3862d3bde1db35dd88a713b714af3f65c51ba58d0f85fd09052c3
5
5
  SHA512:
6
- metadata.gz: 40a9180a57d81c6461c056de6bf38e10ab6a95bb177acb59ad43aa0a92a80f44c3070c490afc67f30a12035427098fd78d26915b4336e587a1cd25188d49223a
7
- data.tar.gz: e2a572fde0fffb372f37fbe8ad02bb49acaf6d55e2c449f6c3502b49b07996843c4396bbde08135629195d39e10b305e8848b1f17aec75ad0690951dba41f8e9
6
+ metadata.gz: 5c72bdc10c87b94b3863826cd1e964be714de2277c1c4627480fe0a9f869de977ae9efeb55cbaec573a625e43a46a0758312c36e15697d97934b177c968c078f
7
+ data.tar.gz: f2c6ec9e5d3d6d0fb86ba2455c0ec8134c8896243c6d8fab859ee3fbbd2c1bb17f2aaf949d591edf96eaf998d5562f7290e1ab9893502764ac9498ddb33aba97
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
- ## [Unreleased]
1
+ ## [0.2.0] - 25-05-12
2
2
 
3
- ## [0.1.0] - 2025-03-18
3
+ - Fixed error context info when raising `UndefinedError` from `StrictUndefined`.
4
+ - Fixed parsing of compound expressions given as filter arguments.
5
+ - Fixed sorting of string representations of floats with the `sort_numeric` filter.
6
+ - Fixed the string representation of variable paths with bracket notation and nested paths.
7
+ - Added `Template#docs`, which returns an array of `DocTag` instances used in a template.
8
+ - Added implementations of the `{% macro %}` and `{% call %}` tags.
9
+ - Added `Template#macros`, which returns arrays of `{% macro %}` and `{% call %}` tags used in a template.
10
+ - Added template inheritance tags `{% extends %}` and `{% block %}`.
11
+ - Added block scoped variables with the `{% with %}` tag.
12
+
13
+ ## [0.1.1] - 2025-05-01
14
+
15
+ - Add `base64` dependency to gemspec.
16
+
17
+ ## [0.1.0] - 2025-05-01
4
18
 
5
19
  - 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.2.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
@@ -190,17 +223,379 @@ Here we use `~` to remove the newline after the opening `for` tag, but preserve
190
223
 
191
224
  Integer and float literals can use scientific notation, like `1.2e3` or `1e-2`.
192
225
 
193
- ## Usage
226
+ #### Extra tags and filters
227
+
228
+ Liquid2 includes implementations of `{% extends %}` and `{% block %}` for template inheritance, `{% with %}` for block scoped variables and `{% macro %}` and `{% call %}` for defining parameterized blocks.
229
+
230
+ There's also built-in implementations of `sort_numeric` and `json` filters.
231
+
232
+ ## API
233
+
234
+ ### Liquid2.render
235
+
236
+ `self.render: (String source, ?Hash[String, untyped]? data) -> String`
237
+
238
+ 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.
239
+
240
+ ```ruby
241
+ require "liquid2"
242
+
243
+ puts Liquid2.render("Hello, {{ you }}!", "you" => "World") # Hello, World!
244
+ ```
245
+
246
+ This is a convenience method equivalent to `Liquid2::DEFAULT_ENVIRONMENT.parse(source).render(data)`.
247
+
248
+ ### Liquid2.parse
249
+
250
+ `self.parse: (String source, ?globals: Hash[String, untyped]?) -> Template`
251
+
252
+ 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.
253
+
254
+ ```ruby
255
+ require "liquid2"
256
+
257
+ template = Liquid2.parse("Hello, {{ you }}!")
258
+ puts template.render("you" => "World") # Hello, World!
259
+ puts template.render("you" => "Liquid") # Hello, Liquid!
260
+ ```
261
+
262
+ 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.
263
+
264
+ `Liquid2.render(source)` is a convenience method equivalent to `Liquid2::DEFAULT_ENVIRONMENT.parse(source)` or `Liquid2::Environment.new.parse(source)`.
265
+
266
+ ### Configure
267
+
268
+ 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.
269
+
270
+ ```ruby
271
+ require "liquid2"
272
+
273
+ env = Liquid2::Environment.new(loader: Liquid2::CachingFileSystemLoader.new("templates/"))
274
+ template = env.parse("Hello, {{ you }}!")
275
+ template.render("you" => "World") # Hello, World!
276
+ ```
277
+
278
+ 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`.
279
+
280
+ ```ruby
281
+ require "liquid2"
282
+
283
+ env = Liquid2::Environment.new(loader: Liquid2::CachingFileSystemLoader.new("templates/"))
284
+ template = env.get_template("index.liquid")
285
+ another_template = env.parse("{% render 'index.liquid' %}")
286
+ # ...
287
+ ```
194
288
 
195
- TODO
289
+ We'd expect a `Liquid2::LiquidTemplateNotFoundError` if `index.liquid` does not exist in the folder `templates/`.
290
+
291
+ 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.
292
+
293
+ #### Tags and filters
294
+
295
+ 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.
296
+
297
+ ```ruby
298
+ require "liquid2"
299
+
300
+ class MyEnv < Liquid2::Environment
301
+ def setup_tags_and_filters
302
+ super
303
+ delete_filter("base64_decode")
304
+ delete_filter("base64_encode")
305
+ delete_filter("base64_url_safe_decode")
306
+ delete_filter("base64_url_safe_encode")
307
+ register_tag("with", WithTag)
308
+ end
309
+ end
310
+
311
+ env = MyEnvironment.new
312
+ # ...
313
+ ```
314
+
315
+ 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.
316
+
317
+ #### Undefined
318
+
319
+ 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`.
320
+
321
+ ```ruby
322
+ require "liquid2"
323
+
324
+ env = Liquid2::Environment.new(undefined: Liquid2::StrictUndefined)
325
+ template = env.parse("Hello, {{ nosuchthing }}!")
326
+ puts template.render
327
+ # -> "Hello, {{ nosuchthing }}!":1:10
328
+ # |
329
+ # 1 | Hello, {{ nosuchthing }}!
330
+ # | ^^^^^^^^^^^ "nosuchthing" is undefined
331
+ ```
332
+
333
+ By default, instances of `Liquid2::StrictUndefined` are considered falsy when tested for truthiness, without raising an error.
334
+
335
+ ```ruby
336
+ require "liquid2"
337
+
338
+ env = Liquid2::Environment.new(undefined: Liquid2::StrictUndefined)
339
+ template = env.parse("Hello, {{ nosuchthing or 'foo' }}!")
340
+ puts template.render # Hello, foo!
341
+ ```
342
+
343
+ Setting `falsy_undefined: false` when initializing a `Liquid2::Environment` will cause instances of `Liquid2::StrictUndefined` to raise an error when tested for truthiness.
344
+
345
+ There's also `Liquid2::StrictDefaultUndefined`, which behaves like `StrictUndefined` but plays nicely with the `default` filter.
346
+
347
+ ### Static analysis
348
+
349
+ Instances of `Liquid2::Template` include several methods for statically analyzing the template's syntax tree and reporting tag, filter and variable usage.
350
+
351
+ `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.
352
+
353
+ ```ruby
354
+ require "liquid2"
355
+
356
+ source = <<~LIQUID
357
+ Hello, {{ you }}!
358
+ {% assign x = 'foo' | upcase %}
359
+
360
+ {% for ch in x %}
361
+ - {{ ch }}
362
+ {% endfor %}
363
+
364
+ Goodbye, {{ you.first_name | capitalize }} {{ you.last_name }}
365
+ Goodbye, {{ you.first_name }} {{ you.last_name }}
366
+ LIQUID
367
+
368
+ template = Liquid2.parse(source)
369
+ p template.variables # ["you", "x", "ch"]
370
+ ```
371
+
372
+ `Template#variable_paths` is similar, but includes all segments for each variable/path.
373
+
374
+ ```ruby
375
+ # ... continued from above
376
+ p template.variable_paths # ["you", "you.first_name", "you.last_name", "x", "ch"]
377
+ ```
378
+
379
+ And `Template#variable_segments` does the same, but returns each variable/path as an array of segments instead of a string.
380
+
381
+ ```ruby
382
+ # ... continued from above
383
+ p template.variable_segments # [["you"], ["you", "first_name"], ["you", "last_name"], ["x"], ["ch"]]
384
+ ```
385
+
386
+ 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`.
387
+
388
+ ```ruby
389
+ # ... continued from above
390
+ p template.global_variables # ["you"]
391
+ ```
392
+
393
+ `Template#tag_names` and `Template#filter_names` return an array of tag and filter names used in the template.
394
+
395
+ ```ruby
396
+ # ... continued from above
397
+ p template.filter_names # ["upcase", "capitalize"]
398
+ p template.tag_names # ["assign", "for"]
399
+ ```
400
+
401
+ `Template#macros` returns arrays of `{% macro %}` and `{% call %}` tags found in the template.
402
+
403
+ ```ruby
404
+ require "liquid2"
405
+
406
+ source = <<~LIQUID
407
+ {% macro foo, you %}Hello, {{ you }}!{% endmacro -%}
408
+ {% call foo, 'World' %}
409
+ {% call bar, 'Liquid' %}
410
+ LIQUID
411
+
412
+ template = Liquid2.parse(source)
413
+ macro_tags, call_tags = template.macros
414
+
415
+ p macro_tags.map(&:macro_name).uniq # ["foo"]
416
+ p call_tags.map(&:macro_name).uniq # ["foo", "bar"]
417
+ ```
418
+
419
+ 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.
420
+
421
+ ```ruby
422
+ require "liquid2"
423
+
424
+ source = <<~LIQUID
425
+ {% doc %}
426
+ Some doc comment
427
+ {% enddoc %}
428
+ {% assign x = 42 %}
429
+
430
+ {# note y could be nil #}
431
+ {{ x | plus: y or 7 }}
432
+ LIQUID
433
+
434
+ template = Liquid2.parse(source)
435
+ p template.docs.map(&:text) # ["\n Some doc comment\n"]
436
+ p template.comments.map(&:text) # [" note y could be nil "]
437
+ ```
438
+
439
+ ### Drops
440
+
441
+ 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.
442
+
443
+ #### To string
444
+
445
+ `to_s` is called when outputting a drop.
446
+
447
+ ```ruby
448
+ require "liquid2"
449
+
450
+ class MyDrop
451
+ def to_s
452
+ "Hi!"
453
+ end
454
+ end
455
+
456
+ puts Liquid2.render("{{ thing }}", "thing" => MyDrop.new) # Hi!
457
+ ```
458
+
459
+ #### Item fetching and method calling
460
+
461
+ 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.
462
+
463
+ ```ruby
464
+ require "liquid2"
465
+
466
+ class MyDrop
467
+ INVOCABLE = Set["foo", "bar"]
468
+
469
+ def [](key)
470
+ send(key) if INVOCABLE.member?(key)
471
+ end
472
+
473
+ def key?(key)
474
+ INVOCABLE.member?(key)
475
+ end
476
+
477
+ def foo = 42
478
+ def bar = "Hello!"
479
+ def baz = "not public"
480
+ end
481
+
482
+ puts Liquid2.render("{{ thing.foo }} {{ thing.baz }}", "thing" => MyDrop.new) # 42
483
+ ```
484
+
485
+ #### Enumerating drops
486
+
487
+ Any `Enumerable` will work with the `{% for %}` tag and filters that operate on arrays.
488
+
489
+ ```ruby
490
+ require "liquid2"
491
+
492
+ class MyDrop
493
+ include Enumerable
494
+
495
+ def each
496
+ yield "foo"
497
+ yield "bar"
498
+ yield 42
499
+ end
500
+ end
501
+
502
+ puts Liquid2.render("{% for x in y %}{{ x }},{% endfor %}", "y" => MyDrop.new) # foo,bar,42,
503
+ ```
504
+
505
+ #### First, last and size
506
+
507
+ 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.
508
+
509
+ ```ruby
510
+ require "liquid2"
511
+
512
+ class MyDrop
513
+ def first
514
+ 42
515
+ end
516
+ end
517
+
518
+ puts Liquid2.render("{{ x.first }} {{ x | first }}", "x" => MyDrop.new) # 42 42
519
+ ```
520
+
521
+ #### Lazy slicing
522
+
523
+ 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.
524
+
525
+ ```ruby
526
+ require "liquid2"
527
+
528
+ class MyDrop
529
+ def initialize(*items)
530
+ @items = items
531
+ end
532
+
533
+ def slice(start, length, reversed)
534
+ # Pretend items are coming from a database or over a network.
535
+ array = @items.slice(start || 0, length || @items.length)
536
+ reversed ? array.reverse! : array
537
+ end
538
+ end
539
+
540
+ puts Liquid2.render("{% for x in y offset: 2, limit: 5 %}{{ x }},{% endfor %}", "y" => MyDrop.new) # 2,3,4,5,6,
541
+ ```
542
+
543
+ #### Truthiness, comparisons and path segments
544
+
545
+ 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.
546
+
547
+ ```ruby
548
+ require "liquid2"
549
+
550
+ class MyDrop
551
+ # @param context [RenderContext]
552
+ def to_liquid(_context)
553
+ "foo"
554
+ end
555
+ end
556
+
557
+ puts Liquid2.render("{% if x == 'foo' %}true{% else %}false{% endif %}", "x" => MyDrop.new) # true
558
+ ```
196
559
 
197
560
  ## Development
198
561
 
199
- TODO
562
+ 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
563
 
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.
564
+ ```shell
565
+ $ git clone git@github.com:jg-rp/ruby-liquid2.git
566
+ $ cd ruby-liquid2
567
+ $ git submodule update --init
568
+ ```
202
569
 
203
- ## Contributing
570
+ We use [Bundler](https://bundler.io/) and [Rake](https://ruby.github.io/rake/). Install development dependencies with:
571
+
572
+ ```
573
+ bundle install
574
+ ```
575
+
576
+ Run tests with:
577
+
578
+ ```
579
+ bundle exec rake test
580
+ ```
581
+
582
+ Lint with:
583
+
584
+ ```
585
+ bundle exec rubocop
586
+ ```
587
+
588
+ And type check with:
589
+
590
+ ```
591
+ bundle exec steep
592
+ ```
593
+
594
+ Run one of the benchmarks with:
595
+
596
+ ```
597
+ bundle exec ruby performance/benchmark.rb
598
+ ```
204
599
 
205
600
  ### Profiling
206
601
 
@@ -212,6 +607,14 @@ Dump profile data with `bundle exec ruby performance/profile.rb`, then generate
212
607
  bundle exec stackprof --d3-flamegraph .stackprof-cpu-parse.dump > flamegraph-cpu-parse.html
213
608
  ```
214
609
 
610
+ #### Memory profile
611
+
612
+ Print memory usage to the terminal.
613
+
614
+ ```
615
+ bundle exec ruby performance/memory_profile.rb
616
+ ```
617
+
215
618
  ## License
216
619
 
217
620
  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.