liquid2 0.1.0 → 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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +16 -2
- data/LICENSE_SHOPIFY.txt +8 -0
- data/README.md +416 -13
- data/lib/liquid2/context.rb +29 -18
- data/lib/liquid2/environment.rb +53 -6
- data/lib/liquid2/errors.rb +4 -2
- data/lib/liquid2/expressions/arguments.rb +20 -0
- data/lib/liquid2/expressions/boolean.rb +2 -1
- data/lib/liquid2/expressions/filtered.rb +19 -25
- data/lib/liquid2/expressions/loop.rb +7 -5
- data/lib/liquid2/expressions/path.rb +19 -2
- data/lib/liquid2/filter.rb +1 -2
- data/lib/liquid2/filters/array.rb +0 -1
- data/lib/liquid2/filters/sort.rb +5 -4
- data/lib/liquid2/loader.rb +1 -0
- data/lib/liquid2/nodes/tags/doc.rb +2 -0
- data/lib/liquid2/nodes/tags/extends.rb +270 -1
- data/lib/liquid2/nodes/tags/include.rb +7 -7
- data/lib/liquid2/nodes/tags/macro.rb +145 -1
- data/lib/liquid2/nodes/tags/render.rb +8 -10
- data/lib/liquid2/nodes/tags/with.rb +42 -1
- data/lib/liquid2/parser.rb +84 -7
- data/lib/liquid2/scanner.rb +18 -42
- data/lib/liquid2/static_analysis.rb +1 -1
- data/lib/liquid2/template.rb +52 -5
- data/lib/liquid2/undefined.rb +22 -20
- data/lib/liquid2/version.rb +1 -1
- data/lib/liquid2.rb +2 -0
- data/sig/liquid2.rbs +234 -28
- data.tar.gz.sig +0 -0
- metadata +15 -2
- metadata.gz.sig +0 -0
- data/.vscode/settings.json +0 -32
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 396a11c43cb73cebe443927f69b83210d9c1651d9168ad7412726f54470492a7
|
4
|
+
data.tar.gz: ed127fb805a3862d3bde1db35dd88a713b714af3f65c51ba58d0f85fd09052c3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
## [
|
1
|
+
## [0.2.0] - 25-05-12
|
2
2
|
|
3
|
-
|
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
|
-
- [
|
31
|
+
- [API](#api)
|
32
|
+
- [Development](#development)
|
16
33
|
|
17
34
|
## Install
|
18
35
|
|
19
|
-
|
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:
|
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
|
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
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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).
|
data/lib/liquid2/context.rb
CHANGED
@@ -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, :
|
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
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
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
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
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
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
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.
|