mirror of
https://github.com/Shopify/liquid.git
synced 2026-09-13 07:50:43 -07:00
Compare commits
383
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4e368b5f9 | ||
|
|
0672ebfcbb | ||
|
|
871336753e | ||
|
|
12f11876dd | ||
|
|
106ca1bd69 | ||
|
|
7d2a9da8bc | ||
|
|
e20df18eca | ||
|
|
9ccbf64571 | ||
|
|
8b1b9f649a | ||
|
|
474315c6bb | ||
|
|
667664bb22 | ||
|
|
1cdd1f0834 | ||
|
|
1cd5ec54f0 | ||
|
|
29732f4305 | ||
|
|
abed47547c | ||
|
|
ace3fe15ac | ||
|
|
db8e85ab31 | ||
|
|
f484b868d0 | ||
|
|
3e8994c258 | ||
|
|
7ccac29688 | ||
|
|
96b5325f87 | ||
|
|
b8f05267a9 | ||
|
|
6ce4ec1011 | ||
|
|
a39422feac | ||
|
|
2c2f5826d5 | ||
|
|
c99c93255d | ||
|
|
c0c191cabd | ||
|
|
6765d93938 | ||
|
|
4f17abfb4a | ||
|
|
eff2a63204 | ||
|
|
fbab19ac8c | ||
|
|
b14bf94c98 | ||
|
|
951be6c1f5 | ||
|
|
c8f3cfa8fc | ||
|
|
f21d1c5d9d | ||
|
|
456be2f75e | ||
|
|
ff1c35b986 | ||
|
|
cab08cfe57 | ||
|
|
bb7027138e | ||
|
|
34512df8e9 | ||
|
|
bca01e8944 | ||
|
|
d3647e280d | ||
|
|
c0f565ce7b | ||
|
|
753015d8fc | ||
|
|
f4e32d2214 | ||
|
|
8adbbfeaa6 | ||
|
|
4648f0fa64 | ||
|
|
3a64b3741f | ||
|
|
2c51a1922a | ||
|
|
57fdc1b5fb | ||
|
|
308dfc3cb6 | ||
|
|
2515f3be09 | ||
|
|
433ed0fff2 | ||
|
|
0787660603 | ||
|
|
93c252fe5a | ||
|
|
df13389940 | ||
|
|
ca2d850eea | ||
|
|
3a736da222 | ||
|
|
eb89f22d93 | ||
|
|
98e146ebf7 | ||
|
|
7611463f02 | ||
|
|
f1846d63a3 | ||
|
|
af3f8612bf | ||
|
|
6f8722a6d3 | ||
|
|
81f44e36be | ||
|
|
c9ec8f4635 | ||
|
|
3fb467f069 | ||
|
|
992e15a173 | ||
|
|
0bb6539dce | ||
|
|
1fdc577246 | ||
|
|
74245cd396 | ||
|
|
86605016e1 | ||
|
|
6981305736 | ||
|
|
6d3c5ef3d3 | ||
|
|
150ddf4c3b | ||
|
|
a3e9088a0e | ||
|
|
b05393884d | ||
|
|
ef9db08642 | ||
|
|
5dd8c84b47 | ||
|
|
1af12c74cc | ||
|
|
72bbbda022 | ||
|
|
489e3ca7bf | ||
|
|
65542f9e4f | ||
|
|
d497bfffe9 | ||
|
|
ebafb0a3fe | ||
|
|
4ec9db3f99 | ||
|
|
95eb5d6036 | ||
|
|
7ba40d48c0 | ||
|
|
9020dbcd41 | ||
|
|
73f7467258 | ||
|
|
f3aa5fbd7c | ||
|
|
eb70bb9b87 | ||
|
|
4d8e55dbc4 | ||
|
|
f697093e94 | ||
|
|
7594bed88a | ||
|
|
f64471eb4e | ||
|
|
d4c24f3ce2 | ||
|
|
3bbb7aa7ba | ||
|
|
102bac2e33 | ||
|
|
e69f729f76 | ||
|
|
4ec0b85d80 | ||
|
|
f77c766262 | ||
|
|
7bc14ae2be | ||
|
|
41c19929ad | ||
|
|
05d768c6ab | ||
|
|
54414dfd83 | ||
|
|
23a8438fa6 | ||
|
|
21f3337dec | ||
|
|
1f0a0ad55c | ||
|
|
36dce29776 | ||
|
|
8b68630a11 | ||
|
|
8882338aa1 | ||
|
|
6c2c621712 | ||
|
|
1cae1e497f | ||
|
|
ed7dae50aa | ||
|
|
7e99432bd1 | ||
|
|
6c187b8470 | ||
|
|
6e07f73f68 | ||
|
|
f64af57b7b | ||
|
|
c60c3c7802 | ||
|
|
11625b1bc9 | ||
|
|
ec6fb4d5fa | ||
|
|
fad58ef436 | ||
|
|
22568080b1 | ||
|
|
dd7ed00ec4 | ||
|
|
1667c1180e | ||
|
|
7357dcf185 | ||
|
|
68c3827ef2 | ||
|
|
4af38bc549 | ||
|
|
df241abf70 | ||
|
|
10f8337209 | ||
|
|
5ed0410a8b | ||
|
|
0f5220c391 | ||
|
|
7a23f46fab | ||
|
|
3f7edf00b9 | ||
|
|
b4a2a79e26 | ||
|
|
1d2bee1f60 | ||
|
|
01e6eec97a | ||
|
|
fbdab19358 | ||
|
|
ce85ac5d3d | ||
|
|
c0ffee16a3 | ||
|
|
a7eb33fa39 | ||
|
|
1a85e98793 | ||
|
|
c588337aac | ||
|
|
97f7922457 | ||
|
|
0d83e64cfe | ||
|
|
0d5e01ae98 | ||
|
|
15eaa49e48 | ||
|
|
91c54c579d | ||
|
|
1310c4978d | ||
|
|
3de1db3c3a | ||
|
|
03522caaf8 | ||
|
|
7acea2a9c9 | ||
|
|
d8ef698539 | ||
|
|
ebdfdb80e5 | ||
|
|
95e9fa5010 | ||
|
|
0e14d539a3 | ||
|
|
db106ae058 | ||
|
|
db3999a008 | ||
|
|
10e2aa8d5b | ||
|
|
7c4114671b | ||
|
|
b01de9d325 | ||
|
|
a5369c26a8 | ||
|
|
a03de8f9ea | ||
|
|
e86fe27259 | ||
|
|
c8906d05b9 | ||
|
|
50d1a2ffc9 | ||
|
|
f686c5dec7 | ||
|
|
aa8ce87b96 | ||
|
|
698f5e0d96 | ||
|
|
996bfe0c82 | ||
|
|
be81c9ae5a | ||
|
|
edd4d70aee | ||
|
|
ac66dbbafe | ||
|
|
017c1b5e83 | ||
|
|
250555c9a8 | ||
|
|
e361a4d53c | ||
|
|
b9e0d28729 | ||
|
|
020f6b93c5 | ||
|
|
cfe1637bdd | ||
|
|
eab13a07d9 | ||
|
|
ca96ca0fef | ||
|
|
4e7a953e73 | ||
|
|
6ac2499f7f | ||
|
|
ff70161512 | ||
|
|
026157e128 | ||
|
|
bf64239ea6 | ||
|
|
c270a6f378 | ||
|
|
4fba61a802 | ||
|
|
6b6baece25 | ||
|
|
15b2d193ec | ||
|
|
fd712d134a | ||
|
|
0c2db998cf | ||
|
|
9dac68cce1 | ||
|
|
c50509b741 | ||
|
|
cd66572514 | ||
|
|
dcb5a67089 | ||
|
|
efe44a7e6a | ||
|
|
8625e66453 | ||
|
|
3cae09b968 | ||
|
|
3c499d0241 | ||
|
|
e71e53ffb5 | ||
|
|
260c863e23 | ||
|
|
42b6c07cd0 | ||
|
|
c91a6827f2 | ||
|
|
5dbc3d5701 | ||
|
|
22683cbd2a | ||
|
|
abfab3bef2 | ||
|
|
51e8d6234a | ||
|
|
7ca2846d9c | ||
|
|
7ba0fc7952 | ||
|
|
e2c86d137f | ||
|
|
776a63b61d | ||
|
|
84f9d6957c | ||
|
|
7d32728e16 | ||
|
|
40a9b72b3c | ||
|
|
4ff26cd707 | ||
|
|
462919a28f | ||
|
|
f3e2be9f85 | ||
|
|
4d40f83457 | ||
|
|
00be1e4dd4 | ||
|
|
f7d67b946e | ||
|
|
ae9aee896b | ||
|
|
6dec172743 | ||
|
|
da581d988a | ||
|
|
7960826552 | ||
|
|
84059691b8 | ||
|
|
896288eff1 | ||
|
|
b3f132efd1 | ||
|
|
60214b957c | ||
|
|
7361220af6 | ||
|
|
cb2ad71a31 | ||
|
|
900e3a6491 | ||
|
|
f18084203d | ||
|
|
3358a892f2 | ||
|
|
bbfcaa2cc0 | ||
|
|
ba657871bc | ||
|
|
29d5d9674a | ||
|
|
0a645e72c1 | ||
|
|
1850511334 | ||
|
|
300adfd7ae | ||
|
|
f357662f37 | ||
|
|
ed0aebcbc9 | ||
|
|
ea4f1885f8 | ||
|
|
2f75db604f | ||
|
|
d844a3dd8b | ||
|
|
9fcba1a26c | ||
|
|
0659891e68 | ||
|
|
e7fb3b18f3 | ||
|
|
e6eef4b2c4 | ||
|
|
2ce577e36b | ||
|
|
c7c21e88f0 | ||
|
|
a89371b0b9 | ||
|
|
8f7f8761d1 | ||
|
|
a3ff300419 | ||
|
|
ea6e326b9c | ||
|
|
740f8759cc | ||
|
|
bb9cd4eb6a | ||
|
|
3a591fbf26 | ||
|
|
7754d5aef5 | ||
|
|
1d63d5db5f | ||
|
|
26640368e5 | ||
|
|
f23c2a83f2 | ||
|
|
61d54d1b19 | ||
|
|
10ea6144e0 | ||
|
|
292d971937 | ||
|
|
5c082472a1 | ||
|
|
0bedc71854 | ||
|
|
fe66edb825 | ||
|
|
bfa2df7036 | ||
|
|
0e52706a5b | ||
|
|
4c6166f989 | ||
|
|
8e99b3bd7f | ||
|
|
f6532de1fd | ||
|
|
001fde7694 | ||
|
|
b872eac2b9 | ||
|
|
038d0585cf | ||
|
|
b15428ea83 | ||
|
|
c9ad9d338c | ||
|
|
ae6bd9f6b0 | ||
|
|
866e437c05 | ||
|
|
784db053f2 | ||
|
|
ff1c6bd26e | ||
|
|
46fd63da5f | ||
|
|
420a1c79e1 | ||
|
|
6d39050e1e | ||
|
|
077bf2a409 | ||
|
|
1a3e38c018 | ||
|
|
e495f75cc2 | ||
|
|
e781449c36 | ||
|
|
7eb03ea198 | ||
|
|
bd34cd5613 | ||
|
|
c28d455f7b | ||
|
|
d250a7f502 | ||
|
|
b0f46326ca | ||
|
|
7aed2f122c | ||
|
|
5199a34d9b | ||
|
|
4c2ab6f878 | ||
|
|
a818dd9d19 | ||
|
|
efef03d944 | ||
|
|
33760f083a | ||
|
|
013802c877 | ||
|
|
3dcad3b3cd | ||
|
|
db065315ba | ||
|
|
a03f02789b | ||
|
|
ca4b9b43af | ||
|
|
77084930e9 | ||
|
|
fb77921b15 | ||
|
|
0d02dea20b | ||
|
|
86b47ba28b | ||
|
|
95ff0595c6 | ||
|
|
bbc56f35ec | ||
|
|
dfbbf87ba9 | ||
|
|
037b603603 | ||
|
|
bd33df09de | ||
|
|
6ca5b62112 | ||
|
|
e1a2057a1b | ||
|
|
ae9dbe0ca7 | ||
|
|
3b486425b0 | ||
|
|
b08bcf00ac | ||
|
|
0740e8b431 | ||
|
|
5532df880f | ||
|
|
2b11efc3ae | ||
|
|
a1d982ca76 | ||
|
|
03be7f1ee3 | ||
|
|
1ced4eaf10 | ||
|
|
4970167726 | ||
|
|
065ccbc4aa | ||
|
|
1feaa63813 | ||
|
|
8541c6be35 | ||
|
|
18654526c8 | ||
|
|
bd1f7f9492 | ||
|
|
40d75dd283 | ||
|
|
f5011365f1 | ||
|
|
ebbd046c92 | ||
|
|
b9979088ec | ||
|
|
bd0e53bd2e | ||
|
|
4b586f4105 | ||
|
|
0410119d5f | ||
|
|
c2f67398d0 | ||
|
|
81149344a5 | ||
|
|
e9b649b345 | ||
|
|
9c538f4237 | ||
|
|
c08a358a2b | ||
|
|
dbaef5e79b | ||
|
|
48a155a213 | ||
|
|
c69a9a77c6 | ||
|
|
ef79fa3898 | ||
|
|
f7ad602bfc | ||
|
|
ffd6049ba2 | ||
|
|
b3ad54c0c2 | ||
|
|
67eca3f58d | ||
|
|
0847bf560f | ||
|
|
8074565c3e | ||
|
|
24e81267b9 | ||
|
|
c0ffee3ff9 | ||
|
|
c0ffeeef26 | ||
|
|
22dbf90b7d | ||
|
|
40c68c9c83 | ||
|
|
b7f0f158ab | ||
|
|
d8f31046a9 | ||
|
|
6c6382ed69 | ||
|
|
53ba1372f9 | ||
|
|
57c9cf64eb | ||
|
|
e83b1e4159 | ||
|
|
3784020a8d | ||
|
|
1223444738 | ||
|
|
2bfeed2b00 | ||
|
|
04b800d768 | ||
|
|
f1d62978ef | ||
|
|
ffadc64f28 | ||
|
|
5302f40342 | ||
|
|
b0f8c2c03e | ||
|
|
37e40673ff | ||
|
|
fefee4c675 | ||
|
|
1aa7d3d2ba | ||
|
|
0db9c56f34 | ||
|
|
f4d134cd5c | ||
|
|
b667bcb48b | ||
|
|
2c14e0b2ba | ||
|
|
ca207ed93f | ||
|
|
ef13343591 | ||
|
|
adb40c41b7 |
@@ -1,2 +0,0 @@
|
||||
enabled:
|
||||
- cla
|
||||
@@ -0,0 +1,22 @@
|
||||
name: Contributor License Agreement (CLA)
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
cla:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
(github.event.issue.pull_request
|
||||
&& !github.event.issue.pull_request.merged_at
|
||||
&& contains(github.event.comment.body, 'signed')
|
||||
)
|
||||
|| (github.event.pull_request && !github.event.pull_request.merged)
|
||||
steps:
|
||||
- uses: Shopify/shopify-cla-action@v1
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
cla-token: ${{ secrets.CLA_TOKEN }}
|
||||
@@ -0,0 +1,39 @@
|
||||
name: Liquid
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
entry:
|
||||
- { ruby: 2.7, allowed-failure: false } # minimum supported
|
||||
- { ruby: 3.1, allowed-failure: false } # latest
|
||||
- { ruby: ruby-head, allowed-failure: true }
|
||||
name: test (${{ matrix.entry.ruby }})
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: ${{ matrix.entry.ruby }}
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: vendor/bundle
|
||||
key: ${{ runner.os }}-gems-${{ hashFiles('Gemfile') }}
|
||||
restore-keys: ${{ runner.os }}-gems-
|
||||
- run: bundle install --jobs=3 --retry=3 --path=vendor/bundle
|
||||
- run: bundle exec rake
|
||||
continue-on-error: ${{ matrix.entry.allowed-failure }}
|
||||
memory_profile:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 2.7
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: vendor/bundle
|
||||
key: ${{ runner.os }}-gems-${{ hashFiles('Gemfile') }}
|
||||
restore-keys: ${{ runner.os }}-gems-
|
||||
- run: bundle install --jobs=3 --retry=3 --path=vendor/bundle
|
||||
- run: bundle exec rake memory_profile:run
|
||||
File diff suppressed because it is too large
Load Diff
+11
-3
@@ -1,5 +1,7 @@
|
||||
inherit_gem:
|
||||
rubocop-shopify: rubocop.yml
|
||||
|
||||
inherit_from:
|
||||
- https://shopify.github.io/ruby-style-guide/rubocop.yml
|
||||
- .rubocop_todo.yml
|
||||
|
||||
require: rubocop-performance
|
||||
@@ -8,9 +10,15 @@ Performance:
|
||||
Enabled: true
|
||||
|
||||
AllCops:
|
||||
TargetRubyVersion: 2.7
|
||||
NewCops: disable
|
||||
Exclude:
|
||||
- 'vendor/bundle/**/*'
|
||||
|
||||
|
||||
Naming/MethodName:
|
||||
Exclude:
|
||||
- 'example/server/liquid_servlet.rb'
|
||||
- 'example/server/liquid_servlet.rb'
|
||||
|
||||
# Backport https://github.com/Shopify/ruby-style-guide/pull/258
|
||||
Layout/BeginEndAlignment:
|
||||
Enabled: true
|
||||
|
||||
+129
-43
@@ -1,56 +1,84 @@
|
||||
# This configuration was generated by
|
||||
# `rubocop --auto-gen-config`
|
||||
# on 2019-09-11 06:34:25 +1000 using RuboCop version 0.74.0.
|
||||
# on 2022-05-18 19:25:47 UTC using RuboCop version 1.29.1.
|
||||
# The point is for the user to remove these configuration records
|
||||
# one by one as the offenses are removed from the code base.
|
||||
# Note that changes in the inspected code, or installation of new
|
||||
# versions of RuboCop, may require this file to be generated again.
|
||||
|
||||
# Offense count: 2
|
||||
Lint/AmbiguousOperator:
|
||||
# Offense count: 1
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
# Configuration parameters: TreatCommentsAsGroupSeparators, ConsiderPunctuation, Include.
|
||||
# Include: **/*.gemspec
|
||||
Gemspec/OrderedDependencies:
|
||||
Exclude:
|
||||
- 'test/unit/condition_unit_test.rb'
|
||||
- 'liquid.gemspec'
|
||||
|
||||
# Offense count: 21
|
||||
# Configuration parameters: AllowSafeAssignment.
|
||||
Lint/AssignmentInCondition:
|
||||
# Offense count: 6
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
Layout/ClosingHeredocIndentation:
|
||||
Exclude:
|
||||
- 'test/integration/tags/for_tag_test.rb'
|
||||
|
||||
# Offense count: 34
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
Layout/EmptyLineAfterGuardClause:
|
||||
Exclude:
|
||||
- 'lib/liquid/block.rb'
|
||||
- 'lib/liquid/block_body.rb'
|
||||
- 'lib/liquid/context.rb'
|
||||
- 'lib/liquid/drop.rb'
|
||||
- 'lib/liquid/lexer.rb'
|
||||
- 'lib/liquid/parser.rb'
|
||||
- 'lib/liquid/profiler/hooks.rb'
|
||||
- 'lib/liquid/standardfilters.rb'
|
||||
- 'lib/liquid/tags/for.rb'
|
||||
- 'lib/liquid/tags/if.rb'
|
||||
- 'lib/liquid/tags/raw.rb'
|
||||
- 'lib/liquid/utils.rb'
|
||||
- 'lib/liquid/variable.rb'
|
||||
- 'performance/profile.rb'
|
||||
- 'test/test_helper.rb'
|
||||
- 'test/unit/tokenizer_unit_test.rb'
|
||||
- 'lib/liquid/variable_lookup.rb'
|
||||
- 'performance/shopify/money_filter.rb'
|
||||
- 'performance/shopify/paginate.rb'
|
||||
|
||||
# Offense count: 2
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle.
|
||||
# SupportedStyles: runtime_error, standard_error
|
||||
Lint/InheritException:
|
||||
# Offense count: 8
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
# Configuration parameters: AllowAliasSyntax, AllowedMethods.
|
||||
# AllowedMethods: alias_method, public, protected, private
|
||||
Layout/EmptyLinesAroundAttributeAccessor:
|
||||
Exclude:
|
||||
- 'lib/liquid/interrupts.rb'
|
||||
- 'lib/liquid/template.rb'
|
||||
- 'test/integration/filter_test.rb'
|
||||
- 'test/integration/tags/include_tag_test.rb'
|
||||
- 'test/unit/strainer_template_unit_test.rb'
|
||||
|
||||
# Offense count: 2
|
||||
Lint/UselessAssignment:
|
||||
# Offense count: 17
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
# Configuration parameters: EnforcedStyle, IndentationWidth.
|
||||
# SupportedStyles: aligned, indented
|
||||
Layout/LineEndStringConcatenationIndentation:
|
||||
Exclude:
|
||||
- 'performance/shopify/database.rb'
|
||||
- 'test/integration/tags/for_tag_test.rb'
|
||||
- 'test/integration/tags/increment_tag_test.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Configuration parameters: CheckForMethodsWithNoSideEffects.
|
||||
Lint/Void:
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
# Configuration parameters: EnforcedStyle, IndentationWidth.
|
||||
# SupportedStyles: aligned, indented
|
||||
Layout/MultilineOperationIndentation:
|
||||
Exclude:
|
||||
- 'lib/liquid/parse_context.rb'
|
||||
- 'lib/liquid/expression.rb'
|
||||
|
||||
# Offense count: 98
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
|
||||
# URISchemes: http, https
|
||||
Metrics/LineLength:
|
||||
Max: 294
|
||||
# Offense count: 9
|
||||
Lint/MissingSuper:
|
||||
Exclude:
|
||||
- 'lib/liquid/forloop_drop.rb'
|
||||
- 'lib/liquid/tablerowloop_drop.rb'
|
||||
- 'test/integration/assign_test.rb'
|
||||
- 'test/integration/context_test.rb'
|
||||
- 'test/integration/filter_test.rb'
|
||||
- 'test/integration/standard_filter_test.rb'
|
||||
- 'test/integration/tags/for_tag_test.rb'
|
||||
- 'test/integration/tags/table_row_test.rb'
|
||||
|
||||
# Offense count: 44
|
||||
Naming/ConstantName:
|
||||
@@ -63,7 +91,6 @@ Naming/ConstantName:
|
||||
- 'lib/liquid/tags/cycle.rb'
|
||||
- 'lib/liquid/tags/for.rb'
|
||||
- 'lib/liquid/tags/if.rb'
|
||||
- 'lib/liquid/tags/include.rb'
|
||||
- 'lib/liquid/tags/raw.rb'
|
||||
- 'lib/liquid/tags/table_row.rb'
|
||||
- 'lib/liquid/variable.rb'
|
||||
@@ -71,24 +98,83 @@ Naming/ConstantName:
|
||||
- 'performance/shopify/paginate.rb'
|
||||
- 'test/integration/tags/include_tag_test.rb'
|
||||
|
||||
# Offense count: 5
|
||||
# Offense count: 9
|
||||
# Configuration parameters: CheckIdentifiers, CheckConstants, CheckVariables, CheckStrings, CheckSymbols, CheckComments, CheckFilepaths, FlaggedTerms.
|
||||
Naming/InclusiveLanguage:
|
||||
Exclude:
|
||||
- 'lib/liquid/drop.rb'
|
||||
- 'lib/liquid/parse_context.rb'
|
||||
- 'test/integration/drop_test.rb'
|
||||
- 'test/integration/tags/if_else_tag_test.rb'
|
||||
|
||||
# Offense count: 2
|
||||
Style/ClassVars:
|
||||
Exclude:
|
||||
- 'lib/liquid/condition.rb'
|
||||
- 'lib/liquid/strainer.rb'
|
||||
- 'lib/liquid/template.rb'
|
||||
|
||||
# Offense count: 3
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
Style/ExplicitBlockArgument:
|
||||
Exclude:
|
||||
- 'test/integration/context_test.rb'
|
||||
- 'test/integration/tag/disableable_test.rb'
|
||||
- 'test/integration/tags/for_tag_test.rb'
|
||||
|
||||
# Offense count: 2982
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
# Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline.
|
||||
# SupportedStyles: single_quotes, double_quotes
|
||||
Style/StringLiterals:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 20
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
# Configuration parameters: EnforcedStyle.
|
||||
# SupportedStyles: single_quotes, double_quotes
|
||||
Style/StringLiteralsInInterpolation:
|
||||
Exclude:
|
||||
- 'lib/liquid/condition.rb'
|
||||
- 'lib/liquid/strainer_template.rb'
|
||||
- 'lib/liquid/tag/disableable.rb'
|
||||
- 'performance/shopify/shop_filter.rb'
|
||||
- 'performance/shopify/tag_filter.rb'
|
||||
|
||||
# Offense count: 6
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
# Configuration parameters: EnforcedStyleForMultiline.
|
||||
# SupportedStylesForMultiline: comma, consistent_comma, no_comma
|
||||
Style/TrailingCommaInArrayLiteral:
|
||||
Exclude:
|
||||
- 'example/server/example_servlet.rb'
|
||||
- 'lib/liquid/condition.rb'
|
||||
- 'test/integration/context_test.rb'
|
||||
- 'test/integration/standard_filter_test.rb'
|
||||
- 'test/unit/parse_tree_visitor_test.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Configuration parameters: AllowCoercion.
|
||||
Style/DateTime:
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
# Configuration parameters: EnforcedStyleForMultiline.
|
||||
# SupportedStylesForMultiline: comma, consistent_comma, no_comma
|
||||
Style/TrailingCommaInHashLiteral:
|
||||
Exclude:
|
||||
- 'test/unit/context_unit_test.rb'
|
||||
- 'lib/liquid/expression.rb'
|
||||
|
||||
# Offense count: 9
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: AllowAsExpressionSeparator.
|
||||
Style/Semicolon:
|
||||
# Offense count: 19
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
# Configuration parameters: EnforcedStyle, MinSize, WordRegex.
|
||||
# SupportedStyles: percent, brackets
|
||||
Style/WordArray:
|
||||
Exclude:
|
||||
- 'test/integration/error_handling_test.rb'
|
||||
- 'test/integration/template_test.rb'
|
||||
- 'test/unit/context_unit_test.rb'
|
||||
- 'lib/liquid/tags/if.rb'
|
||||
- 'liquid.gemspec'
|
||||
- 'test/integration/assign_test.rb'
|
||||
- 'test/integration/context_test.rb'
|
||||
- 'test/integration/drop_test.rb'
|
||||
- 'test/integration/standard_filter_test.rb'
|
||||
|
||||
# Offense count: 117
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns, IgnoredPatterns.
|
||||
# URISchemes: http, https
|
||||
Layout/LineLength:
|
||||
Max: 260
|
||||
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
language: ruby
|
||||
cache: bundler
|
||||
|
||||
rvm:
|
||||
- 2.4
|
||||
- 2.5
|
||||
- &latest_ruby 2.6
|
||||
- 2.7
|
||||
- ruby-head
|
||||
- jruby-head
|
||||
- truffleruby
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- rvm: *latest_ruby
|
||||
script: bundle exec rake memory_profile:run
|
||||
name: Profiling Memory Usage
|
||||
allow_failures:
|
||||
- rvm: ruby-head
|
||||
- rvm: jruby-head
|
||||
- rvm: truffleruby
|
||||
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- gh-pages
|
||||
- /.*-stable/
|
||||
|
||||
notifications:
|
||||
disable: true
|
||||
+4
-1
@@ -5,6 +5,7 @@
|
||||
* Bugfixes
|
||||
* Performance improvements
|
||||
* Features that are likely to be useful to the majority of Liquid users
|
||||
* Documentation updates that are concise and likely to be useful to the majority of Liquid users
|
||||
|
||||
## Things we won't merge
|
||||
|
||||
@@ -14,12 +15,14 @@
|
||||
* Features that can easily be implemented on top of Liquid (for example as a custom filter or custom filesystem)
|
||||
* Code that does not include tests
|
||||
* Code that breaks existing tests
|
||||
* Documentation changes that are verbose, incorrect or not important to most people (we want to keep it simple and easy to understand)
|
||||
|
||||
## Workflow
|
||||
|
||||
* [Sign the CLA](https://cla.shopify.com/) if you haven't already
|
||||
* Fork the Liquid repository
|
||||
* Create a new branch in your fork
|
||||
* For updating [Liquid documentation](https://shopify.github.io/liquid/), create it from `gh-pages` branch. (You can skip tests.)
|
||||
* If it makes sense, add tests for your code and/or run a performance benchmark
|
||||
* Make sure all tests pass (`bundle exec rake`)
|
||||
* Create a pull request
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ group :benchmark, :test do
|
||||
end
|
||||
|
||||
group :test do
|
||||
gem 'rubocop', '~> 0.74.0', require: false
|
||||
gem 'rubocop-shopify', '~> 2.7.0', require: false
|
||||
gem 'rubocop-performance', require: false
|
||||
|
||||
platform :mri, :truffleruby do
|
||||
|
||||
+89
@@ -1,5 +1,94 @@
|
||||
# Liquid Change Log
|
||||
|
||||
## 5.4.0 2022-07-29
|
||||
|
||||
### Breaking Changes
|
||||
* Drop support for end-of-life Ruby versions (2.5 and 2.6) (#1578) [Andy Waite]
|
||||
|
||||
### Features
|
||||
* Allow `#` to be used as an inline comment tag (#1498) [CP Clermont]
|
||||
|
||||
### Fixes
|
||||
* `PartialCache` now shares snippet cache with subcontexts by default (#1553) [Chris AtLee]
|
||||
* Hash registers no longer leak into subcontexts as static registers (#1564) [Chris AtLee]
|
||||
* Fix `ParseTreeVisitor` for `with` variable expressions in `Render` tag (#1596) [CP Clermont]
|
||||
|
||||
### Changed
|
||||
* Liquid::Context#registers now always returns a Liquid::Registers object, though supports the most used Hash functions for compatibility (#1553)
|
||||
|
||||
## 5.3.0 2022-03-22
|
||||
|
||||
### Fixes
|
||||
* StandardFilter: Fix missing @context on iterations (#1525) [Thierry Joyal]
|
||||
* Fix warning about block and default value in `static_registers.rb` (#1531) [Peter Zhu]
|
||||
|
||||
### Deprecation
|
||||
* Condition#evaluate to require mandatory context argument in Liquid 6.0.0 (#1527) [Thierry Joyal]
|
||||
|
||||
## 5.2.0 2022-03-01
|
||||
|
||||
### Features
|
||||
* Add `remove_last`, and `replace_last` filters (#1422) [Anders Hagbard]
|
||||
* Eagerly cache global filters (#1524) [Jean Boussier]
|
||||
|
||||
### Fixes
|
||||
* Fix some internal errors in filters from invalid input (#1476) [Dylan Thacker-Smith]
|
||||
* Allow dash in filter kwarg name for consistency with Liquid::C (#1518) [CP Clermont]
|
||||
|
||||
## 5.1.0 / 2021-09-09
|
||||
|
||||
### Features
|
||||
* Add `base64_encode`, `base64_decode`, `base64_url_safe_encode`, and `base64_url_safe_decode` filters (#1450) [Daniel Insley]
|
||||
* Introduce `to_liquid_value` in `Liquid::Drop` (#1441) [Michael Go]
|
||||
|
||||
### Fixes
|
||||
* Fix support for using a String subclass for the liquid source (#1421) [Dylan Thacker-Smith]
|
||||
* Add `ParseTreeVisitor` to `RangeLookup` (#1470) [CP Clermont]
|
||||
* Translate `RangeError` to `Liquid::Error` for `truncatewords` with large int (#1431) [Dylan Thacker-Smith]
|
||||
|
||||
## 5.0.1 / 2021-03-24
|
||||
|
||||
### Fixes
|
||||
* Add ParseTreeVisitor to Echo tag (#1414) [CP Clermont]
|
||||
* Test with ruby 3.0 as the latest ruby version (#1398) [Dylan Thacker-Smith]
|
||||
* Handle carriage return in newlines_to_br (#1391) [Unending]
|
||||
|
||||
### Performance Improvements
|
||||
* Use split limit in truncatewords (#1361) [Dylan Thacker-Smith]
|
||||
|
||||
## 5.0.0 / 2021-01-06
|
||||
|
||||
### Features
|
||||
* Add new `{% render %}` tag (#1122) [Samuel Doiron]
|
||||
* Add support for `as` in `{% render %}` and `{% include %}` (#1181) [Mike Angell]
|
||||
* Add `{% liquid %}` and `{% echo %}` tags (#1086) [Justin Li]
|
||||
* Add [usage tracking](README.md#usage-tracking) [Mike Angell]
|
||||
* Add `Tag.disable_tags` for disabling tags that prepend `Tag::Disableable` at render time (#1162, #1274, #1275) [Mike Angell]
|
||||
* Support using a profiler for multiple renders (#1365, #1366) [Dylan Thacker-Smith]
|
||||
|
||||
### Fixes
|
||||
* Fix catastrophic backtracking in `RANGES_REGEX` regular expression (#1357) [Dylan Thacker-Smith]
|
||||
* Make sure the for tag's limit and offset are integers (#1094) [David Cornu]
|
||||
* Invokable methods for enumerable reject include (#1151) [Thierry Joyal]
|
||||
* Allow `default` filter to handle `false` as value (#1144) [Mike Angell]
|
||||
* Fix render length resource limit so it doesn't multiply nested output (#1285) [Dylan Thacker-Smith]
|
||||
* Fix duplication of text in raw tags (#1304) [Peter Zhu]
|
||||
* Fix strict parsing of find variable with a name expression (#1317) [Dylan Thacker-Smith]
|
||||
* Use monotonic time to measure durations in Liquid::Profiler (#1362) [Dylan Thacker-Smith]
|
||||
|
||||
### Breaking Changes
|
||||
* Require Ruby >= 2.5 (#1131, #1310) [Mike Angell, Dylan Thacker-Smith]
|
||||
* Remove support for taint checking (#1268) [Dylan Thacker-Smith]
|
||||
* Split Strainer class into StrainerFactory and StrainerTemplate (#1208) [Thierry Joyal]
|
||||
* Remove handling of a nil context in the Strainer class (#1218) [Thierry Joyal]
|
||||
* Handle `BlockBody#blank?` at parse time (#1287) [Dylan Thacker-Smith]
|
||||
* Pass the tag markup and tokenizer to `Document#unknown_tag` (#1290) [Dylan Thacker-Smith]
|
||||
* And several internal changes
|
||||
|
||||
### Performance Improvements
|
||||
* Reduce allocations (#1073, #1091, #1115, #1099, #1117, #1141, #1322, #1341) [Richard Monette, Florian Weingarten, Ashwin Maroli]
|
||||
* Improve resources limits performance (#1093, #1323) [Florian Weingarten, Dylan Thacker-Smith]
|
||||
|
||||
## 4.0.3 / 2019-03-12
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
* [Contributing guidelines](CONTRIBUTING.md)
|
||||
* [Version history](History.md)
|
||||
* [Liquid documentation from Shopify](http://docs.shopify.com/themes/liquid-basics)
|
||||
* [Liquid documentation from Shopify](https://shopify.dev/api/liquid)
|
||||
* [Liquid Wiki at GitHub](https://github.com/Shopify/liquid/wiki)
|
||||
* [Website](http://liquidmarkup.org/)
|
||||
|
||||
@@ -56,20 +56,20 @@ For standard use you can just pass it the content of a file and call render with
|
||||
|
||||
Setting the error mode of Liquid lets you specify how strictly you want your templates to be interpreted.
|
||||
Normally the parser is very lax and will accept almost anything without error. Unfortunately this can make
|
||||
it very hard to debug and can lead to unexpected behaviour.
|
||||
it very hard to debug and can lead to unexpected behaviour.
|
||||
|
||||
Liquid also comes with a stricter parser that can be used when editing templates to give better error messages
|
||||
when templates are invalid. You can enable this new parser like this:
|
||||
|
||||
```ruby
|
||||
Liquid::Template.error_mode = :strict # Raises a SyntaxError when invalid syntax is used
|
||||
Liquid::Template.error_mode = :warn # Adds errors to template.errors but continues as normal
|
||||
Liquid::Template.error_mode = :warn # Adds strict errors to template.errors but continues as normal
|
||||
Liquid::Template.error_mode = :lax # The default mode, accepts almost anything.
|
||||
```
|
||||
|
||||
If you want to set the error mode only on specific templates you can pass `:error_mode` as an option to `parse`:
|
||||
```ruby
|
||||
Liquid::Template.parse(source, :error_mode => :strict)
|
||||
Liquid::Template.parse(source, error_mode: :strict)
|
||||
```
|
||||
This is useful for doing things like enabling strict mode only in the theme editor.
|
||||
|
||||
|
||||
@@ -5,13 +5,19 @@ require 'rake/testtask'
|
||||
$LOAD_PATH.unshift(File.expand_path("../lib", __FILE__))
|
||||
require "liquid/version"
|
||||
|
||||
task(default: [:test, :rubocop])
|
||||
task(default: [:test, "test:migrator_integration", :rubocop])
|
||||
|
||||
desc('run test suite with default parser')
|
||||
Rake::TestTask.new(:base_test) do |t|
|
||||
t.libs << '.' << 'lib' << 'test'
|
||||
t.libs << 'lib' << 'test'
|
||||
t.test_files = FileList['test/{integration,unit}/**/*_test.rb']
|
||||
t.verbose = false
|
||||
t.verbose = false
|
||||
end
|
||||
|
||||
Rake::TestTask.new(:integration_test) do |t|
|
||||
t.libs << 'lib' << 'test'
|
||||
t.test_files = FileList['test/integration/**/*_test.rb']
|
||||
t.verbose = false
|
||||
end
|
||||
|
||||
desc('run test suite with warn error mode')
|
||||
@@ -27,6 +33,23 @@ task :rubocop do
|
||||
end
|
||||
end
|
||||
|
||||
namespace :test do
|
||||
task :migrator_integration do
|
||||
ENV['LIQUID_MIGRATOR'] = '1'
|
||||
original_parse_mode = ENV['LIQUID_PARSER_MODE']
|
||||
begin
|
||||
Rake::Task['integration_test'].reenable
|
||||
["lax", "strict"].each do |parse_mode|
|
||||
ENV['LIQUID_PARSER_MODE'] = parse_mode
|
||||
Rake::Task['integration_test'].invoke
|
||||
Rake::Task['integration_test'].reenable
|
||||
end
|
||||
ensure
|
||||
ENV['LIQUID_PARSER_MODE'] = original_parse_mode
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
desc('runs test suite with both strict and lax parsers')
|
||||
task :test do
|
||||
ENV['LIQUID_PARSER_MODE'] = 'lax'
|
||||
@@ -40,12 +63,12 @@ task :test do
|
||||
ENV['LIQUID_C'] = '1'
|
||||
|
||||
ENV['LIQUID_PARSER_MODE'] = 'lax'
|
||||
Rake::Task['base_test'].reenable
|
||||
Rake::Task['base_test'].invoke
|
||||
Rake::Task['integration_test'].reenable
|
||||
Rake::Task['integration_test'].invoke
|
||||
|
||||
ENV['LIQUID_PARSER_MODE'] = 'strict'
|
||||
Rake::Task['base_test'].reenable
|
||||
Rake::Task['base_test'].invoke
|
||||
Rake::Task['integration_test'].reenable
|
||||
Rake::Task['integration_test'].invoke
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -12,16 +12,16 @@ class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet
|
||||
private
|
||||
|
||||
def handle(_type, req, res)
|
||||
@request = req
|
||||
@request = req
|
||||
@response = res
|
||||
|
||||
@request.path_info =~ /(\w+)\z/
|
||||
@action = Regexp.last_match(1) || 'index'
|
||||
@action = Regexp.last_match(1) || 'index'
|
||||
@assigns = send(@action) if respond_to?(@action)
|
||||
|
||||
@response['Content-Type'] = "text/html"
|
||||
@response.status = 200
|
||||
@response.body = Liquid::Template.parse(read_template).render(@assigns, filters: [ProductsFilter])
|
||||
@response.body = Liquid::Template.parse(read_template).render(@assigns, filters: [ProductsFilter])
|
||||
end
|
||||
|
||||
def read_template(filename = @action)
|
||||
|
||||
+11
-4
@@ -29,6 +29,7 @@ module Liquid
|
||||
WhitespaceControl = '-'
|
||||
TagStart = /\{\%/
|
||||
TagEnd = /\%\}/
|
||||
TagName = /#|\w+/
|
||||
VariableSignature = /\(?[\w\-\.\[\]]\)?/
|
||||
VariableSegment = /[\w\-]/
|
||||
VariableStart = /\{\{/
|
||||
@@ -36,12 +37,14 @@ module Liquid
|
||||
VariableIncompleteEnd = /\}\}?/
|
||||
QuotedString = /"[^"]*"|'[^']*'/
|
||||
QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o
|
||||
TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/o
|
||||
TagAttributes = /(\w[\w-]*)\s*\:\s*(#{QuotedFragment})/o
|
||||
AnyStartingTag = /#{TagStart}|#{VariableStart}/o
|
||||
PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om
|
||||
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om
|
||||
VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o
|
||||
|
||||
RAISE_EXCEPTION_LAMBDA = ->(_e) { raise }
|
||||
|
||||
singleton_class.send(:attr_accessor, :cache_classes)
|
||||
self.cache_classes = true
|
||||
end
|
||||
@@ -57,11 +60,15 @@ require 'liquid/forloop_drop'
|
||||
require 'liquid/extensions'
|
||||
require 'liquid/errors'
|
||||
require 'liquid/interrupts'
|
||||
require 'liquid/strainer'
|
||||
require 'liquid/strainer_template'
|
||||
require 'liquid/strainer_factory'
|
||||
require 'liquid/expression'
|
||||
require 'liquid/context'
|
||||
require 'liquid/parser_switching'
|
||||
require 'liquid/tag'
|
||||
require 'liquid/tag/disabler'
|
||||
require 'liquid/tag/disableable'
|
||||
require 'liquid/parse_context'
|
||||
require 'liquid/block'
|
||||
require 'liquid/block_body'
|
||||
require 'liquid/document'
|
||||
@@ -75,10 +82,10 @@ require 'liquid/standardfilters'
|
||||
require 'liquid/condition'
|
||||
require 'liquid/utils'
|
||||
require 'liquid/tokenizer'
|
||||
require 'liquid/parse_context'
|
||||
require 'liquid/partial_cache'
|
||||
require 'liquid/usage'
|
||||
require 'liquid/static_registers'
|
||||
require 'liquid/registers'
|
||||
require 'liquid/template_factory'
|
||||
|
||||
# Load all the tags of the standard library
|
||||
#
|
||||
|
||||
+35
-6
@@ -10,9 +10,10 @@ module Liquid
|
||||
end
|
||||
|
||||
def parse(tokens)
|
||||
@body = BlockBody.new
|
||||
@body = new_body
|
||||
while parse_body(@body, tokens)
|
||||
end
|
||||
@body.freeze
|
||||
end
|
||||
|
||||
# For backwards compatibility
|
||||
@@ -28,7 +29,12 @@ module Liquid
|
||||
@body.nodelist
|
||||
end
|
||||
|
||||
def unknown_tag(tag, _params, _tokens)
|
||||
def unknown_tag(tag_name, _markup, _tokenizer)
|
||||
Block.raise_unknown_tag(tag_name, block_name, block_delimiter, parse_context)
|
||||
end
|
||||
|
||||
# @api private
|
||||
def self.raise_unknown_tag(tag, block_name, block_delimiter, parse_context)
|
||||
if tag == 'else'
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_else",
|
||||
block_name: block_name)
|
||||
@@ -42,6 +48,10 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
def raise_tag_never_closed(block_name)
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_never_closed", block_name: block_name)
|
||||
end
|
||||
|
||||
def block_name
|
||||
@tag_name
|
||||
end
|
||||
@@ -50,8 +60,29 @@ module Liquid
|
||||
@block_delimiter ||= "end#{block_name}"
|
||||
end
|
||||
|
||||
protected
|
||||
def self.migrate_body(start_tag_name, tokenizer, parse_context)
|
||||
new_body, unknown_tag = BlockBody.migrate(tokenizer, parse_context)
|
||||
|
||||
raise SyntaxError unless unknown_tag
|
||||
|
||||
block_delimiter = "end#{start_tag_name}"
|
||||
if unknown_tag.tag_name == block_delimiter
|
||||
new_body << unknown_tag.replaced_markup("") # markup was ignored on end tags
|
||||
return [new_body, nil]
|
||||
end
|
||||
|
||||
# handle the delimiter tag in the caller
|
||||
[new_body, unknown_tag]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# @api public
|
||||
def new_body
|
||||
parse_context.new_block_body
|
||||
end
|
||||
|
||||
# @api public
|
||||
def parse_body(body, tokens)
|
||||
if parse_context.depth >= MAX_DEPTH
|
||||
raise StackLevelError, "Nesting too deep"
|
||||
@@ -62,9 +93,7 @@ module Liquid
|
||||
@blank &&= body.blank?
|
||||
|
||||
return false if end_tag_name == block_delimiter
|
||||
unless end_tag_name
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_never_closed", block_name: block_name)
|
||||
end
|
||||
raise_tag_never_closed(block_name) unless end_tag_name
|
||||
|
||||
# this tag is not registered with the system
|
||||
# pass it to the current block for special handling or error reporting
|
||||
|
||||
+257
-52
@@ -1,22 +1,26 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'English'
|
||||
|
||||
module Liquid
|
||||
class BlockBody
|
||||
LiquidTagToken = /\A\s*(\w+)\s*(.*?)\z/o
|
||||
FullToken = /\A#{TagStart}#{WhitespaceControl}?(\s*)(\w+)(\s*)(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
|
||||
ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
|
||||
LiquidTagToken = /\A\s*(#{TagName})\s*(.*?)\z/o
|
||||
FullToken = /\A#{TagStart}#{WhitespaceControl}?(\s*)(#{TagName})(\s*)(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
|
||||
ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
|
||||
WhitespaceOrNothing = /\A\s*\z/
|
||||
TAGSTART = "{%"
|
||||
VARSTART = "{{"
|
||||
TAGSTART = "{%"
|
||||
VARSTART = "{{"
|
||||
|
||||
attr_reader :nodelist
|
||||
|
||||
def initialize
|
||||
@nodelist = []
|
||||
@blank = true
|
||||
@blank = true
|
||||
end
|
||||
|
||||
def parse(tokenizer, parse_context, &block)
|
||||
raise FrozenError, "can't modify frozen Liquid::BlockBody" if frozen?
|
||||
|
||||
parse_context.line_number = tokenizer.line_number
|
||||
|
||||
if tokenizer.for_liquid_tag
|
||||
@@ -26,17 +30,32 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
def self.migrate(tokenizer, parse_context, &block)
|
||||
parse_context.line_number = tokenizer.line_number
|
||||
|
||||
if tokenizer.for_liquid_tag
|
||||
migrate_for_liquid_tag(tokenizer, parse_context, &block)
|
||||
else
|
||||
migrate_for_document(tokenizer, parse_context, &block)
|
||||
end
|
||||
end
|
||||
|
||||
def freeze
|
||||
@nodelist.freeze
|
||||
super
|
||||
end
|
||||
|
||||
private def parse_for_liquid_tag(tokenizer, parse_context)
|
||||
while token = tokenizer.shift
|
||||
unless token.empty? || token =~ WhitespaceOrNothing
|
||||
while (token = tokenizer.shift)
|
||||
unless token.empty? || token.match?(WhitespaceOrNothing)
|
||||
unless token =~ LiquidTagToken
|
||||
# line isn't empty but didn't match tag syntax, yield and let the
|
||||
# caller raise a syntax error
|
||||
return yield token, token
|
||||
end
|
||||
tag_name = Regexp.last_match(1)
|
||||
markup = Regexp.last_match(2)
|
||||
unless tag = registered_tags[tag_name]
|
||||
markup = Regexp.last_match(2)
|
||||
unless (tag = registered_tags[tag_name])
|
||||
# end parsing if we reach an unknown tag and let the caller decide
|
||||
# determine how to proceed
|
||||
return yield tag_name, markup
|
||||
@@ -51,17 +70,134 @@ module Liquid
|
||||
yield nil, nil
|
||||
end
|
||||
|
||||
class UnknownTagMigrator
|
||||
attr_reader :tag_name, :markup
|
||||
|
||||
def initialize(match:, markup_capture_number:, tag_name:, markup:)
|
||||
@match = match
|
||||
@tag_name = tag_name
|
||||
@markup = markup
|
||||
@markup_capture_number = markup_capture_number
|
||||
end
|
||||
|
||||
def original_tag_string
|
||||
@match[0]
|
||||
end
|
||||
|
||||
def replaced_markup(new_markup)
|
||||
Utils.match_capture_replace(@match, @markup_capture_number, new_markup)
|
||||
end
|
||||
end
|
||||
|
||||
private_class_method def self.migrate_for_liquid_tag(tokenizer, parse_context)
|
||||
result = +""
|
||||
while (token = tokenizer.shift)
|
||||
token += "\n" if tokenizer.more?
|
||||
if token.empty? || token.match?(WhitespaceOrNothing)
|
||||
result << token
|
||||
else
|
||||
# modified version of LiquidTagToken with following changes:
|
||||
# * TagName is optional, to continue supporting its absence in the comment tag
|
||||
# * trailing spaces is allowed to support the newline appended above and so the tag
|
||||
# migrate method doesn't have to handle trailing whitespace
|
||||
match = token.match(/\A\s*(#{TagName})?\s*(.*?)\s*\z/o)
|
||||
tag_name = match[1]
|
||||
markup = match[2]
|
||||
unless (tag = Template.tags[tag_name])
|
||||
# delegate handling of unknown tags to the caller, where a block tag may treat
|
||||
# it as an end tag or body delimiter.
|
||||
unknown_tag = UnknownTagMigrator.new(
|
||||
match: match, markup_capture_number: 2, tag_name: tag_name, markup: markup
|
||||
)
|
||||
return [result, unknown_tag]
|
||||
end
|
||||
new_markup, new_tag_body = tag.migrate(tag_name, markup, tokenizer, parse_context)
|
||||
result << Utils.match_capture_replace(match, 2, new_markup)
|
||||
result << new_tag_body.to_s
|
||||
end
|
||||
parse_context.line_number = tokenizer.line_number
|
||||
end
|
||||
|
||||
[result, nil]
|
||||
end
|
||||
|
||||
# @api private
|
||||
def self.unknown_tag_in_liquid_tag(tag, parse_context)
|
||||
Block.raise_unknown_tag(tag, 'liquid', '%}', parse_context)
|
||||
end
|
||||
|
||||
# @api private
|
||||
def self.raise_missing_tag_terminator(token, parse_context)
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_termination", token: token, tag_end: TagEnd.inspect)
|
||||
end
|
||||
|
||||
# @api private
|
||||
def self.raise_missing_variable_terminator(token, parse_context)
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.variable_termination", token: token, tag_end: VariableEnd.inspect)
|
||||
end
|
||||
|
||||
# @api private
|
||||
def self.render_node(context, output, node)
|
||||
node.render_to_output_buffer(context, output)
|
||||
rescue => exc
|
||||
blank_tag = !node.instance_of?(Variable) && node.blank?
|
||||
rescue_render_node(context, output, node.line_number, exc, blank_tag)
|
||||
end
|
||||
|
||||
# @api private
|
||||
def self.rescue_render_node(context, output, line_number, exc, blank_tag)
|
||||
case exc
|
||||
when MemoryError
|
||||
raise
|
||||
when UndefinedVariable, UndefinedDropMethod, UndefinedFilter
|
||||
context.handle_error(exc, line_number)
|
||||
else
|
||||
error_message = context.handle_error(exc, line_number)
|
||||
unless blank_tag # conditional for backwards compatibility
|
||||
output << error_message
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private def parse_liquid_tag(markup, parse_context)
|
||||
liquid_tag_tokenizer = parse_context.new_tokenizer(
|
||||
markup, start_line_number: parse_context.line_number, for_liquid_tag: true
|
||||
)
|
||||
parse_for_liquid_tag(liquid_tag_tokenizer, parse_context) do |end_tag_name, _end_tag_markup|
|
||||
if end_tag_name
|
||||
BlockBody.unknown_tag_in_liquid_tag(end_tag_name, parse_context)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private_class_method def self.migrate_liquid_tag(markup, parse_context)
|
||||
liquid_tag_tokenizer = parse_context.new_tokenizer(
|
||||
markup, start_line_number: parse_context.line_number, for_liquid_tag: true
|
||||
)
|
||||
result, unknown_tag = migrate_for_liquid_tag(liquid_tag_tokenizer, parse_context)
|
||||
raise SyntaxError if unknown_tag
|
||||
result
|
||||
end
|
||||
|
||||
private def handle_invalid_tag_token(token, parse_context)
|
||||
if token.end_with?('%}')
|
||||
yield token, token
|
||||
else
|
||||
BlockBody.raise_missing_tag_terminator(token, parse_context)
|
||||
end
|
||||
end
|
||||
|
||||
private def parse_for_document(tokenizer, parse_context, &block)
|
||||
while token = tokenizer.shift
|
||||
while (token = tokenizer.shift)
|
||||
next if token.empty?
|
||||
case
|
||||
when token.start_with?(TAGSTART)
|
||||
whitespace_handler(token, parse_context)
|
||||
unless token =~ FullToken
|
||||
raise_missing_tag_terminator(token, parse_context)
|
||||
return handle_invalid_tag_token(token, parse_context, &block)
|
||||
end
|
||||
tag_name = Regexp.last_match(2)
|
||||
markup = Regexp.last_match(4)
|
||||
markup = Regexp.last_match(4)
|
||||
|
||||
if parse_context.line_number
|
||||
# newlines inside the tag should increase the line number,
|
||||
@@ -70,11 +206,11 @@ module Liquid
|
||||
end
|
||||
|
||||
if tag_name == 'liquid'
|
||||
liquid_tag_tokenizer = Tokenizer.new(markup, line_number: parse_context.line_number, for_liquid_tag: true)
|
||||
next parse_for_liquid_tag(liquid_tag_tokenizer, parse_context, &block)
|
||||
parse_liquid_tag(markup, parse_context)
|
||||
next
|
||||
end
|
||||
|
||||
unless tag = registered_tags[tag_name]
|
||||
unless (tag = registered_tags[tag_name])
|
||||
# end parsing if we reach an unknown tag and let the caller decide
|
||||
# determine how to proceed
|
||||
return yield tag_name, markup
|
||||
@@ -92,7 +228,7 @@ module Liquid
|
||||
end
|
||||
parse_context.trim_whitespace = false
|
||||
@nodelist << token
|
||||
@blank &&= !!(token =~ WhitespaceOrNothing)
|
||||
@blank &&= token.match?(WhitespaceOrNothing)
|
||||
end
|
||||
parse_context.line_number = tokenizer.line_number
|
||||
end
|
||||
@@ -100,11 +236,67 @@ module Liquid
|
||||
yield nil, nil
|
||||
end
|
||||
|
||||
private_class_method def self.migrate_for_document(tokenizer, parse_context, &block)
|
||||
result = +""
|
||||
while (token = tokenizer.shift)
|
||||
next if token.empty?
|
||||
|
||||
case
|
||||
when token.start_with?(TAGSTART)
|
||||
raise SyntaxError unless token.end_with?('%}')
|
||||
# modified FullToken regex with optional tag name, to allow its absence in a comment tag
|
||||
match = token.match(/\A#{TagStart}#{WhitespaceControl}?(\s*)(#{TagName})?(\s*)(.*?)#{WhitespaceControl}?#{TagEnd}\z/om)
|
||||
tag_name = match[2]
|
||||
markup = match[4]
|
||||
|
||||
if parse_context.line_number
|
||||
# newlines inside the tag should increase the line number,
|
||||
# particularly important for multiline {% liquid %} tags
|
||||
parse_context.line_number += Regexp.last_match(1).count("\n") + Regexp.last_match(3).count("\n")
|
||||
end
|
||||
|
||||
if tag_name == 'liquid'
|
||||
new_markup = Utils.migrate_stripped(markup) do |stripped_markup|
|
||||
migrate_liquid_tag(stripped_markup, parse_context)
|
||||
end
|
||||
result << Utils.match_capture_replace(match, 4, new_markup)
|
||||
next
|
||||
end
|
||||
|
||||
unless (tag = Template.tags[tag_name])
|
||||
# delegate handling of unknown tags to the caller, where a block tag may treat
|
||||
# it as an end tag or body delimiter.
|
||||
unknown_tag = UnknownTagMigrator.new(
|
||||
match: match, markup_capture_number: 4, tag_name: tag_name, markup: markup
|
||||
)
|
||||
return [result, unknown_tag]
|
||||
end
|
||||
new_tag_body = nil
|
||||
new_markup = Utils.migrate_stripped(markup) do |stripped_markup|
|
||||
new_stripped_markup, new_tag_body = tag.migrate(tag_name, stripped_markup, tokenizer, parse_context)
|
||||
new_stripped_markup
|
||||
end
|
||||
result << Utils.match_capture_replace(match, 4, new_markup) << new_tag_body.to_s
|
||||
when token.start_with?(VARSTART)
|
||||
result << migrate_variable(token, parse_context)
|
||||
else
|
||||
result << token
|
||||
end
|
||||
parse_context.line_number = tokenizer.line_number
|
||||
end
|
||||
|
||||
[result, nil]
|
||||
end
|
||||
|
||||
def whitespace_handler(token, parse_context)
|
||||
if token[2] == WhitespaceControl
|
||||
previous_token = @nodelist.last
|
||||
if previous_token.is_a?(String)
|
||||
first_byte = previous_token.getbyte(0)
|
||||
previous_token.rstrip!
|
||||
if previous_token.empty? && parse_context[:bug_compatible_whitespace_trimming] && first_byte
|
||||
previous_token << first_byte
|
||||
end
|
||||
end
|
||||
end
|
||||
parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
|
||||
@@ -114,38 +306,49 @@ module Liquid
|
||||
@blank
|
||||
end
|
||||
|
||||
# Remove blank strings in the block body for a control flow tag (e.g. `if`, `for`, `case`, `unless`)
|
||||
# with a blank body.
|
||||
#
|
||||
# For example, in a conditional assignment like the following
|
||||
#
|
||||
# ```
|
||||
# {% if size > max_size %}
|
||||
# {% assign size = max_size %}
|
||||
# {% endif %}
|
||||
# ```
|
||||
#
|
||||
# we assume the intention wasn't to output the blank spaces in the `if` tag's block body, so this method
|
||||
# will remove them to reduce the render output size.
|
||||
#
|
||||
# Note that it is now preferred to use the `liquid` tag for this use case.
|
||||
def remove_blank_strings
|
||||
raise "remove_blank_strings only support being called on a blank block body" unless @blank
|
||||
@nodelist.reject! { |node| node.instance_of?(String) }
|
||||
end
|
||||
|
||||
def render(context)
|
||||
render_to_output_buffer(context, +'')
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
context.resource_limits.render_score += @nodelist.length
|
||||
freeze unless frozen?
|
||||
|
||||
context.resource_limits.increment_render_score(@nodelist.length)
|
||||
|
||||
idx = 0
|
||||
while node = @nodelist[idx]
|
||||
previous_output_size = output.bytesize
|
||||
|
||||
case node
|
||||
when String
|
||||
while (node = @nodelist[idx])
|
||||
if node.instance_of?(String)
|
||||
output << node
|
||||
when Variable
|
||||
else
|
||||
render_node(context, output, node)
|
||||
when Block
|
||||
render_node(context, node.blank? ? +'' : output, node)
|
||||
break if context.interrupt? # might have happened in a for-block
|
||||
when Continue, Break
|
||||
# If we get an Interrupt that means the block must stop processing. An
|
||||
# Interrupt is any command that stops block execution such as {% break %}
|
||||
# or {% continue %}
|
||||
context.push_interrupt(node.interrupt)
|
||||
break
|
||||
else # Other non-Block tags
|
||||
render_node(context, output, node)
|
||||
break if context.interrupt? # might have happened through an include
|
||||
# or {% continue %}. These tags may also occur through Block or Include tags.
|
||||
break if context.interrupt? # might have happened in a for-block
|
||||
end
|
||||
idx += 1
|
||||
|
||||
raise_if_resource_limits_reached(context, output.bytesize - previous_output_size)
|
||||
context.resource_limits.increment_write_score(output)
|
||||
end
|
||||
|
||||
output
|
||||
@@ -154,34 +357,36 @@ module Liquid
|
||||
private
|
||||
|
||||
def render_node(context, output, node)
|
||||
node.render_to_output_buffer(context, output)
|
||||
rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
|
||||
context.handle_error(e, node.line_number)
|
||||
rescue ::StandardError => e
|
||||
line_number = node.is_a?(String) ? nil : node.line_number
|
||||
output << context.handle_error(e, line_number)
|
||||
end
|
||||
|
||||
def raise_if_resource_limits_reached(context, length)
|
||||
context.resource_limits.render_length += length
|
||||
return unless context.resource_limits.reached?
|
||||
raise MemoryError, "Memory limits exceeded"
|
||||
BlockBody.render_node(context, output, node)
|
||||
end
|
||||
|
||||
def create_variable(token, parse_context)
|
||||
token.scan(ContentOfVariable) do |content|
|
||||
markup = content.first
|
||||
if token =~ ContentOfVariable
|
||||
markup = Regexp.last_match(1)
|
||||
return Variable.new(markup, parse_context)
|
||||
end
|
||||
raise_missing_variable_terminator(token, parse_context)
|
||||
BlockBody.raise_missing_variable_terminator(token, parse_context)
|
||||
end
|
||||
|
||||
private_class_method def self.migrate_variable(token, parse_context)
|
||||
match = token.match(ContentOfVariable)
|
||||
if match
|
||||
new_markup = Utils.migrate_stripped(match[1]) do |markup|
|
||||
Variable.migrate(markup, parse_context)
|
||||
end
|
||||
return Utils.match_capture_replace(match, 1, new_markup)
|
||||
end
|
||||
BlockBody.raise_missing_variable_terminator(token, parse_context)
|
||||
end
|
||||
|
||||
# @deprecated Use {.raise_missing_tag_terminator} instead
|
||||
def raise_missing_tag_terminator(token, parse_context)
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_termination", token: token, tag_end: TagEnd.inspect)
|
||||
BlockBody.raise_missing_tag_terminator(token, parse_context)
|
||||
end
|
||||
|
||||
# @deprecated Use {.raise_missing_variable_terminator} instead
|
||||
def raise_missing_variable_terminator(token, parse_context)
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.variable_termination", token: token, tag_end: VariableEnd.inspect)
|
||||
BlockBody.raise_missing_variable_terminator(token, parse_context)
|
||||
end
|
||||
|
||||
def registered_tags
|
||||
|
||||
+36
-11
@@ -8,7 +8,7 @@ module Liquid
|
||||
# c = Condition.new(1, '==', 1)
|
||||
# c.evaluate #=> true
|
||||
#
|
||||
class Condition #:nodoc:
|
||||
class Condition # :nodoc:
|
||||
@@operators = {
|
||||
'==' => ->(cond, left, right) { cond.send(:equal_variables, left, right) },
|
||||
'!=' => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
|
||||
@@ -27,22 +27,41 @@ module Liquid
|
||||
end,
|
||||
}
|
||||
|
||||
class MethodLiteral
|
||||
attr_reader :method_name, :to_s
|
||||
|
||||
def initialize(method_name, to_s)
|
||||
@method_name = method_name
|
||||
@to_s = to_s
|
||||
end
|
||||
end
|
||||
|
||||
@@method_literals = {
|
||||
'blank' => MethodLiteral.new(:blank?, '').freeze,
|
||||
'empty' => MethodLiteral.new(:empty?, '').freeze,
|
||||
}
|
||||
|
||||
def self.operators
|
||||
@@operators
|
||||
end
|
||||
|
||||
def self.parse_expression(parse_context, markup)
|
||||
@@method_literals[markup] || parse_context.parse_expression(markup)
|
||||
end
|
||||
|
||||
attr_reader :attachment, :child_condition
|
||||
attr_accessor :left, :operator, :right
|
||||
|
||||
def initialize(left = nil, operator = nil, right = nil)
|
||||
@left = left
|
||||
@left = left
|
||||
@operator = operator
|
||||
@right = right
|
||||
@child_relation = nil
|
||||
@right = right
|
||||
|
||||
@child_relation = nil
|
||||
@child_condition = nil
|
||||
end
|
||||
|
||||
def evaluate(context = Context.new)
|
||||
def evaluate(context = deprecated_default_context)
|
||||
condition = self
|
||||
result = nil
|
||||
loop do
|
||||
@@ -62,12 +81,12 @@ module Liquid
|
||||
end
|
||||
|
||||
def or(condition)
|
||||
@child_relation = :or
|
||||
@child_relation = :or
|
||||
@child_condition = condition
|
||||
end
|
||||
|
||||
def and(condition)
|
||||
@child_relation = :and
|
||||
@child_relation = :and
|
||||
@child_condition = condition
|
||||
end
|
||||
|
||||
@@ -90,7 +109,7 @@ module Liquid
|
||||
private
|
||||
|
||||
def equal_variables(left, right)
|
||||
if left.is_a?(Liquid::Expression::MethodLiteral)
|
||||
if left.is_a?(MethodLiteral)
|
||||
if right.respond_to?(left.method_name)
|
||||
return right.send(left.method_name)
|
||||
else
|
||||
@@ -98,7 +117,7 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
if right.is_a?(Liquid::Expression::MethodLiteral)
|
||||
if right.is_a?(MethodLiteral)
|
||||
if left.respond_to?(right.method_name)
|
||||
return left.send(right.method_name)
|
||||
else
|
||||
@@ -115,8 +134,8 @@ module Liquid
|
||||
# return this as the result.
|
||||
return context.evaluate(left) if op.nil?
|
||||
|
||||
left = context.evaluate(left)
|
||||
right = context.evaluate(right)
|
||||
left = Liquid::Utils.to_liquid_value(context.evaluate(left))
|
||||
right = Liquid::Utils.to_liquid_value(context.evaluate(right))
|
||||
|
||||
operation = self.class.operators[op] || raise(Liquid::ArgumentError, "Unknown operator #{op}")
|
||||
|
||||
@@ -131,6 +150,12 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
def deprecated_default_context
|
||||
warn("DEPRECATION WARNING: Condition#evaluate without a context argument is deprecated" \
|
||||
" and will be removed from Liquid 6.0.0.")
|
||||
Context.new
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
[
|
||||
|
||||
+43
-19
@@ -18,32 +18,40 @@ module Liquid
|
||||
attr_accessor :exception_renderer, :template_name, :partial, :global_filter, :strict_variables, :strict_filters
|
||||
|
||||
# rubocop:disable Metrics/ParameterLists
|
||||
def self.build(environments: {}, outer_scope: {}, registers: {}, rethrow_errors: false, resource_limits: nil, static_environments: {})
|
||||
new(environments, outer_scope, registers, rethrow_errors, resource_limits, static_environments)
|
||||
def self.build(environments: {}, outer_scope: {}, registers: {}, rethrow_errors: false, resource_limits: nil, static_environments: {}, &block)
|
||||
new(environments, outer_scope, registers, rethrow_errors, resource_limits, static_environments, &block)
|
||||
end
|
||||
|
||||
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil, static_environments = {})
|
||||
@environments = [environments]
|
||||
@environments.flatten!
|
||||
|
||||
@static_environments = [static_environments].flat_map(&:freeze).freeze
|
||||
@static_environments = [static_environments].flatten(1).freeze
|
||||
@scopes = [(outer_scope || {})]
|
||||
@registers = registers
|
||||
@registers = registers.is_a?(Registers) ? registers : Registers.new(registers)
|
||||
@errors = []
|
||||
@partial = false
|
||||
@strict_variables = false
|
||||
@resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
|
||||
@base_scope_depth = 0
|
||||
squash_instance_assigns_with_environments
|
||||
@interrupts = []
|
||||
@filters = []
|
||||
@global_filter = nil
|
||||
@disabled_tags = {}
|
||||
|
||||
@registers.static[:cached_partials] ||= {}
|
||||
@registers.static[:file_system] ||= Liquid::Template.file_system
|
||||
@registers.static[:template_factory] ||= Liquid::TemplateFactory.new
|
||||
|
||||
self.exception_renderer = Template.default_exception_renderer
|
||||
if rethrow_errors
|
||||
self.exception_renderer = ->(_e) { raise }
|
||||
self.exception_renderer = Liquid::RAISE_EXCEPTION_LAMBDA
|
||||
end
|
||||
|
||||
@interrupts = []
|
||||
@filters = []
|
||||
@global_filter = nil
|
||||
yield self if block_given?
|
||||
|
||||
# Do this last, since it could result in this object being passed to a Proc in the environment
|
||||
squash_instance_assigns_with_environments
|
||||
end
|
||||
# rubocop:enable Metrics/ParameterLists
|
||||
|
||||
@@ -52,7 +60,7 @@ module Liquid
|
||||
end
|
||||
|
||||
def strainer
|
||||
@strainer ||= Strainer.create(self, @filters)
|
||||
@strainer ||= StrainerFactory.create(self, @filters)
|
||||
end
|
||||
|
||||
# Adds filters to this context.
|
||||
@@ -87,7 +95,7 @@ module Liquid
|
||||
def handle_error(e, line_number = nil)
|
||||
e = internal_error unless e.is_a?(Liquid::Error)
|
||||
e.template_name ||= template_name
|
||||
e.line_number ||= line_number
|
||||
e.line_number ||= line_number
|
||||
errors.push(e)
|
||||
exception_renderer.call(e).to_s
|
||||
end
|
||||
@@ -120,7 +128,7 @@ module Liquid
|
||||
# context['var'] = 'hi'
|
||||
# end
|
||||
#
|
||||
# context['var] #=> nil
|
||||
# context['var'] #=> nil
|
||||
def stack(new_scope = {})
|
||||
push(new_scope)
|
||||
yield
|
||||
@@ -133,17 +141,18 @@ module Liquid
|
||||
def new_isolated_subcontext
|
||||
check_overflow
|
||||
|
||||
Context.build(
|
||||
self.class.build(
|
||||
resource_limits: resource_limits,
|
||||
static_environments: static_environments,
|
||||
registers: StaticRegisters.new(registers)
|
||||
registers: Registers.new(registers)
|
||||
).tap do |subcontext|
|
||||
subcontext.base_scope_depth = base_scope_depth + 1
|
||||
subcontext.base_scope_depth = base_scope_depth + 1
|
||||
subcontext.exception_renderer = exception_renderer
|
||||
subcontext.filters = @filters
|
||||
subcontext.filters = @filters
|
||||
subcontext.strainer = nil
|
||||
subcontext.errors = errors
|
||||
subcontext.errors = errors
|
||||
subcontext.warnings = warnings
|
||||
subcontext.disabled_tags = @disabled_tags
|
||||
end
|
||||
end
|
||||
|
||||
@@ -188,7 +197,7 @@ module Liquid
|
||||
try_variable_find_in_environments(key, raise_on_not_found: raise_on_not_found)
|
||||
end
|
||||
|
||||
variable = variable.to_liquid
|
||||
variable = variable.to_liquid
|
||||
variable.context = self if variable.respond_to?(:context=)
|
||||
|
||||
variable
|
||||
@@ -208,9 +217,24 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
def with_disabled_tags(tag_names)
|
||||
tag_names.each do |name|
|
||||
@disabled_tags[name] = @disabled_tags.fetch(name, 0) + 1
|
||||
end
|
||||
yield
|
||||
ensure
|
||||
tag_names.each do |name|
|
||||
@disabled_tags[name] -= 1
|
||||
end
|
||||
end
|
||||
|
||||
def tag_disabled?(tag_name)
|
||||
@disabled_tags.fetch(tag_name, 0) > 0
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
attr_writer :base_scope_depth, :warnings, :errors, :strainer, :filters
|
||||
attr_writer :base_scope_depth, :warnings, :errors, :strainer, :filters, :disabled_tags
|
||||
|
||||
private
|
||||
|
||||
|
||||
+52
-6
@@ -1,23 +1,44 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Document < BlockBody
|
||||
class Document
|
||||
def self.parse(tokens, parse_context)
|
||||
doc = new
|
||||
doc = new(parse_context)
|
||||
doc.parse(tokens, parse_context)
|
||||
doc
|
||||
end
|
||||
|
||||
def parse(tokens, parse_context)
|
||||
super do |end_tag_name, _end_tag_params|
|
||||
unknown_tag(end_tag_name, parse_context) if end_tag_name
|
||||
attr_reader :parse_context, :body
|
||||
|
||||
def initialize(parse_context)
|
||||
@parse_context = parse_context
|
||||
@body = new_body
|
||||
end
|
||||
|
||||
def nodelist
|
||||
@body.nodelist
|
||||
end
|
||||
|
||||
def parse(tokenizer, parse_context)
|
||||
while parse_body(tokenizer)
|
||||
end
|
||||
@body.freeze
|
||||
rescue SyntaxError => e
|
||||
e.line_number ||= parse_context.line_number
|
||||
raise
|
||||
end
|
||||
|
||||
def unknown_tag(tag, parse_context)
|
||||
def self.migrate(tokenizer, parse_context)
|
||||
new_body, unknown_tag = BlockBody.migrate(tokenizer, parse_context)
|
||||
raise SyntaxError if unknown_tag
|
||||
|
||||
new_body
|
||||
rescue SyntaxError => e
|
||||
e.line_number ||= parse_context.line_number
|
||||
raise
|
||||
end
|
||||
|
||||
def unknown_tag(tag, _markup, _tokenizer)
|
||||
case tag
|
||||
when 'else', 'end'
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_outer_tag", tag: tag)
|
||||
@@ -25,5 +46,30 @@ module Liquid
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag", tag: tag)
|
||||
end
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
@body.render_to_output_buffer(context, output)
|
||||
end
|
||||
|
||||
def render(context)
|
||||
render_to_output_buffer(context, +'')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def new_body
|
||||
parse_context.new_block_body
|
||||
end
|
||||
|
||||
def parse_body(tokenizer)
|
||||
@body.parse(tokenizer, parse_context) do |unknown_tag_name, unknown_tag_markup|
|
||||
if unknown_tag_name
|
||||
unknown_tag(unknown_tag_name, unknown_tag_markup, tokenizer)
|
||||
true
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+13
-13
@@ -40,19 +40,19 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
ArgumentError = Class.new(Error)
|
||||
ContextError = Class.new(Error)
|
||||
FileSystemError = Class.new(Error)
|
||||
StandardError = Class.new(Error)
|
||||
SyntaxError = Class.new(Error)
|
||||
StackLevelError = Class.new(Error)
|
||||
TaintedError = Class.new(Error)
|
||||
MemoryError = Class.new(Error)
|
||||
ZeroDivisionError = Class.new(Error)
|
||||
FloatDomainError = Class.new(Error)
|
||||
UndefinedVariable = Class.new(Error)
|
||||
ArgumentError = Class.new(Error)
|
||||
ContextError = Class.new(Error)
|
||||
FileSystemError = Class.new(Error)
|
||||
StandardError = Class.new(Error)
|
||||
SyntaxError = Class.new(Error)
|
||||
StackLevelError = Class.new(Error)
|
||||
MemoryError = Class.new(Error)
|
||||
ZeroDivisionError = Class.new(Error)
|
||||
FloatDomainError = Class.new(Error)
|
||||
UndefinedVariable = Class.new(Error)
|
||||
UndefinedDropMethod = Class.new(Error)
|
||||
UndefinedFilter = Class.new(Error)
|
||||
UndefinedFilter = Class.new(Error)
|
||||
MethodOverrideError = Class.new(Error)
|
||||
InternalError = Class.new(Error)
|
||||
DisabledError = Class.new(Error)
|
||||
InternalError = Class.new(Error)
|
||||
end
|
||||
|
||||
+54
-29
@@ -2,50 +2,75 @@
|
||||
|
||||
module Liquid
|
||||
class Expression
|
||||
class MethodLiteral
|
||||
attr_reader :method_name, :to_s
|
||||
|
||||
def initialize(method_name, to_s)
|
||||
@method_name = method_name
|
||||
@to_s = to_s
|
||||
end
|
||||
|
||||
def to_liquid
|
||||
to_s
|
||||
end
|
||||
end
|
||||
|
||||
LITERALS = {
|
||||
nil => nil, 'nil' => nil, 'null' => nil, '' => nil,
|
||||
'true' => true,
|
||||
'false' => false,
|
||||
'blank' => MethodLiteral.new(:blank?, '').freeze,
|
||||
'empty' => MethodLiteral.new(:empty?, '').freeze
|
||||
'blank' => '',
|
||||
'empty' => ''
|
||||
}.freeze
|
||||
|
||||
SINGLE_QUOTED_STRING = /\A'(.*)'\z/m
|
||||
DOUBLE_QUOTED_STRING = /\A"(.*)"\z/m
|
||||
INTEGERS_REGEX = /\A(-?\d+)\z/
|
||||
FLOATS_REGEX = /\A(-?\d[\d\.]+)\z/
|
||||
RANGES_REGEX = /\A\((\S+)\.\.(\S+)\)\z/
|
||||
|
||||
# Use an atomic group (?>...) to avoid pathological backtracing from
|
||||
# malicious input as described in https://github.com/Shopify/liquid/issues/1357
|
||||
RANGES_REGEX = /\A\(\s*(?>(\S+)\s*\.\.)\s*(\S+)\s*\)\z/
|
||||
|
||||
def self.parse(markup)
|
||||
if LITERALS.key?(markup)
|
||||
LITERALS[markup]
|
||||
return nil unless markup
|
||||
|
||||
markup = markup.strip
|
||||
if (markup.start_with?('"') && markup.end_with?('"')) ||
|
||||
(markup.start_with?("'") && markup.end_with?("'"))
|
||||
return markup[1..-2]
|
||||
end
|
||||
|
||||
case markup
|
||||
when INTEGERS_REGEX
|
||||
Regexp.last_match(1).to_i
|
||||
when RANGES_REGEX
|
||||
RangeLookup.parse(Regexp.last_match(1), Regexp.last_match(2))
|
||||
when FLOATS_REGEX
|
||||
Regexp.last_match(1).to_f
|
||||
else
|
||||
case markup
|
||||
when SINGLE_QUOTED_STRING, DOUBLE_QUOTED_STRING
|
||||
Regexp.last_match(1)
|
||||
when INTEGERS_REGEX
|
||||
Regexp.last_match(1).to_i
|
||||
when RANGES_REGEX
|
||||
RangeLookup.parse(Regexp.last_match(1), Regexp.last_match(2))
|
||||
when FLOATS_REGEX
|
||||
Regexp.last_match(1).to_f
|
||||
if LITERALS.key?(markup)
|
||||
LITERALS[markup]
|
||||
else
|
||||
VariableLookup.parse(markup)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.lax_migrate(markup)
|
||||
Utils.migrate_stripped(markup) do |markup|
|
||||
raise ArgumentError, "unexpected empty expression" if markup.empty?
|
||||
|
||||
if (markup.start_with?('"') && markup.end_with?('"')) ||
|
||||
(markup.start_with?("'") && markup.end_with?("'"))
|
||||
markup
|
||||
else
|
||||
case markup
|
||||
when INTEGERS_REGEX
|
||||
markup
|
||||
when RANGES_REGEX
|
||||
match = Regexp.last_match
|
||||
new_start, new_end = RangeLookup.lax_migrate(match[1], match[2])
|
||||
Utils.match_captures_replace(match, 1 => new_start, 2 => new_end)
|
||||
when FLOATS_REGEX
|
||||
# lax parser allowed multiple periods, but the second period and following characters were ignored
|
||||
new_markup = markup.slice(/\A(-?\d+\.\d*)/)
|
||||
new_markup << "0" if new_markup.end_with?(".")
|
||||
new_markup
|
||||
else
|
||||
if LITERALS.key?(markup)
|
||||
markup
|
||||
else
|
||||
VariableLookup.lax_migrate(markup)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -47,7 +47,7 @@ module Liquid
|
||||
attr_accessor :root
|
||||
|
||||
def initialize(root, pattern = "_%s.liquid")
|
||||
@root = root
|
||||
@root = root
|
||||
@pattern = pattern
|
||||
end
|
||||
|
||||
@@ -59,7 +59,7 @@ module Liquid
|
||||
end
|
||||
|
||||
def full_path(template_path)
|
||||
raise FileSystemError, "Illegal template name '#{template_path}'" unless template_path =~ %r{\A[^./][a-zA-Z0-9_/]+\z}
|
||||
raise FileSystemError, "Illegal template name '#{template_path}'" unless %r{\A[^./][a-zA-Z0-9_/]+\z}.match?(template_path)
|
||||
|
||||
full_path = if template_path.include?('/')
|
||||
File.join(root, File.dirname(template_path), @pattern % File.basename(template_path))
|
||||
|
||||
@@ -1,36 +1,81 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# @liquid_public_docs
|
||||
# @liquid_type object
|
||||
# @liquid_name forloop
|
||||
# @liquid_summary
|
||||
# Information about a parent [`for` loop](/api/liquid/tags/for).
|
||||
class ForloopDrop < Drop
|
||||
def initialize(name, length, parentloop)
|
||||
@name = name
|
||||
@length = length
|
||||
@name = name
|
||||
@length = length
|
||||
@parentloop = parentloop
|
||||
@index = 0
|
||||
@index = 0
|
||||
end
|
||||
|
||||
attr_reader :name, :length, :parentloop
|
||||
# @liquid_public_docs
|
||||
# @liquid_name length
|
||||
# @liquid_summary
|
||||
# The total number of iterations in the loop.
|
||||
# @liquid_return [number]
|
||||
attr_reader :length
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_name parentloop
|
||||
# @liquid_summary
|
||||
# The parent `forloop` object.
|
||||
# @liquid_description
|
||||
# If the current `for` loop isn't nested inside another `for` loop, then `nil` is returned.
|
||||
# @liquid_return [forloop]
|
||||
attr_reader :parentloop
|
||||
|
||||
attr_reader :name
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# The 1-based index of the current iteration.
|
||||
# @liquid_return [number]
|
||||
def index
|
||||
@index + 1
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# The 0-based index of the current iteration.
|
||||
# @liquid_return [number]
|
||||
def index0
|
||||
@index
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# The 1-based index of the current iteration, in reverse order.
|
||||
# @liquid_return [number]
|
||||
def rindex
|
||||
@length - @index
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# The 0-based index of the current iteration, in reverse order.
|
||||
# @liquid_return [number]
|
||||
def rindex0
|
||||
@length - @index - 1
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# Returns `true` if the current iteration is the first. Returns `false` if not.
|
||||
# @liquid_return [boolean]
|
||||
def first
|
||||
@index == 0
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# Returns `true` if the current iteration is the last. Returns `false` if not.
|
||||
# @liquid_return [boolean]
|
||||
def last
|
||||
@index == @length - 1
|
||||
end
|
||||
|
||||
+17
-12
@@ -15,12 +15,13 @@ module Liquid
|
||||
'?' => :question,
|
||||
'-' => :dash,
|
||||
}.freeze
|
||||
IDENTIFIER = /[a-zA-Z_][\w-]*\??/
|
||||
IDENTIFIER = /[a-zA-Z_][\w-]*\??/
|
||||
SINGLE_STRING_LITERAL = /'[^\']*'/
|
||||
DOUBLE_STRING_LITERAL = /"[^\"]*"/
|
||||
NUMBER_LITERAL = /-?\d+(\.\d+)?/
|
||||
DOTDOT = /\.\./
|
||||
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/
|
||||
STRING_LITERAL = Regexp.union(SINGLE_STRING_LITERAL, DOUBLE_STRING_LITERAL)
|
||||
NUMBER_LITERAL = /-?\d+(\.\d+)?/
|
||||
DOTDOT = /\.\./
|
||||
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/
|
||||
WHITESPACE_OR_NOTHING = /\s*/
|
||||
|
||||
def initialize(input)
|
||||
@@ -33,15 +34,19 @@ module Liquid
|
||||
until @ss.eos?
|
||||
@ss.skip(WHITESPACE_OR_NOTHING)
|
||||
break if @ss.eos?
|
||||
tok = if t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t]
|
||||
elsif t = @ss.scan(SINGLE_STRING_LITERAL) then [:string, t]
|
||||
elsif t = @ss.scan(DOUBLE_STRING_LITERAL) then [:string, t]
|
||||
elsif t = @ss.scan(NUMBER_LITERAL) then [:number, t]
|
||||
elsif t = @ss.scan(IDENTIFIER) then [:id, t]
|
||||
elsif t = @ss.scan(DOTDOT) then [:dotdot, t]
|
||||
tok = if (t = @ss.scan(COMPARISON_OPERATOR))
|
||||
[:comparison, t]
|
||||
elsif (t = @ss.scan(STRING_LITERAL))
|
||||
[:string, t]
|
||||
elsif (t = @ss.scan(NUMBER_LITERAL))
|
||||
[:number, t]
|
||||
elsif (t = @ss.scan(IDENTIFIER))
|
||||
[:id, t]
|
||||
elsif (t = @ss.scan(DOTDOT))
|
||||
[:dotdot, t]
|
||||
else
|
||||
c = @ss.getch
|
||||
if s = SPECIALS[c]
|
||||
c = @ss.getch
|
||||
if (s = SPECIALS[c])
|
||||
[s, c]
|
||||
else
|
||||
raise SyntaxError, "Unexpected character #{c}"
|
||||
|
||||
@@ -13,15 +13,17 @@
|
||||
for_invalid_attribute: "Invalid attribute in for loop. Valid attributes are limit and offset"
|
||||
if: "Syntax Error in tag 'if' - Valid syntax: if [expression]"
|
||||
include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]"
|
||||
unknown_tag: "Unknown tag '%{tag}'"
|
||||
inline_comment_invalid: "Syntax error in tag '#' - Each line of comments must be prefixed by the '#' character"
|
||||
invalid_delimiter: "'%{tag}' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}"
|
||||
render: "Syntax error in tag 'render' - Template name must be a quoted string"
|
||||
table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
|
||||
tag_never_closed: "'%{block_name}' tag was never closed"
|
||||
tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"
|
||||
unexpected_else: "%{block_name} tag does not expect 'else' tag"
|
||||
unexpected_outer_tag: "Unexpected outer '%{tag}' tag"
|
||||
tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"
|
||||
unknown_tag: "Unknown tag '%{tag}'"
|
||||
variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}"
|
||||
tag_never_closed: "'%{block_name}' tag was never closed"
|
||||
meta_syntax_error: "Liquid syntax error: #{e.message}"
|
||||
table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
|
||||
render: "Syntax error in tag 'render' - Template name must be a quoted string"
|
||||
argument:
|
||||
include: "Argument error in tag 'include' - Illegal template name"
|
||||
disabled:
|
||||
tag: "usage is not allowed in this context"
|
||||
|
||||
@@ -5,11 +5,16 @@ module Liquid
|
||||
attr_accessor :locale, :line_number, :trim_whitespace, :depth
|
||||
attr_reader :partial, :warnings, :error_mode
|
||||
|
||||
# @api private
|
||||
attr_writer :error_mode
|
||||
|
||||
def initialize(options = {})
|
||||
@template_options = options ? options.dup : {}
|
||||
@locale = @template_options[:locale] ||= I18n.new
|
||||
|
||||
@locale = @template_options[:locale] ||= I18n.new
|
||||
@warnings = []
|
||||
self.depth = 0
|
||||
|
||||
self.depth = 0
|
||||
self.partial = false
|
||||
end
|
||||
|
||||
@@ -17,11 +22,23 @@ module Liquid
|
||||
@options[option_key]
|
||||
end
|
||||
|
||||
def new_block_body
|
||||
Liquid::BlockBody.new
|
||||
end
|
||||
|
||||
def new_tokenizer(markup, start_line_number: nil, for_liquid_tag: false)
|
||||
Tokenizer.new(markup, line_number: start_line_number, for_liquid_tag: for_liquid_tag)
|
||||
end
|
||||
|
||||
def parse_expression(markup)
|
||||
Expression.parse(markup)
|
||||
end
|
||||
|
||||
def partial=(value)
|
||||
@partial = value
|
||||
@options = value ? partial_options : @template_options
|
||||
|
||||
@error_mode = @options[:error_mode] || Template.error_mode
|
||||
value
|
||||
end
|
||||
|
||||
def partial_options
|
||||
|
||||
@@ -11,7 +11,7 @@ module Liquid
|
||||
end
|
||||
|
||||
def initialize(node, callbacks)
|
||||
@node = node
|
||||
@node = node
|
||||
@callbacks = callbacks
|
||||
end
|
||||
|
||||
|
||||
+26
-19
@@ -3,9 +3,9 @@
|
||||
module Liquid
|
||||
class Parser
|
||||
def initialize(input)
|
||||
l = Lexer.new(input)
|
||||
l = Lexer.new(input)
|
||||
@tokens = l.tokenize
|
||||
@p = 0 # pointer to current location
|
||||
@p = 0 # pointer to current location
|
||||
end
|
||||
|
||||
def jump(point)
|
||||
@@ -46,16 +46,20 @@ module Liquid
|
||||
tok[0] == type
|
||||
end
|
||||
|
||||
SINGLE_TOKEN_EXPRESSION_TYPES = [:string, :number].freeze
|
||||
private_constant :SINGLE_TOKEN_EXPRESSION_TYPES
|
||||
|
||||
def expression
|
||||
token = @tokens[@p]
|
||||
if token[0] == :id
|
||||
variable_signature
|
||||
elsif SINGLE_TOKEN_EXPRESSION_TYPES.include?(token[0])
|
||||
case token[0]
|
||||
when :id
|
||||
str = consume
|
||||
str << variable_lookups
|
||||
when :open_square
|
||||
str = consume
|
||||
str << expression
|
||||
str << consume(:close_square)
|
||||
str << variable_lookups
|
||||
when :string, :number
|
||||
consume
|
||||
elsif token.first == :open_round
|
||||
when :open_round
|
||||
consume
|
||||
first = expression
|
||||
consume(:dotdot)
|
||||
@@ -78,16 +82,19 @@ module Liquid
|
||||
str
|
||||
end
|
||||
|
||||
def variable_signature
|
||||
str = consume(:id)
|
||||
while look(:open_square)
|
||||
str << consume
|
||||
str << expression
|
||||
str << consume(:close_square)
|
||||
end
|
||||
if look(:dot)
|
||||
str << consume
|
||||
str << variable_signature
|
||||
def variable_lookups
|
||||
str = +""
|
||||
loop do
|
||||
if look(:open_square)
|
||||
str << consume
|
||||
str << expression
|
||||
str << consume(:close_square)
|
||||
elsif look(:dot)
|
||||
str << consume
|
||||
str << consume(:id)
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
str
|
||||
end
|
||||
|
||||
@@ -2,16 +2,52 @@
|
||||
|
||||
module Liquid
|
||||
module ParserSwitching
|
||||
module ClassMethods
|
||||
def migrate_with_selected_parser(tag_name, markup, tokenizer, parse_context)
|
||||
case parse_context.error_mode
|
||||
when :strict then strict_migrate(tag_name, markup, tokenizer, parse_context)
|
||||
when :lax then lax_migrate(tag_name, markup, tokenizer, parse_context)
|
||||
when :warn
|
||||
begin
|
||||
parse_context.error_mode = :strict
|
||||
begin
|
||||
# Use exception side effect to conditionally branch to lax migration
|
||||
parse(tag_name, markup, tokenizer, parse_context)
|
||||
ensure
|
||||
parse_context.error_mode = :warn
|
||||
end
|
||||
|
||||
strict_migrate(tag_name, markup, tokenizer, parse_context)
|
||||
rescue SyntaxError => e
|
||||
parse_context.warnings << e
|
||||
lax_migrate(tag_name, markup, tokenizer, parse_context)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def strict_parse_with_error_mode_fallback(markup)
|
||||
strict_parse_with_error_context(markup)
|
||||
rescue SyntaxError => e
|
||||
case parse_context.error_mode
|
||||
when :strict
|
||||
raise
|
||||
when :warn
|
||||
parse_context.warnings << e
|
||||
end
|
||||
lax_parse(markup)
|
||||
end
|
||||
|
||||
def parse_with_selected_parser(markup)
|
||||
case parse_context.error_mode
|
||||
when :strict then strict_parse_with_error_context(markup)
|
||||
when :lax then lax_parse(markup)
|
||||
when :warn
|
||||
begin
|
||||
return strict_parse_with_error_context(markup)
|
||||
strict_parse_with_error_context(markup)
|
||||
rescue SyntaxError => e
|
||||
parse_context.warnings << e
|
||||
return lax_parse(markup)
|
||||
lax_parse(markup)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -21,7 +57,7 @@ module Liquid
|
||||
def strict_parse_with_error_context(markup)
|
||||
strict_parse(markup)
|
||||
rescue SyntaxError => e
|
||||
e.line_number = line_number
|
||||
e.line_number = line_number
|
||||
e.markup_context = markup_context(markup)
|
||||
raise e
|
||||
end
|
||||
|
||||
@@ -3,15 +3,19 @@
|
||||
module Liquid
|
||||
class PartialCache
|
||||
def self.load(template_name, context:, parse_context:)
|
||||
cached_partials = (context.registers[:cached_partials] ||= {})
|
||||
cached_partials = context.registers[:cached_partials]
|
||||
cached = cached_partials[template_name]
|
||||
return cached if cached
|
||||
|
||||
file_system = (context.registers[:file_system] ||= Liquid::Template.file_system)
|
||||
source = file_system.read_template_file(template_name)
|
||||
file_system = context.registers[:file_system]
|
||||
source = file_system.read_template_file(template_name)
|
||||
|
||||
parse_context.partial = true
|
||||
|
||||
partial = Liquid::Template.parse(source, parse_context)
|
||||
template_factory = context.registers[:template_factory]
|
||||
template = template_factory.for(template_name)
|
||||
|
||||
partial = template.parse(source, parse_context)
|
||||
cached_partials[template_name] = partial
|
||||
ensure
|
||||
parse_context.partial = false
|
||||
|
||||
+63
-84
@@ -25,7 +25,7 @@ module Liquid
|
||||
# node.code
|
||||
#
|
||||
# # Which template and line number of this node.
|
||||
# # If top level, this will be "<root>".
|
||||
# # The top-level template name is `nil` by default, but can be set in the Liquid::Context before rendering.
|
||||
# node.partial
|
||||
# node.line_number
|
||||
#
|
||||
@@ -46,115 +46,94 @@ module Liquid
|
||||
include Enumerable
|
||||
|
||||
class Timing
|
||||
attr_reader :code, :partial, :line_number, :children
|
||||
attr_reader :code, :template_name, :line_number, :children
|
||||
attr_accessor :total_time
|
||||
alias_method :render_time, :total_time
|
||||
alias_method :partial, :template_name
|
||||
|
||||
def initialize(node, partial)
|
||||
@code = node.respond_to?(:raw) ? node.raw : node
|
||||
@partial = partial
|
||||
@line_number = node.respond_to?(:line_number) ? node.line_number : nil
|
||||
@children = []
|
||||
def initialize(code: nil, template_name: nil, line_number: nil)
|
||||
@code = code
|
||||
@template_name = template_name
|
||||
@line_number = line_number
|
||||
@children = []
|
||||
end
|
||||
|
||||
def self.start(node, partial)
|
||||
new(node, partial).tap(&:start)
|
||||
end
|
||||
|
||||
def start
|
||||
@start_time = Time.now
|
||||
end
|
||||
|
||||
def finish
|
||||
@end_time = Time.now
|
||||
end
|
||||
|
||||
def render_time
|
||||
@end_time - @start_time
|
||||
def self_time
|
||||
@self_time ||= begin
|
||||
total_children_time = 0.0
|
||||
@children.each do |child|
|
||||
total_children_time += child.total_time
|
||||
end
|
||||
@total_time - total_children_time
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.profile_node_render(node)
|
||||
if Profiler.current_profile && node.respond_to?(:render)
|
||||
Profiler.current_profile.start_node(node)
|
||||
output = yield
|
||||
Profiler.current_profile.end_node(node)
|
||||
output
|
||||
else
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
def self.profile_children(template_name)
|
||||
if Profiler.current_profile
|
||||
Profiler.current_profile.push_partial(template_name)
|
||||
output = yield
|
||||
Profiler.current_profile.pop_partial
|
||||
output
|
||||
else
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
def self.current_profile
|
||||
Thread.current[:liquid_profiler]
|
||||
end
|
||||
attr_reader :total_time
|
||||
alias_method :total_render_time, :total_time
|
||||
|
||||
def initialize
|
||||
@partial_stack = ["<root>"]
|
||||
|
||||
@root_timing = Timing.new("", current_partial)
|
||||
@timing_stack = [@root_timing]
|
||||
|
||||
@render_start_at = Time.now
|
||||
@render_end_at = @render_start_at
|
||||
@root_children = []
|
||||
@current_children = nil
|
||||
@total_time = 0.0
|
||||
end
|
||||
|
||||
def start
|
||||
Thread.current[:liquid_profiler] = self
|
||||
@render_start_at = Time.now
|
||||
def profile(template_name, &block)
|
||||
# nested renders are done from a tag that already has a timing node
|
||||
return yield if @current_children
|
||||
|
||||
root_children = @root_children
|
||||
render_idx = root_children.length
|
||||
begin
|
||||
@current_children = root_children
|
||||
profile_node(template_name, &block)
|
||||
ensure
|
||||
@current_children = nil
|
||||
if (timing = root_children[render_idx])
|
||||
@total_time += timing.total_time
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def stop
|
||||
Thread.current[:liquid_profiler] = nil
|
||||
@render_end_at = Time.now
|
||||
end
|
||||
|
||||
def total_render_time
|
||||
@render_end_at - @render_start_at
|
||||
def children
|
||||
children = @root_children
|
||||
if children.length == 1
|
||||
children.first.children
|
||||
else
|
||||
children
|
||||
end
|
||||
end
|
||||
|
||||
def each(&block)
|
||||
@root_timing.children.each(&block)
|
||||
children.each(&block)
|
||||
end
|
||||
|
||||
def [](idx)
|
||||
@root_timing.children[idx]
|
||||
children[idx]
|
||||
end
|
||||
|
||||
def length
|
||||
@root_timing.children.length
|
||||
children.length
|
||||
end
|
||||
|
||||
def start_node(node)
|
||||
@timing_stack.push(Timing.start(node, current_partial))
|
||||
def profile_node(template_name, code: nil, line_number: nil)
|
||||
timing = Timing.new(code: code, template_name: template_name, line_number: line_number)
|
||||
parent_children = @current_children
|
||||
start_time = monotonic_time
|
||||
begin
|
||||
@current_children = timing.children
|
||||
yield
|
||||
ensure
|
||||
@current_children = parent_children
|
||||
timing.total_time = monotonic_time - start_time
|
||||
parent_children << timing
|
||||
end
|
||||
end
|
||||
|
||||
def end_node(_node)
|
||||
timing = @timing_stack.pop
|
||||
timing.finish
|
||||
private
|
||||
|
||||
@timing_stack.last.children << timing
|
||||
end
|
||||
|
||||
def current_partial
|
||||
@partial_stack.last
|
||||
end
|
||||
|
||||
def push_partial(partial_name)
|
||||
@partial_stack.push(partial_name)
|
||||
end
|
||||
|
||||
def pop_partial
|
||||
@partial_stack.pop
|
||||
def monotonic_time
|
||||
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,25 +1,35 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class BlockBody
|
||||
def render_node_with_profiling(context, output, node)
|
||||
Profiler.profile_node_render(node) do
|
||||
render_node_without_profiling(context, output, node)
|
||||
module BlockBodyProfilingHook
|
||||
def render_node(context, output, node)
|
||||
if (profiler = context.profiler)
|
||||
profiler.profile_node(context.template_name, code: node.raw, line_number: node.line_number) do
|
||||
super
|
||||
end
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
alias_method :render_node_without_profiling, :render_node
|
||||
alias_method :render_node, :render_node_with_profiling
|
||||
end
|
||||
BlockBody.prepend(BlockBodyProfilingHook)
|
||||
|
||||
class Include < Tag
|
||||
def render_to_output_buffer_with_profiling(context, output)
|
||||
Profiler.profile_children(context.evaluate(@template_name_expr).to_s) do
|
||||
render_to_output_buffer_without_profiling(context, output)
|
||||
end
|
||||
module DocumentProfilingHook
|
||||
def render_to_output_buffer(context, output)
|
||||
return super unless context.profiler
|
||||
context.profiler.profile(context.template_name) { super }
|
||||
end
|
||||
|
||||
alias_method :render_to_output_buffer_without_profiling, :render_to_output_buffer
|
||||
alias_method :render_to_output_buffer, :render_to_output_buffer_with_profiling
|
||||
end
|
||||
Document.prepend(DocumentProfilingHook)
|
||||
|
||||
module ContextProfilingHook
|
||||
attr_accessor :profiler
|
||||
|
||||
def new_isolated_subcontext
|
||||
new_context = super
|
||||
new_context.profiler = profiler
|
||||
new_context
|
||||
end
|
||||
end
|
||||
Context.prepend(ContextProfilingHook)
|
||||
end
|
||||
|
||||
@@ -4,22 +4,65 @@ module Liquid
|
||||
class RangeLookup
|
||||
def self.parse(start_markup, end_markup)
|
||||
start_obj = Expression.parse(start_markup)
|
||||
end_obj = Expression.parse(end_markup)
|
||||
end_obj = Expression.parse(end_markup)
|
||||
if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate)
|
||||
new(start_obj, end_obj)
|
||||
else
|
||||
start_obj.to_i..end_obj.to_i
|
||||
begin
|
||||
start_obj.to_i..end_obj.to_i
|
||||
rescue NoMethodError
|
||||
invalid_expr = start_markup unless start_obj.respond_to?(:to_i)
|
||||
invalid_expr ||= end_markup unless end_obj.respond_to?(:to_i)
|
||||
if invalid_expr
|
||||
raise Liquid::SyntaxError, "Invalid expression type '#{invalid_expr}' in range expression"
|
||||
end
|
||||
|
||||
raise
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.lax_migrate(start_markup, end_markup)
|
||||
new_start = Expression.lax_migrate(start_markup)
|
||||
new_end = Expression.lax_migrate(end_markup)
|
||||
|
||||
# cast literals
|
||||
start_obj = Expression.parse(new_start)
|
||||
end_obj = Expression.parse(new_end)
|
||||
if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate)
|
||||
new_start = lax_migrate_range_expression(new_start, start_obj)
|
||||
new_end = lax_migrate_range_expression(new_end, end_obj)
|
||||
else
|
||||
new_start = start_obj.to_i.to_s unless start_obj.is_a?(Integer)
|
||||
new_end = end_obj.to_i.to_s unless end_obj.is_a?(Integer)
|
||||
end
|
||||
|
||||
[new_start, new_end]
|
||||
end
|
||||
|
||||
def self.lax_migrate_range_expression(markup, expression)
|
||||
return markup if expression.respond_to?(:evaluate)
|
||||
|
||||
case expression
|
||||
when Integer
|
||||
markup
|
||||
when NilClass, String
|
||||
expression.to_i.to_s
|
||||
else
|
||||
Utils.to_integer(input).to_s
|
||||
end
|
||||
end
|
||||
|
||||
attr_reader :start_obj, :end_obj
|
||||
|
||||
def initialize(start_obj, end_obj)
|
||||
@start_obj = start_obj
|
||||
@end_obj = end_obj
|
||||
@end_obj = end_obj
|
||||
end
|
||||
|
||||
def evaluate(context)
|
||||
start_int = to_integer(context.evaluate(@start_obj))
|
||||
end_int = to_integer(context.evaluate(@end_obj))
|
||||
end_int = to_integer(context.evaluate(@end_obj))
|
||||
start_int..end_int
|
||||
end
|
||||
|
||||
@@ -35,5 +78,11 @@ module Liquid
|
||||
Utils.to_integer(input)
|
||||
end
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
[@node.start_obj, @node.end_obj]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Registers
|
||||
attr_reader :static
|
||||
|
||||
def initialize(registers = {})
|
||||
@static = registers.is_a?(Registers) ? registers.static : registers
|
||||
@changes = {}
|
||||
end
|
||||
|
||||
def []=(key, value)
|
||||
@changes[key] = value
|
||||
end
|
||||
|
||||
def [](key)
|
||||
if @changes.key?(key)
|
||||
@changes[key]
|
||||
else
|
||||
@static[key]
|
||||
end
|
||||
end
|
||||
|
||||
def delete(key)
|
||||
@changes.delete(key)
|
||||
end
|
||||
|
||||
UNDEFINED = Object.new
|
||||
|
||||
def fetch(key, default = UNDEFINED, &block)
|
||||
if @changes.key?(key)
|
||||
@changes.fetch(key)
|
||||
elsif default != UNDEFINED
|
||||
if block_given?
|
||||
@static.fetch(key, &block)
|
||||
else
|
||||
@static.fetch(key, default)
|
||||
end
|
||||
else
|
||||
@static.fetch(key, &block)
|
||||
end
|
||||
end
|
||||
|
||||
def key?(key)
|
||||
@changes.key?(key) || @static.key?(key)
|
||||
end
|
||||
end
|
||||
|
||||
# Alias for backwards compatibility
|
||||
StaticRegisters = Registers
|
||||
end
|
||||
@@ -2,24 +2,61 @@
|
||||
|
||||
module Liquid
|
||||
class ResourceLimits
|
||||
attr_accessor :render_length, :render_score, :assign_score,
|
||||
:render_length_limit, :render_score_limit, :assign_score_limit
|
||||
attr_accessor :render_length_limit, :render_score_limit, :assign_score_limit
|
||||
attr_reader :render_score, :assign_score
|
||||
|
||||
def initialize(limits)
|
||||
@render_length_limit = limits[:render_length_limit]
|
||||
@render_score_limit = limits[:render_score_limit]
|
||||
@assign_score_limit = limits[:assign_score_limit]
|
||||
@render_score_limit = limits[:render_score_limit]
|
||||
@assign_score_limit = limits[:assign_score_limit]
|
||||
reset
|
||||
end
|
||||
|
||||
def increment_render_score(amount)
|
||||
@render_score += amount
|
||||
raise_limits_reached if @render_score_limit && @render_score > @render_score_limit
|
||||
end
|
||||
|
||||
def increment_assign_score(amount)
|
||||
@assign_score += amount
|
||||
raise_limits_reached if @assign_score_limit && @assign_score > @assign_score_limit
|
||||
end
|
||||
|
||||
# update either render_length or assign_score based on whether or not the writes are captured
|
||||
def increment_write_score(output)
|
||||
if (last_captured = @last_capture_length)
|
||||
captured = output.bytesize
|
||||
increment = captured - last_captured
|
||||
@last_capture_length = captured
|
||||
increment_assign_score(increment)
|
||||
elsif @render_length_limit && output.bytesize > @render_length_limit
|
||||
raise_limits_reached
|
||||
end
|
||||
end
|
||||
|
||||
def raise_limits_reached
|
||||
@reached_limit = true
|
||||
raise MemoryError, "Memory limits exceeded"
|
||||
end
|
||||
|
||||
def reached?
|
||||
(@render_length_limit && @render_length > @render_length_limit) ||
|
||||
(@render_score_limit && @render_score > @render_score_limit) ||
|
||||
(@assign_score_limit && @assign_score > @assign_score_limit)
|
||||
@reached_limit
|
||||
end
|
||||
|
||||
def reset
|
||||
@render_length = @render_score = @assign_score = 0
|
||||
@reached_limit = false
|
||||
@last_capture_length = nil
|
||||
@render_score = @assign_score = 0
|
||||
end
|
||||
|
||||
def with_capture
|
||||
old_capture_length = @last_capture_length
|
||||
begin
|
||||
@last_capture_length = 0
|
||||
yield
|
||||
ensure
|
||||
@last_capture_length = old_capture_length
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+546
-96
@@ -1,10 +1,19 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'cgi'
|
||||
require 'base64'
|
||||
require 'bigdecimal'
|
||||
|
||||
module Liquid
|
||||
module StandardFilters
|
||||
MAX_I32 = (1 << 31) - 1
|
||||
private_constant :MAX_I32
|
||||
|
||||
MIN_I64 = -(1 << 63)
|
||||
MAX_I64 = (1 << 63) - 1
|
||||
I64_RANGE = MIN_I64..MAX_I64
|
||||
private_constant :MIN_I64, :MAX_I64, :I64_RANGE
|
||||
|
||||
HTML_ESCAPE = {
|
||||
'&' => '&',
|
||||
'>' => '>',
|
||||
@@ -13,46 +22,106 @@ module Liquid
|
||||
"'" => ''',
|
||||
}.freeze
|
||||
HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/
|
||||
STRIP_HTML_BLOCKS = Regexp.union(
|
||||
STRIP_HTML_BLOCKS = Regexp.union(
|
||||
%r{<script.*?</script>}m,
|
||||
/<!--.*?-->/m,
|
||||
%r{<style.*?</style>}m
|
||||
)
|
||||
STRIP_HTML_TAGS = /<.*?>/m
|
||||
|
||||
# Return the size of an array or of an string
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category array
|
||||
# @liquid_summary
|
||||
# Returns the size of a string or array.
|
||||
# @liquid_description
|
||||
# The size of a string is the number of characters that the string includes. The size of an array is the number of items
|
||||
# in the array.
|
||||
# @liquid_syntax variable | size
|
||||
# @liquid_return [number]
|
||||
def size(input)
|
||||
input.respond_to?(:size) ? input.size : 0
|
||||
end
|
||||
|
||||
# convert an input string to DOWNCASE
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category string
|
||||
# @liquid_summary
|
||||
# Converts a string to all lowercase characters.
|
||||
# @liquid_syntax string | downcase
|
||||
# @liquid_return [string]
|
||||
def downcase(input)
|
||||
input.to_s.downcase
|
||||
end
|
||||
|
||||
# convert an input string to UPCASE
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category string
|
||||
# @liquid_summary
|
||||
# Converts a string to all uppercase characters.
|
||||
# @liquid_syntax string | upcase
|
||||
# @liquid_return [string]
|
||||
def upcase(input)
|
||||
input.to_s.upcase
|
||||
end
|
||||
|
||||
# capitalize words in the input centence
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category string
|
||||
# @liquid_summary
|
||||
# Capitalizes the first word in a string.
|
||||
# @liquid_syntax string | capitalize
|
||||
# @liquid_return [string]
|
||||
def capitalize(input)
|
||||
input.to_s.capitalize
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category string
|
||||
# @liquid_summary
|
||||
# Escapes special characters in HTML, such as `<>`, `'`, and `&`, and converts characters into escape sequences. The filter doesn't effect characters within the string that don’t have a corresponding escape sequence.".
|
||||
# @liquid_syntax string | escape
|
||||
# @liquid_return [string]
|
||||
def escape(input)
|
||||
CGI.escapeHTML(input.to_s).untaint unless input.nil?
|
||||
CGI.escapeHTML(input.to_s) unless input.nil?
|
||||
end
|
||||
alias_method :h, :escape
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category string
|
||||
# @liquid_summary
|
||||
# Escapes a string without changing characters that have already been escaped.
|
||||
# @liquid_syntax string | escape_once
|
||||
# @liquid_return [string]
|
||||
def escape_once(input)
|
||||
input.to_s.gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE)
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category string
|
||||
# @liquid_summary
|
||||
# Converts any URL-unsafe characters in a string to the
|
||||
# [percent-encoded](https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding) equivalent.
|
||||
# @liquid_description
|
||||
# > Note:
|
||||
# > Spaces are converted to a `+` character, instead of a percent-encoded character.
|
||||
# @liquid_syntax string | url_encode
|
||||
# @liquid_return [string]
|
||||
def url_encode(input)
|
||||
CGI.escape(input.to_s) unless input.nil?
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category string
|
||||
# @liquid_summary
|
||||
# Decodes any [percent-encoded](https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding) characters
|
||||
# in a string.
|
||||
# @liquid_syntax string | url_decode
|
||||
# @liquid_return [string]
|
||||
def url_decode(input)
|
||||
return if input.nil?
|
||||
|
||||
@@ -62,79 +131,229 @@ module Liquid
|
||||
result
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category string
|
||||
# @liquid_summary
|
||||
# Encodes a string to [Base64 format](https://developer.mozilla.org/en-US/docs/Glossary/Base64).
|
||||
# @liquid_syntax string | base64_encode
|
||||
# @liquid_return [string]
|
||||
def base64_encode(input)
|
||||
Base64.strict_encode64(input.to_s)
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category string
|
||||
# @liquid_summary
|
||||
# Decodes a string in [Base64 format](https://developer.mozilla.org/en-US/docs/Glossary/Base64).
|
||||
# @liquid_syntax string | base64_decode
|
||||
# @liquid_return [string]
|
||||
def base64_decode(input)
|
||||
Base64.strict_decode64(input.to_s)
|
||||
rescue ::ArgumentError
|
||||
raise Liquid::ArgumentError, "invalid base64 provided to base64_decode"
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category string
|
||||
# @liquid_summary
|
||||
# Encodes a string to URL-safe [Base64 format](https://developer.mozilla.org/en-US/docs/Glossary/Base64).
|
||||
# @liquid_syntax string | base64_url_safe_encode
|
||||
# @liquid_return [string]
|
||||
def base64_url_safe_encode(input)
|
||||
Base64.urlsafe_encode64(input.to_s)
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category string
|
||||
# @liquid_summary
|
||||
# Decodes a string in URL-safe [Base64 format](https://developer.mozilla.org/en-US/docs/Glossary/Base64).
|
||||
# @liquid_syntax string | base64_url_safe_decode
|
||||
# @liquid_return [string]
|
||||
def base64_url_safe_decode(input)
|
||||
Base64.urlsafe_decode64(input.to_s)
|
||||
rescue ::ArgumentError
|
||||
raise Liquid::ArgumentError, "invalid base64 provided to base64_url_safe_decode"
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category string
|
||||
# @liquid_summary
|
||||
# Returns a substring or series of array items, starting at a given 0-based index.
|
||||
# @liquid_description
|
||||
# By default, the substring has a length of one character, and the array series has one array item. However, you can
|
||||
# provide a second parameter to specify the number of characters or array items.
|
||||
# @liquid_syntax string | slice
|
||||
# @liquid_return [string]
|
||||
def slice(input, offset, length = nil)
|
||||
offset = Utils.to_integer(offset)
|
||||
length = length ? Utils.to_integer(length) : 1
|
||||
|
||||
if input.is_a?(Array)
|
||||
input.slice(offset, length) || []
|
||||
else
|
||||
input.to_s.slice(offset, length) || ''
|
||||
begin
|
||||
if input.is_a?(Array)
|
||||
input.slice(offset, length) || []
|
||||
else
|
||||
input.to_s.slice(offset, length) || ''
|
||||
end
|
||||
rescue RangeError
|
||||
if I64_RANGE.cover?(length) && I64_RANGE.cover?(offset)
|
||||
raise # unexpected error
|
||||
end
|
||||
offset = offset.clamp(I64_RANGE)
|
||||
length = length.clamp(I64_RANGE)
|
||||
retry
|
||||
end
|
||||
end
|
||||
|
||||
# Truncate a string down to x characters
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category string
|
||||
# @liquid_summary
|
||||
# Truncates a string down to a given number of characters.
|
||||
# @liquid_description
|
||||
# If the specified number of characters is less than the length of the string, then an ellipsis (`...`) is appended to
|
||||
# the truncated string. The ellipsis is included in the character count of the truncated string.
|
||||
# @liquid_syntax string | truncate: number
|
||||
# @liquid_return [string]
|
||||
def truncate(input, length = 50, truncate_string = "...")
|
||||
return if input.nil?
|
||||
input_str = input.to_s
|
||||
length = Utils.to_integer(length)
|
||||
length = Utils.to_integer(length)
|
||||
|
||||
truncate_string_str = truncate_string.to_s
|
||||
|
||||
l = length - truncate_string_str.length
|
||||
l = 0 if l < 0
|
||||
|
||||
input_str.length > length ? input_str[0...l].concat(truncate_string_str) : input_str
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category string
|
||||
# @liquid_summary
|
||||
# Truncates a string down to a given number of words.
|
||||
# @liquid_description
|
||||
# If the specified number of words is less than the number of words in the string, then an ellipsis (`...`) is appended to
|
||||
# the truncated string.
|
||||
#
|
||||
# > Caution:
|
||||
# > HTML tags are treated as words, so you should strip any HTML from truncated content. If you don't strip HTML, then
|
||||
# > closing HTML tags can be removed, which can result in unexpected behavior.
|
||||
# @liquid_syntax string | truncatewords: number
|
||||
# @liquid_return [string]
|
||||
def truncatewords(input, words = 15, truncate_string = "...")
|
||||
return if input.nil?
|
||||
wordlist = input.to_s.split
|
||||
input = input.to_s
|
||||
words = Utils.to_integer(words)
|
||||
l = words - 1
|
||||
l = 0 if l < 0
|
||||
wordlist.length > l ? wordlist[0..l].join(" ").concat(truncate_string.to_s) : input
|
||||
words = 1 if words <= 0
|
||||
|
||||
wordlist = begin
|
||||
input.split(" ", words + 1)
|
||||
rescue RangeError
|
||||
# integer too big for String#split, but we can semantically assume no truncation is needed
|
||||
return input if words + 1 > MAX_I32
|
||||
raise # unexpected error
|
||||
end
|
||||
return input if wordlist.length <= words
|
||||
|
||||
wordlist.pop
|
||||
wordlist.join(" ").concat(truncate_string.to_s)
|
||||
end
|
||||
|
||||
# Split input string into an array of substrings separated by given pattern.
|
||||
#
|
||||
# Example:
|
||||
# <div class="summary">{{ post | split '//' | first }}</div>
|
||||
#
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category string
|
||||
# @liquid_summary
|
||||
# Splits a string into an array of substrings based on a given separator.
|
||||
# @liquid_syntax string | split: string
|
||||
# @liquid_return [array[string]]
|
||||
def split(input, pattern)
|
||||
input.to_s.split(pattern.to_s)
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category string
|
||||
# @liquid_summary
|
||||
# Strips all whitespace from the left and right of a string.
|
||||
# @liquid_syntax string | strip
|
||||
# @liquid_return [string]
|
||||
def strip(input)
|
||||
input.to_s.strip
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category string
|
||||
# @liquid_summary
|
||||
# Strips all whitespace from the left of a string.
|
||||
# @liquid_syntax string | lstrip
|
||||
# @liquid_return [string]
|
||||
def lstrip(input)
|
||||
input.to_s.lstrip
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category string
|
||||
# @liquid_summary
|
||||
# Strips all whitespace from the right of a string.
|
||||
# @liquid_syntax string | rstrip
|
||||
# @liquid_return [string]
|
||||
def rstrip(input)
|
||||
input.to_s.rstrip
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category string
|
||||
# @liquid_summary
|
||||
# Strips all HTML tags from a string.
|
||||
# @liquid_syntax string | strip_html
|
||||
# @liquid_return [string]
|
||||
def strip_html(input)
|
||||
empty = ''
|
||||
empty = ''
|
||||
result = input.to_s.gsub(STRIP_HTML_BLOCKS, empty)
|
||||
result.gsub!(STRIP_HTML_TAGS, empty)
|
||||
result
|
||||
end
|
||||
|
||||
# Remove all newlines from the string
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category string
|
||||
# @liquid_summary
|
||||
# Strips all newline characters (line breaks) from a string.
|
||||
# @liquid_syntax string | strip_newlines
|
||||
# @liquid_return [string]
|
||||
def strip_newlines(input)
|
||||
input.to_s.gsub(/\r?\n/, '')
|
||||
end
|
||||
|
||||
# Join elements of the array with certain character between them
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category array
|
||||
# @liquid_summary
|
||||
# Combines all of the items in an array into a single string, separated by a space.
|
||||
# @liquid_syntax array | join
|
||||
# @liquid_return [string]
|
||||
def join(input, glue = ' ')
|
||||
InputIterator.new(input).join(glue)
|
||||
InputIterator.new(input, context).join(glue)
|
||||
end
|
||||
|
||||
# Sort elements of the array
|
||||
# provide optional property with which to sort an array of hashes or drops
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category array
|
||||
# @liquid_summary
|
||||
# Sorts the items in an array in case-sensitive alphabetical, or numerical, order.
|
||||
# @liquid_syntax array | sort
|
||||
# @liquid_return [array[untyped]]
|
||||
def sort(input, property = nil)
|
||||
ary = InputIterator.new(input)
|
||||
ary = InputIterator.new(input, context)
|
||||
|
||||
return [] if ary.empty?
|
||||
|
||||
@@ -151,10 +370,19 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
# Sort elements of an array ignoring case if strings
|
||||
# provide optional property with which to sort an array of hashes or drops
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category array
|
||||
# @liquid_summary
|
||||
# Sorts the items in an array in case-insensitive alphabetical order.
|
||||
# @liquid_description
|
||||
# > Caution:
|
||||
# > You shouldn't use the `sort_natural` filter to sort numerical values. When comparing items an array, each item is converted to a
|
||||
# > string, so sorting on numerical values can lead to unexpected results.
|
||||
# @liquid_syntax array | sort_natural
|
||||
# @liquid_return [array[untyped]]
|
||||
def sort_natural(input, property = nil)
|
||||
ary = InputIterator.new(input)
|
||||
ary = InputIterator.new(input, context)
|
||||
|
||||
return [] if ary.empty?
|
||||
|
||||
@@ -171,55 +399,88 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
# Filter the elements of an array to those with a certain property value.
|
||||
# By default the target is any truthy value.
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category array
|
||||
# @liquid_summary
|
||||
# Filters an array to include only items with a specific property value.
|
||||
# @liquid_description
|
||||
# This requires you to provide both the property name and the associated value.
|
||||
# @liquid_syntax array | where: string, string
|
||||
# @liquid_return [array[untyped]]
|
||||
def where(input, property, target_value = nil)
|
||||
ary = InputIterator.new(input)
|
||||
ary = InputIterator.new(input, context)
|
||||
|
||||
if ary.empty?
|
||||
[]
|
||||
elsif ary.first.respond_to?(:[]) && target_value.nil?
|
||||
begin
|
||||
ary.select { |item| item[property] }
|
||||
elsif target_value.nil?
|
||||
ary.select do |item|
|
||||
item[property]
|
||||
rescue TypeError
|
||||
raise_property_error(property)
|
||||
rescue NoMethodError
|
||||
return nil unless item.respond_to?(:[])
|
||||
raise
|
||||
end
|
||||
elsif ary.first.respond_to?(:[])
|
||||
begin
|
||||
ary.select { |item| item[property] == target_value }
|
||||
else
|
||||
ary.select do |item|
|
||||
item[property] == target_value
|
||||
rescue TypeError
|
||||
raise_property_error(property)
|
||||
rescue NoMethodError
|
||||
return nil unless item.respond_to?(:[])
|
||||
raise
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Remove duplicate elements from an array
|
||||
# provide optional property with which to determine uniqueness
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category array
|
||||
# @liquid_summary
|
||||
# Removes any duplicate items in an array.
|
||||
# @liquid_syntax array | uniq
|
||||
# @liquid_return [array[untyped]]
|
||||
def uniq(input, property = nil)
|
||||
ary = InputIterator.new(input)
|
||||
ary = InputIterator.new(input, context)
|
||||
|
||||
if property.nil?
|
||||
ary.uniq
|
||||
elsif ary.empty? # The next two cases assume a non-empty array.
|
||||
[]
|
||||
elsif ary.first.respond_to?(:[])
|
||||
begin
|
||||
ary.uniq { |a| a[property] }
|
||||
else
|
||||
ary.uniq do |item|
|
||||
item[property]
|
||||
rescue TypeError
|
||||
raise_property_error(property)
|
||||
rescue NoMethodError
|
||||
return nil unless item.respond_to?(:[])
|
||||
raise
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Reverse the elements of an array
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category array
|
||||
# @liquid_summary
|
||||
# Reverses the order of the items in an array.
|
||||
# @liquid_syntax array | reverse
|
||||
# @liquid_return [array[untyped]]
|
||||
def reverse(input)
|
||||
ary = InputIterator.new(input)
|
||||
ary = InputIterator.new(input, context)
|
||||
ary.reverse
|
||||
end
|
||||
|
||||
# map/collect on a given property
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category array
|
||||
# @liquid_summary
|
||||
# Creates an array of values from a specific property of the items in an array.
|
||||
# @liquid_syntax array | map: string
|
||||
# @liquid_return [array[untyped]]
|
||||
def map(input, property)
|
||||
InputIterator.new(input).map do |e|
|
||||
InputIterator.new(input, context).map do |e|
|
||||
e = e.call if e.is_a?(Proc)
|
||||
|
||||
if property == "to_liquid"
|
||||
@@ -233,64 +494,157 @@ module Liquid
|
||||
raise_property_error(property)
|
||||
end
|
||||
|
||||
# Remove nils within an array
|
||||
# provide optional property with which to check for nil
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category array
|
||||
# @liquid_summary
|
||||
# Removes any `nil` items from an array.
|
||||
# @liquid_syntax array | compact
|
||||
# @liquid_return [array[untyped]]
|
||||
def compact(input, property = nil)
|
||||
ary = InputIterator.new(input)
|
||||
ary = InputIterator.new(input, context)
|
||||
|
||||
if property.nil?
|
||||
ary.compact
|
||||
elsif ary.empty? # The next two cases assume a non-empty array.
|
||||
[]
|
||||
elsif ary.first.respond_to?(:[])
|
||||
begin
|
||||
ary.reject { |a| a[property].nil? }
|
||||
else
|
||||
ary.reject do |item|
|
||||
item[property].nil?
|
||||
rescue TypeError
|
||||
raise_property_error(property)
|
||||
rescue NoMethodError
|
||||
return nil unless item.respond_to?(:[])
|
||||
raise
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Replace occurrences of a string with another
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category string
|
||||
# @liquid_summary
|
||||
# Replaces any instance of a substring inside a string with a given string.
|
||||
# @liquid_syntax string | replace: string, string
|
||||
# @liquid_return [string]
|
||||
def replace(input, string, replacement = '')
|
||||
input.to_s.gsub(string.to_s, replacement.to_s)
|
||||
end
|
||||
|
||||
# Replace the first occurrences of a string with another
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category string
|
||||
# @liquid_summary
|
||||
# Replaces the first instance of a substring inside a string with a given string.
|
||||
# @liquid_syntax string | replace_first: string, string
|
||||
# @liquid_return [string]
|
||||
def replace_first(input, string, replacement = '')
|
||||
input.to_s.sub(string.to_s, replacement.to_s)
|
||||
end
|
||||
|
||||
# remove a substring
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category string
|
||||
# @liquid_summary
|
||||
# Replaces the last instance of a substring inside a string with a given string.
|
||||
# @liquid_syntax string | replace_last: string, string
|
||||
# @liquid_return [string]
|
||||
def replace_last(input, string, replacement)
|
||||
input = input.to_s
|
||||
string = string.to_s
|
||||
replacement = replacement.to_s
|
||||
|
||||
start_index = input.rindex(string)
|
||||
|
||||
return input unless start_index
|
||||
|
||||
output = input.dup
|
||||
output[start_index, string.length] = replacement
|
||||
output
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category string
|
||||
# @liquid_summary
|
||||
# Removes any instance of a substring inside a string.
|
||||
# @liquid_syntax string | remove: string
|
||||
# @liquid_return [string]
|
||||
def remove(input, string)
|
||||
input.to_s.gsub(string.to_s, '')
|
||||
replace(input, string, '')
|
||||
end
|
||||
|
||||
# remove the first occurrences of a substring
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category string
|
||||
# @liquid_summary
|
||||
# Removes the first instance of a substring inside a string.
|
||||
# @liquid_syntax string | remove_first: string
|
||||
# @liquid_return [string]
|
||||
def remove_first(input, string)
|
||||
input.to_s.sub(string.to_s, '')
|
||||
replace_first(input, string, '')
|
||||
end
|
||||
|
||||
# add one string to another
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category string
|
||||
# @liquid_summary
|
||||
# Removes the last instance of a substring inside a string.
|
||||
# @liquid_syntax string | remove_last: string
|
||||
# @liquid_return [string]
|
||||
def remove_last(input, string)
|
||||
replace_last(input, string, '')
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category string
|
||||
# @liquid_summary
|
||||
# Adds a given string to the end of a string.
|
||||
# @liquid_syntax string | append: string
|
||||
# @liquid_return [string]
|
||||
def append(input, string)
|
||||
input.to_s + string.to_s
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category array
|
||||
# @liquid_summary
|
||||
# Concatenates (combines) two arrays.
|
||||
# @liquid_description
|
||||
# > Note:
|
||||
# > The `concat` filter won't filter out duplicates. If you want to remove duplicates, then you need to use the
|
||||
# > [`uniq` filter](/api/liquid/filters/uniq).
|
||||
# @liquid_syntax array | concat: array
|
||||
# @liquid_return [array[untyped]]
|
||||
def concat(input, array)
|
||||
unless array.respond_to?(:to_ary)
|
||||
raise ArgumentError, "concat filter requires an array argument"
|
||||
end
|
||||
InputIterator.new(input).concat(array)
|
||||
InputIterator.new(input, context).concat(array)
|
||||
end
|
||||
|
||||
# prepend a string to another
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category string
|
||||
# @liquid_summary
|
||||
# Adds a given string to the beginning of a string.
|
||||
# @liquid_syntax string | prepend: string
|
||||
# @liquid_return [string]
|
||||
def prepend(input, string)
|
||||
string.to_s + input.to_s
|
||||
end
|
||||
|
||||
# Add <br /> tags in front of all newlines in input string
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category string
|
||||
# @liquid_summary
|
||||
# Converts newlines (`\n`) in a string to HTML line breaks (`<br>`).
|
||||
# @liquid_syntax string | newline_to_br
|
||||
# @liquid_return [string]
|
||||
def newline_to_br(input)
|
||||
input.to_s.gsub(/\n/, "<br />\n")
|
||||
input.to_s.gsub(/\r?\n/, "<br />\n")
|
||||
end
|
||||
|
||||
# Reformat a date using Ruby's core Time#strftime( string ) -> string
|
||||
@@ -327,63 +681,111 @@ module Liquid
|
||||
def date(input, format)
|
||||
return input if format.to_s.empty?
|
||||
|
||||
return input unless date = Utils.to_date(input)
|
||||
return input unless (date = Utils.to_date(input))
|
||||
|
||||
date.strftime(format.to_s)
|
||||
end
|
||||
|
||||
# Get the first element of the passed in array
|
||||
#
|
||||
# Example:
|
||||
# {{ product.images | first | to_img }}
|
||||
#
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category array
|
||||
# @liquid_summary
|
||||
# Returns the first item in an array.
|
||||
# @liquid_syntax array | first
|
||||
# @liquid_return [untyped]
|
||||
def first(array)
|
||||
array.first if array.respond_to?(:first)
|
||||
end
|
||||
|
||||
# Get the last element of the passed in array
|
||||
#
|
||||
# Example:
|
||||
# {{ product.images | last | to_img }}
|
||||
#
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category array
|
||||
# @liquid_summary
|
||||
# Returns the last item in an array.
|
||||
# @liquid_syntax array | last
|
||||
# @liquid_return [untyped]
|
||||
def last(array)
|
||||
array.last if array.respond_to?(:last)
|
||||
end
|
||||
|
||||
# absolute value
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category math
|
||||
# @liquid_summary
|
||||
# Returns the absolute value of a number.
|
||||
# @liquid_syntax number | abs
|
||||
# @liquid_return [number]
|
||||
def abs(input)
|
||||
result = Utils.to_number(input).abs
|
||||
result.is_a?(BigDecimal) ? result.to_f : result
|
||||
end
|
||||
|
||||
# addition
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category math
|
||||
# @liquid_summary
|
||||
# Adds two numbers.
|
||||
# @liquid_syntax number | plus: number
|
||||
# @liquid_return [number]
|
||||
def plus(input, operand)
|
||||
apply_operation(input, operand, :+)
|
||||
end
|
||||
|
||||
# subtraction
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category math
|
||||
# @liquid_summary
|
||||
# Subtracts a given number from another number.
|
||||
# @liquid_syntax number | minus: number
|
||||
# @liquid_return [number]
|
||||
def minus(input, operand)
|
||||
apply_operation(input, operand, :-)
|
||||
end
|
||||
|
||||
# multiplication
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category math
|
||||
# @liquid_summary
|
||||
# Multiplies a number by a given number.
|
||||
# @liquid_syntax number | times: number
|
||||
# @liquid_return [number]
|
||||
def times(input, operand)
|
||||
apply_operation(input, operand, :*)
|
||||
end
|
||||
|
||||
# division
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category math
|
||||
# @liquid_summary
|
||||
# Divides a number by a given number.
|
||||
# @liquid_syntax number | divided_by: number
|
||||
# @liquid_return [number]
|
||||
def divided_by(input, operand)
|
||||
apply_operation(input, operand, :/)
|
||||
rescue ::ZeroDivisionError => e
|
||||
raise Liquid::ZeroDivisionError, e.message
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category math
|
||||
# @liquid_summary
|
||||
# Returns the remainder of dividing a number by a given number.
|
||||
# @liquid_syntax number | modulo: number
|
||||
# @liquid_return [number]
|
||||
def modulo(input, operand)
|
||||
apply_operation(input, operand, :%)
|
||||
rescue ::ZeroDivisionError => e
|
||||
raise Liquid::ZeroDivisionError, e.message
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category math
|
||||
# @liquid_summary
|
||||
# Rounds a number to the nearest integer.
|
||||
# @liquid_syntax number | round
|
||||
# @liquid_return [number]
|
||||
def round(input, n = 0)
|
||||
result = Utils.to_number(input).round(Utils.to_number(n))
|
||||
result = result.to_f if result.is_a?(BigDecimal)
|
||||
@@ -393,18 +795,39 @@ module Liquid
|
||||
raise Liquid::FloatDomainError, e.message
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category math
|
||||
# @liquid_summary
|
||||
# Rounds a number up to the nearest integer.
|
||||
# @liquid_syntax number | ceil
|
||||
# @liquid_return [number]
|
||||
def ceil(input)
|
||||
Utils.to_number(input).ceil.to_i
|
||||
rescue ::FloatDomainError => e
|
||||
raise Liquid::FloatDomainError, e.message
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category math
|
||||
# @liquid_summary
|
||||
# Rounds a number down to the nearest integer.
|
||||
# @liquid_syntax number | floor
|
||||
# @liquid_return [number]
|
||||
def floor(input)
|
||||
Utils.to_number(input).floor.to_i
|
||||
rescue ::FloatDomainError => e
|
||||
raise Liquid::FloatDomainError, e.message
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category math
|
||||
# @liquid_summary
|
||||
# Limits a number to a minimum value.
|
||||
# @liquid_syntax number | at_least
|
||||
# @liquid_return [number]
|
||||
def at_least(input, n)
|
||||
min_value = Utils.to_number(n)
|
||||
|
||||
@@ -413,6 +836,13 @@ module Liquid
|
||||
result.is_a?(BigDecimal) ? result.to_f : result
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category math
|
||||
# @liquid_summary
|
||||
# Limits a number to a maximum value.
|
||||
# @liquid_syntax number | at_most
|
||||
# @liquid_return [number]
|
||||
def at_most(input, n)
|
||||
max_value = Utils.to_number(n)
|
||||
|
||||
@@ -421,17 +851,28 @@ module Liquid
|
||||
result.is_a?(BigDecimal) ? result.to_f : result
|
||||
end
|
||||
|
||||
def default(input, default_value = '')
|
||||
if !input || input.respond_to?(:empty?) && input.empty?
|
||||
Usage.increment("default_filter_received_false_value") if input == false # See https://github.com/Shopify/liquid/issues/1127
|
||||
default_value
|
||||
else
|
||||
input
|
||||
end
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category default
|
||||
# @liquid_summary
|
||||
# Sets a default value for any variable whose value is one of the following:
|
||||
#
|
||||
# - [`empty`](/api/liquid/basics#empty)
|
||||
# - [`false`](/api/liquid/basics#truthy-and-falsy)
|
||||
# - [`nil`](/api/liquid/basics#nil)
|
||||
# @liquid_syntax variable | default: variable
|
||||
# @liquid_return [untyped]
|
||||
# @liquid_optional_param allow_false [boolean] Whether to use false values instead of the default.
|
||||
def default(input, default_value = '', options = {})
|
||||
options = {} unless options.is_a?(Hash)
|
||||
false_check = options['allow_false'] ? input.nil? : !Liquid::Utils.to_liquid_value(input)
|
||||
false_check || (input.respond_to?(:empty?) && input.empty?) ? default_value : input
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :context
|
||||
|
||||
def raise_property_error(property)
|
||||
raise Liquid::ArgumentError, "cannot select the property '#{property}'"
|
||||
end
|
||||
@@ -442,10 +883,16 @@ module Liquid
|
||||
end
|
||||
|
||||
def nil_safe_compare(a, b)
|
||||
if !a.nil? && !b.nil?
|
||||
a <=> b
|
||||
result = a <=> b
|
||||
|
||||
if result
|
||||
result
|
||||
elsif a.nil?
|
||||
1
|
||||
elsif b.nil?
|
||||
-1
|
||||
else
|
||||
a.nil? ? 1 : -1
|
||||
raise Liquid::ArgumentError, "cannot sort values of incompatible types"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -460,8 +907,9 @@ module Liquid
|
||||
class InputIterator
|
||||
include Enumerable
|
||||
|
||||
def initialize(input)
|
||||
@input = if input.is_a?(Array)
|
||||
def initialize(input, context)
|
||||
@context = context
|
||||
@input = if input.is_a?(Array)
|
||||
input.flatten
|
||||
elsif input.is_a?(Hash)
|
||||
[input]
|
||||
@@ -499,7 +947,9 @@ module Liquid
|
||||
|
||||
def each
|
||||
@input.each do |e|
|
||||
yield(e.respond_to?(:to_liquid) ? e.to_liquid : e)
|
||||
e = e.respond_to?(:to_liquid) ? e.to_liquid : e
|
||||
e.context = @context if e.respond_to?(:context=)
|
||||
yield(e)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class StaticRegisters
|
||||
attr_reader :static_registers, :registers
|
||||
|
||||
def initialize(registers = {})
|
||||
@static_registers = registers.is_a?(StaticRegisters) ? registers.static_registers : registers.freeze
|
||||
@registers = {}
|
||||
end
|
||||
|
||||
def []=(key, value)
|
||||
@registers[key] = value
|
||||
end
|
||||
|
||||
def [](key)
|
||||
if @registers.key?(key)
|
||||
@registers[key]
|
||||
else
|
||||
@static_registers[key]
|
||||
end
|
||||
end
|
||||
|
||||
def delete(key)
|
||||
@registers.delete(key)
|
||||
end
|
||||
|
||||
def fetch(key, default = nil)
|
||||
key?(key) ? self[key] : default
|
||||
end
|
||||
|
||||
def key?(key)
|
||||
@registers.key?(key) || @static_registers.key?(key)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,68 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'set'
|
||||
|
||||
module Liquid
|
||||
# Strainer is the parent class for the filters system.
|
||||
# New filters are mixed into the strainer class which is then instantiated for each liquid template render run.
|
||||
#
|
||||
# The Strainer only allows method calls defined in filters given to it via Strainer.global_filter,
|
||||
# Context#add_filters or Template.register_filter
|
||||
class Strainer #:nodoc:
|
||||
@@global_strainer = Class.new(Strainer) do
|
||||
@filter_methods = Set.new
|
||||
end
|
||||
@@strainer_class_cache = Hash.new do |hash, filters|
|
||||
hash[filters] = Class.new(@@global_strainer) do
|
||||
@filter_methods = @@global_strainer.filter_methods.dup
|
||||
filters.each { |f| add_filter(f) }
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(context)
|
||||
@context = context
|
||||
end
|
||||
|
||||
class << self
|
||||
attr_reader :filter_methods
|
||||
end
|
||||
|
||||
def self.add_filter(filter)
|
||||
raise ArgumentError, "Expected module but got: #{filter.class}" unless filter.is_a?(Module)
|
||||
unless include?(filter)
|
||||
invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) }
|
||||
if invokable_non_public_methods.any?
|
||||
raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}"
|
||||
else
|
||||
send(:include, filter)
|
||||
@filter_methods.merge(filter.public_instance_methods.map(&:to_s))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.global_filter(filter)
|
||||
@@strainer_class_cache.clear
|
||||
@@global_strainer.add_filter(filter)
|
||||
end
|
||||
|
||||
def self.invokable?(method)
|
||||
@filter_methods.include?(method.to_s)
|
||||
end
|
||||
|
||||
def self.create(context, filters = [])
|
||||
@@strainer_class_cache[filters].new(context)
|
||||
end
|
||||
|
||||
def invoke(method, *args)
|
||||
if self.class.invokable?(method)
|
||||
send(method, *args)
|
||||
elsif @context&.strict_filters
|
||||
raise Liquid::UndefinedFilter, "undefined filter #{method}"
|
||||
else
|
||||
args.first
|
||||
end
|
||||
rescue ::ArgumentError => e
|
||||
raise Liquid::ArgumentError, e.message, e.backtrace
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,41 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# StrainerFactory is the factory for the filters system.
|
||||
module StrainerFactory
|
||||
extend self
|
||||
|
||||
def add_global_filter(filter)
|
||||
strainer_class_cache.clear
|
||||
GlobalCache.add_filter(filter)
|
||||
end
|
||||
|
||||
def create(context, filters = [])
|
||||
strainer_from_cache(filters).new(context)
|
||||
end
|
||||
|
||||
def global_filter_names
|
||||
GlobalCache.filter_method_names
|
||||
end
|
||||
|
||||
GlobalCache = Class.new(StrainerTemplate)
|
||||
|
||||
private
|
||||
|
||||
def strainer_from_cache(filters)
|
||||
if filters.empty?
|
||||
GlobalCache
|
||||
else
|
||||
strainer_class_cache[filters] ||= begin
|
||||
klass = Class.new(GlobalCache)
|
||||
filters.each { |f| klass.add_filter(f) }
|
||||
klass
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def strainer_class_cache
|
||||
@strainer_class_cache ||= {}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,62 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'set'
|
||||
|
||||
module Liquid
|
||||
# StrainerTemplate is the computed class for the filters system.
|
||||
# New filters are mixed into the strainer class which is then instantiated for each liquid template render run.
|
||||
#
|
||||
# The Strainer only allows method calls defined in filters given to it via StrainerFactory.add_global_filter,
|
||||
# Context#add_filters or Template.register_filter
|
||||
class StrainerTemplate
|
||||
def initialize(context)
|
||||
@context = context
|
||||
end
|
||||
|
||||
class << self
|
||||
def add_filter(filter)
|
||||
return if include?(filter)
|
||||
|
||||
invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) }
|
||||
if invokable_non_public_methods.any?
|
||||
raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}"
|
||||
end
|
||||
|
||||
include(filter)
|
||||
|
||||
filter_methods.merge(filter.public_instance_methods.map(&:to_s))
|
||||
end
|
||||
|
||||
def invokable?(method)
|
||||
filter_methods.include?(method.to_s)
|
||||
end
|
||||
|
||||
def inherited(subclass)
|
||||
super
|
||||
subclass.instance_variable_set(:@filter_methods, @filter_methods.dup)
|
||||
end
|
||||
|
||||
def filter_method_names
|
||||
filter_methods.map(&:to_s).to_a
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def filter_methods
|
||||
@filter_methods ||= Set.new
|
||||
end
|
||||
end
|
||||
|
||||
def invoke(method, *args)
|
||||
if self.class.invokable?(method)
|
||||
send(method, *args)
|
||||
elsif @context.strict_filters
|
||||
raise Liquid::UndefinedFilter, "undefined filter #{method}"
|
||||
else
|
||||
args.first
|
||||
end
|
||||
rescue ::ArgumentError => e
|
||||
raise Liquid::ArgumentError, e.message, e.backtrace
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,49 +1,106 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# @liquid_public_docs
|
||||
# @liquid_type object
|
||||
# @liquid_name tablerowloop
|
||||
# @liquid_summary
|
||||
# Information about a parent [`tablerow` loop](/api/liquid/tags/tablerow).
|
||||
class TablerowloopDrop < Drop
|
||||
def initialize(length, cols)
|
||||
@length = length
|
||||
@row = 1
|
||||
@col = 1
|
||||
@cols = cols
|
||||
@index = 0
|
||||
@row = 1
|
||||
@col = 1
|
||||
@cols = cols
|
||||
@index = 0
|
||||
end
|
||||
|
||||
attr_reader :length, :col, :row
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# The total number of iterations in the loop.
|
||||
# @liquid_return [number]
|
||||
attr_reader :length
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# The 1-based index of the current column.
|
||||
# @liquid_return [number]
|
||||
attr_reader :col
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# The 1-based index of current row.
|
||||
# @liquid_return [number]
|
||||
attr_reader :row
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# The 1-based index of the current iteration.
|
||||
# @liquid_return [number]
|
||||
def index
|
||||
@index + 1
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# The 0-based index of the current iteration.
|
||||
# @liquid_return [number]
|
||||
def index0
|
||||
@index
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# The 0-based index of the current column.
|
||||
# @liquid_return [number]
|
||||
def col0
|
||||
@col - 1
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# The 1-based index of the current iteration, in reverse order.
|
||||
# @liquid_return [number]
|
||||
def rindex
|
||||
@length - @index
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# The 0-based index of the current iteration, in reverse order.
|
||||
# @liquid_return [number]
|
||||
def rindex0
|
||||
@length - @index - 1
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# Returns `true` if the current iteration is the first. Returns `false` if not.
|
||||
# @liquid_return [boolean]
|
||||
def first
|
||||
@index == 0
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# Returns `true` if the current iteration is the last. Returns `false` if not.
|
||||
# @liquid_return [boolean]
|
||||
def last
|
||||
@index == @length - 1
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# Returns `true` if the current column is the first in the row. Returns `false` if not.
|
||||
# @liquid_return [boolean]
|
||||
def col_first
|
||||
@col == 1
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# Returns `true` if the current column is the last in the row. Returns `false` if not.
|
||||
# @liquid_return [boolean]
|
||||
def col_last
|
||||
@col == @cols
|
||||
end
|
||||
|
||||
+23
-3
@@ -7,20 +7,34 @@ module Liquid
|
||||
include ParserSwitching
|
||||
|
||||
class << self
|
||||
include ParserSwitching::ClassMethods
|
||||
|
||||
def parse(tag_name, markup, tokenizer, parse_context)
|
||||
tag = new(tag_name, markup, parse_context)
|
||||
tag.parse(tokenizer)
|
||||
tag
|
||||
end
|
||||
|
||||
def disable_tags(*tag_names)
|
||||
tag_names += disabled_tags
|
||||
define_singleton_method(:disabled_tags) { tag_names }
|
||||
prepend(Disabler)
|
||||
end
|
||||
|
||||
private :new
|
||||
|
||||
protected
|
||||
|
||||
def disabled_tags
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(tag_name, markup, parse_context)
|
||||
@tag_name = tag_name
|
||||
@markup = markup
|
||||
@tag_name = tag_name
|
||||
@markup = markup
|
||||
@parse_context = parse_context
|
||||
@line_number = parse_context.line_number
|
||||
@line_number = parse_context.line_number
|
||||
end
|
||||
|
||||
def parse(_tokens)
|
||||
@@ -49,5 +63,11 @@ module Liquid
|
||||
def blank?
|
||||
false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse_expression(markup)
|
||||
parse_context.parse_expression(markup)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Tag
|
||||
module Disableable
|
||||
def render_to_output_buffer(context, output)
|
||||
if context.tag_disabled?(tag_name)
|
||||
output << disabled_error(context)
|
||||
return
|
||||
end
|
||||
super
|
||||
end
|
||||
|
||||
def disabled_error(context)
|
||||
# raise then rescue the exception so that the Context#exception_renderer can re-raise it
|
||||
raise DisabledError, "#{tag_name} #{parse_context[:locale].t('errors.disabled.tag')}"
|
||||
rescue DisabledError => exc
|
||||
context.handle_error(exc, line_number)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Tag
|
||||
module Disabler
|
||||
def render_to_output_buffer(context, output)
|
||||
context.with_disabled_tags(self.class.disabled_tags) do
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
+40
-16
@@ -1,37 +1,54 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# Assign sets a variable in your template.
|
||||
#
|
||||
# {% assign foo = 'monkey' %}
|
||||
#
|
||||
# You can then use the variable later in the page.
|
||||
#
|
||||
# {{ foo }}
|
||||
#
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category variable
|
||||
# @liquid_name assign
|
||||
# @liquid_summary
|
||||
# Creates a new variable.
|
||||
# @liquid_description
|
||||
# You can create variables of any [basic type](/api/liquid/basics#types), [object](/api/liquid/objects), or object property.
|
||||
# @liquid_syntax
|
||||
# {% assign variable_name = value %}
|
||||
# @liquid_syntax_keyword variable_name The name of the variable being created.
|
||||
# @liquid_syntax_keyword value The value you want to assign to the variable.
|
||||
class Assign < Tag
|
||||
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
|
||||
|
||||
def self.syntax_error_translation_key
|
||||
"errors.syntax.assign"
|
||||
# @api private
|
||||
def self.raise_syntax_error(parse_context)
|
||||
raise Liquid::SyntaxError, parse_context.locale.t('errors.syntax.assign')
|
||||
end
|
||||
|
||||
def self.migrate(tag_name, markup, tokenizer, parse_context)
|
||||
match = markup.match(Syntax)
|
||||
new_variable_markup = Variable.migrate(match[2], parse_context)
|
||||
new_markup = Utils.match_captures_replace(match, 2 => new_variable_markup)
|
||||
|
||||
# replace scanned over characters with a space to ensure there is a space
|
||||
# to separate the tag name and the variable name
|
||||
new_markup.prepend(" ") if match.begin(0) > 0
|
||||
|
||||
new_markup
|
||||
end
|
||||
|
||||
attr_reader :to, :from
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
def initialize(tag_name, markup, parse_context)
|
||||
super
|
||||
if markup =~ Syntax
|
||||
@to = Regexp.last_match(1)
|
||||
@from = Variable.new(Regexp.last_match(2), options)
|
||||
@to = Regexp.last_match(1)
|
||||
@from = Variable.new(Regexp.last_match(2), parse_context)
|
||||
else
|
||||
raise SyntaxError, options[:locale].t(self.class.syntax_error_translation_key)
|
||||
self.class.raise_syntax_error(parse_context)
|
||||
end
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
val = @from.render(context)
|
||||
context.scopes.last[@to] = val
|
||||
context.resource_limits.assign_score += assign_score_of(val)
|
||||
context.resource_limits.increment_assign_score(assign_score_of(val))
|
||||
output
|
||||
end
|
||||
|
||||
@@ -44,11 +61,18 @@ module Liquid
|
||||
def assign_score_of(val)
|
||||
if val.instance_of?(String)
|
||||
val.bytesize
|
||||
elsif val.instance_of?(Array) || val.instance_of?(Hash)
|
||||
elsif val.instance_of?(Array)
|
||||
sum = 1
|
||||
# Uses #each to avoid extra allocations.
|
||||
val.each { |child| sum += assign_score_of(child) }
|
||||
sum
|
||||
elsif val.instance_of?(Hash)
|
||||
sum = 1
|
||||
val.each do |key, entry_value|
|
||||
sum += assign_score_of(key)
|
||||
sum += assign_score_of(entry_value)
|
||||
end
|
||||
sum
|
||||
else
|
||||
1
|
||||
end
|
||||
|
||||
@@ -10,9 +10,24 @@ module Liquid
|
||||
# {% endif %}
|
||||
# {% endfor %}
|
||||
#
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category iteration
|
||||
# @liquid_name break
|
||||
# @liquid_summary
|
||||
# Stops a [`for` loop](/api/liquid/tags/for) from iterating.
|
||||
# @liquid_syntax
|
||||
# {% break %}
|
||||
class Break < Tag
|
||||
def interrupt
|
||||
BreakInterrupt.new
|
||||
INTERRUPT = BreakInterrupt.new.freeze
|
||||
|
||||
def self.migrate(_tag_name, _markup, _tokenizer, _parse_context)
|
||||
"" # markup was ignored
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
context.push_interrupt(INTERRUPT)
|
||||
output
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
+32
-14
@@ -1,20 +1,38 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# Capture stores the result of a block into a variable without rendering it inplace.
|
||||
#
|
||||
# {% capture heading %}
|
||||
# Monkeys!
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category variable
|
||||
# @liquid_name capture
|
||||
# @liquid_summary
|
||||
# Creates a new variable with a string value.
|
||||
# @liquid_description
|
||||
# You can create complex strings with Liquid logic and variables.
|
||||
# @liquid_syntax
|
||||
# {% capture variable %}
|
||||
# value
|
||||
# {% endcapture %}
|
||||
# ...
|
||||
# <h1>{{ heading }}</h1>
|
||||
#
|
||||
# Capture is useful for saving content for use later in your template, such as
|
||||
# in a sidebar or footer.
|
||||
#
|
||||
# @liquid_syntax_keyword variable The name of the variable being created.
|
||||
# @liquid_syntax_keyword value The value you want to assign to the variable.
|
||||
class Capture < Block
|
||||
Syntax = /(#{VariableSignature}+)/o
|
||||
|
||||
def self.migrate(tag_name, markup, tokenizer, parse_context)
|
||||
match = markup.match(Syntax)
|
||||
|
||||
new_markup = match[1]
|
||||
|
||||
# replace scanned over characters with a space to ensure there is a space
|
||||
# to separate the tag name and the variable name
|
||||
new_markup.prepend(" ") if match.begin(0) > 0
|
||||
|
||||
new_body, unknown_tag = migrate_body(tag_name, tokenizer, parse_context)
|
||||
raise SyntaxError if unknown_tag
|
||||
|
||||
[new_markup, new_body]
|
||||
end
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
if markup =~ Syntax
|
||||
@@ -25,10 +43,10 @@ module Liquid
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
previous_output_size = output.bytesize
|
||||
super
|
||||
context.scopes.last[@to] = output
|
||||
context.resource_limits.assign_score += (output.bytesize - previous_output_size)
|
||||
context.resource_limits.with_capture do
|
||||
capture_output = render(context)
|
||||
context.scopes.last[@to] = capture_output
|
||||
end
|
||||
output
|
||||
end
|
||||
|
||||
|
||||
+106
-6
@@ -1,26 +1,100 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category conditional
|
||||
# @liquid_name case
|
||||
# @liquid_summary
|
||||
# Renders a specific expression depending on the value of a specific variable.
|
||||
# @liquid_syntax
|
||||
# {% case variable %}
|
||||
# {% when first_value %}
|
||||
# first_expression
|
||||
# {% when second_value %}
|
||||
# second_expression
|
||||
# {% else %}
|
||||
# third_expression
|
||||
# {% endcase %}
|
||||
# @liquid_syntax_keyword variable The name of the variable you want to base your case statement on.
|
||||
# @liquid_syntax_keyword first_value A specific value to check for.
|
||||
# @liquid_syntax_keyword second_value A specific value to check for.
|
||||
# @liquid_syntax_keyword first_expression An expression to be rendered when the variable's value matches `first_value`.
|
||||
# @liquid_syntax_keyword second_expression An expression to be rendered when the variable's value matches `second_value`.
|
||||
# @liquid_syntax_keyword third_expression An expression to be rendered when the variable's value has no match.
|
||||
class Case < Block
|
||||
Syntax = /(#{QuotedFragment})/o
|
||||
WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om
|
||||
|
||||
attr_reader :blocks, :left
|
||||
|
||||
def self.migrate(tag_name, markup, tokenizer, parse_context)
|
||||
match = markup.match(/\s*#{Syntax}\s*/o) || raise(SyntaxError)
|
||||
|
||||
new_expression = Expression.lax_migrate(match[1])
|
||||
new_markup = Utils.match_captures_replace(match, { 1 => new_expression })
|
||||
|
||||
# replace scanned over characters with a space to ensure there is a space
|
||||
# to separate the tag name and the variable name
|
||||
new_markup.prepend(" ") if match.begin(0) > 0
|
||||
|
||||
new_body = migrate_body(tag_name, tokenizer, parse_context)
|
||||
[new_markup, new_body]
|
||||
end
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
@blocks = []
|
||||
|
||||
if markup =~ Syntax
|
||||
@left = Expression.parse(Regexp.last_match(1))
|
||||
@left = parse_expression(Regexp.last_match(1))
|
||||
else
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.case")
|
||||
end
|
||||
end
|
||||
|
||||
def self.migrate_body(start_tag_name, tokenizer, parse_context)
|
||||
result = +""
|
||||
|
||||
# body before first `when` delimiter tag is ignored
|
||||
unused_body, delimiter_tag = super(start_tag_name, tokenizer, parse_context)
|
||||
unless delimiter_tag
|
||||
raise NotImplementedError, "TODO: migrate `case` tag with no `when` or `else` tags"
|
||||
end
|
||||
|
||||
result << Utils.migrate_stripped(unused_body) { "" } # just keep whitespace (e.g. newline and indent)
|
||||
|
||||
while delimiter_tag
|
||||
break unless delimiter_tag
|
||||
|
||||
case delimiter_tag.tag_name
|
||||
when "when"
|
||||
new_markup = migrate_when_markup(delimiter_tag.markup)
|
||||
result << delimiter_tag.replaced_markup(new_markup)
|
||||
when "else"
|
||||
result << delimiter_tag.original_tag_string
|
||||
else
|
||||
raise SyntaxError
|
||||
end
|
||||
|
||||
new_body, delimiter_tag = super(start_tag_name, tokenizer, parse_context)
|
||||
result << new_body
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
def parse(tokens)
|
||||
body = BlockBody.new
|
||||
body = case_body = new_body
|
||||
body = @blocks.last.attachment while parse_body(body, tokens)
|
||||
@blocks.reverse_each do |condition|
|
||||
body = condition.attachment
|
||||
unless body.frozen?
|
||||
body.remove_blank_strings if blank?
|
||||
body.freeze
|
||||
end
|
||||
end
|
||||
case_body.freeze
|
||||
end
|
||||
|
||||
def nodelist
|
||||
@@ -44,7 +118,14 @@ module Liquid
|
||||
@blocks.each do |block|
|
||||
if block.else?
|
||||
block.attachment.render_to_output_buffer(context, output) if execute_else_block
|
||||
elsif block.evaluate(context)
|
||||
next
|
||||
end
|
||||
|
||||
result = Liquid::Utils.to_liquid_value(
|
||||
block.evaluate(context)
|
||||
)
|
||||
|
||||
if result
|
||||
execute_else_block = false
|
||||
block.attachment.render_to_output_buffer(context, output)
|
||||
end
|
||||
@@ -55,8 +136,27 @@ module Liquid
|
||||
|
||||
private
|
||||
|
||||
private_class_method def self.migrate_when_markup(unstripped_markup)
|
||||
Utils.migrate_stripped(unstripped_markup) do |markup|
|
||||
match = markup.match(WhenSyntax) || raise(SyntaxError)
|
||||
|
||||
replacements = { 1 => Expression.lax_migrate(match[1]) }
|
||||
if (right = match[2])
|
||||
replacements[2] = migrate_when_markup(right)
|
||||
end
|
||||
|
||||
new_markup = Utils.match_captures_replace(match, replacements)
|
||||
|
||||
# replace scanned over characters with a space to ensure there is a space
|
||||
# to separate the tag name and the variable name
|
||||
new_markup.prepend(" ") if match.begin(0) > 0
|
||||
|
||||
new_markup
|
||||
end
|
||||
end
|
||||
|
||||
def record_when_condition(markup)
|
||||
body = BlockBody.new
|
||||
body = new_body
|
||||
|
||||
while markup
|
||||
unless markup =~ WhenSyntax
|
||||
@@ -65,7 +165,7 @@ module Liquid
|
||||
|
||||
markup = Regexp.last_match(2)
|
||||
|
||||
block = Condition.new(@left, '==', Expression.parse(Regexp.last_match(1)))
|
||||
block = Condition.new(@left, '==', Condition.parse_expression(parse_context, Regexp.last_match(1)))
|
||||
block.attach(body)
|
||||
@blocks << block
|
||||
end
|
||||
@@ -77,7 +177,7 @@ module Liquid
|
||||
end
|
||||
|
||||
block = ElseCondition.new
|
||||
block.attach(BlockBody.new)
|
||||
block.attach(new_body)
|
||||
@blocks << block
|
||||
end
|
||||
|
||||
|
||||
@@ -1,7 +1,38 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category syntax
|
||||
# @liquid_name comment
|
||||
# @liquid_summary
|
||||
# Prevents an expression from being rendered or output.
|
||||
# @liquid_description
|
||||
# Any text inside `comment` tags won't be output, and any Liquid code will be parsed, but not executed.
|
||||
# @liquid_syntax
|
||||
# {% comment %}
|
||||
# content
|
||||
# {% endcomment %}
|
||||
# @liquid_syntax_keyword content The content of the comment.
|
||||
class Comment < Block
|
||||
def self.migrate(tag_name, _markup, tokenizer, parse_context)
|
||||
new_markup = "" # markup was ignored
|
||||
new_body = migrate_body(tag_name, tokenizer, parse_context)
|
||||
[new_markup, new_body]
|
||||
end
|
||||
|
||||
def self.migrate_body(start_tag_name, tokenizer, parse_context)
|
||||
result = +""
|
||||
loop do
|
||||
new_body, delimiter_tag = super(start_tag_name, tokenizer, parse_context)
|
||||
result << new_body
|
||||
break unless delimiter_tag
|
||||
|
||||
result << delimiter_tag.original_tag_string # unknown tags allowed
|
||||
end
|
||||
result
|
||||
end
|
||||
|
||||
def render_to_output_buffer(_context, output)
|
||||
output
|
||||
end
|
||||
|
||||
+17
-11
@@ -1,18 +1,24 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# Continue tag to be used to break out of a for loop.
|
||||
#
|
||||
# == Basic Usage:
|
||||
# {% for item in collection %}
|
||||
# {% if item.condition %}
|
||||
# {% continue %}
|
||||
# {% endif %}
|
||||
# {% endfor %}
|
||||
#
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category iteration
|
||||
# @liquid_name continue
|
||||
# @liquid_summary
|
||||
# Causes a [`for` loop](/api/liquid/tags/for) to skip to the next iteration.
|
||||
# @liquid_syntax
|
||||
# {% continue %}
|
||||
class Continue < Tag
|
||||
def interrupt
|
||||
ContinueInterrupt.new
|
||||
INTERRUPT = ContinueInterrupt.new.freeze
|
||||
|
||||
def self.migrate(_tag_name, _markup, _tokenizer, _parse_context)
|
||||
"" # markup was ignored
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
context.push_interrupt(INTERRUPT)
|
||||
output
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
+53
-17
@@ -1,33 +1,69 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# Cycle is usually used within a loop to alternate between values, like colors or DOM classes.
|
||||
#
|
||||
# {% for item in items %}
|
||||
# <div class="{% cycle 'red', 'green', 'blue' %}"> {{ item }} </div>
|
||||
# {% end %}
|
||||
#
|
||||
# <div class="red"> Item one </div>
|
||||
# <div class="green"> Item two </div>
|
||||
# <div class="blue"> Item three </div>
|
||||
# <div class="red"> Item four </div>
|
||||
# <div class="green"> Item five</div>
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category iteration
|
||||
# @liquid_name cycle
|
||||
# @liquid_summary
|
||||
# Loops through a group of strings and outputs them one at a time for each iteration of a [`for` loop](/api/liquid/tags/for).
|
||||
# @liquid_description
|
||||
# The `cycle` tag must be used inside a `for` loop.
|
||||
#
|
||||
# > Tip:
|
||||
# > Use the `cycle` tag to output text in a predictable pattern. For example, to apply odd/even classes to rows in a table.
|
||||
# @liquid_syntax
|
||||
# {% cycle string, string, ... %}
|
||||
class Cycle < Tag
|
||||
SimpleSyntax = /\A#{QuotedFragment}+/o
|
||||
NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om
|
||||
|
||||
def self.migrate(tag_name, markup, tokenizer, parse_context)
|
||||
new_markup = case markup
|
||||
when NamedSyntax
|
||||
match = Regexp.last_match
|
||||
|
||||
new_name_syntax = Expression.lax_migrate(match[1])
|
||||
new_variables_markup = migrate_variables_from_string(match[2])
|
||||
|
||||
Utils.match_captures_replace(match, 1 => new_name_syntax, 2 => new_variables_markup)
|
||||
when SimpleSyntax
|
||||
match = Regexp.last_match
|
||||
migrate_variables_from_string(markup)
|
||||
else
|
||||
raise SyntaxError
|
||||
end
|
||||
|
||||
# replace scanned over characters with a space to ensure there is a space
|
||||
# to separate the tag name and the variable name
|
||||
new_markup.prepend(" ") if match.begin(0) > 0
|
||||
|
||||
new_markup
|
||||
end
|
||||
|
||||
def self.migrate_variables_from_string(markup)
|
||||
markup.split(',').collect do |var|
|
||||
match = var.match(/\s*(#{QuotedFragment})\s*/o)
|
||||
if match
|
||||
Utils.match_captures_replace(match, 1 => Expression.lax_migrate(match[1]))
|
||||
end
|
||||
end.compact.join(",")
|
||||
end
|
||||
|
||||
attr_reader :variables
|
||||
|
||||
# @api private
|
||||
attr_reader :name
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
case markup
|
||||
when NamedSyntax
|
||||
@variables = variables_from_string(Regexp.last_match(2))
|
||||
@name = Expression.parse(Regexp.last_match(1))
|
||||
@name = parse_expression(Regexp.last_match(1))
|
||||
when SimpleSyntax
|
||||
@variables = variables_from_string(markup)
|
||||
@name = @variables.to_s
|
||||
@name = @variables.to_s
|
||||
else
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.cycle")
|
||||
end
|
||||
@@ -36,7 +72,7 @@ module Liquid
|
||||
def render_to_output_buffer(context, output)
|
||||
context.registers[:cycle] ||= {}
|
||||
|
||||
key = context.evaluate(@name)
|
||||
key = context.evaluate(@name)
|
||||
iteration = context.registers[:cycle][key].to_i
|
||||
|
||||
val = context.evaluate(@variables[iteration])
|
||||
@@ -50,9 +86,9 @@ module Liquid
|
||||
output << val
|
||||
|
||||
iteration += 1
|
||||
iteration = 0 if iteration >= @variables.size
|
||||
context.registers[:cycle][key] = iteration
|
||||
iteration = 0 if iteration >= @variables.size
|
||||
|
||||
context.registers[:cycle][key] = iteration
|
||||
output
|
||||
end
|
||||
|
||||
@@ -61,7 +97,7 @@ module Liquid
|
||||
def variables_from_string(markup)
|
||||
markup.split(',').collect do |var|
|
||||
var =~ /\s*(#{QuotedFragment})\s*/o
|
||||
Regexp.last_match(1) ? Expression.parse(Regexp.last_match(1)) : nil
|
||||
Regexp.last_match(1) ? parse_expression(Regexp.last_match(1)) : nil
|
||||
end.compact
|
||||
end
|
||||
|
||||
|
||||
@@ -1,34 +1,40 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# decrement is used in a place where one needs to insert a counter
|
||||
# into a template, and needs the counter to survive across
|
||||
# multiple instantiations of the template.
|
||||
# NOTE: decrement is a pre-decrement, --i,
|
||||
# while increment is post: i++.
|
||||
#
|
||||
# (To achieve the survival, the application must keep the context)
|
||||
#
|
||||
# if the variable does not exist, it is created with value 0.
|
||||
|
||||
# Hello: {% decrement variable %}
|
||||
#
|
||||
# gives you:
|
||||
#
|
||||
# Hello: -1
|
||||
# Hello: -2
|
||||
# Hello: -3
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category variable
|
||||
# @liquid_name decrement
|
||||
# @liquid_summary
|
||||
# Creates a new variable, with a default value of -1, that's decreased by 1 with each subsequent call.
|
||||
# @liquid_description
|
||||
# Variables that are declared with `decrement` are unique to the [layout](/themes/architecture/layouts), [template](/themes/architecture/templates),
|
||||
# or [section](/themes/architecture/sections) file that they're created in. However, the variable is shared across
|
||||
# [snippets](/themes/architecture#snippets) included in the file.
|
||||
#
|
||||
# Similarly, variables that are created with `decrement` are independent from those created with [`assign`](/api/liquid/tags/assign)
|
||||
# and [`capture`](/api/liquid/tags/capture). However, `decrement` and [`increment`](/api/liquid/tags/increment) share
|
||||
# variables.
|
||||
# @liquid_syntax
|
||||
# {% decrement variable_name %}
|
||||
# @liquid_syntax_keyword variable_name The name of the variable being decremented.
|
||||
class Decrement < Tag
|
||||
attr_reader :variable_name
|
||||
|
||||
def self.migrate(_tag_name, markup, _tokenizer, _parse_context)
|
||||
markup # no characters ignored, it just uses anything for the variable name
|
||||
end
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
@variable = markup.strip
|
||||
@variable_name = markup.strip
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
value = context.environments.first[@variable] ||= 0
|
||||
counter_environment = context.environments.first
|
||||
value = counter_environment[@variable_name] || 0
|
||||
value -= 1
|
||||
context.environments.first[@variable] = value
|
||||
counter_environment[@variable_name] = value
|
||||
output << value.to_s
|
||||
output
|
||||
end
|
||||
|
||||
+28
-9
@@ -1,17 +1,30 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# Echo outputs an expression
|
||||
#
|
||||
# {% echo monkey %}
|
||||
# {% echo user.name %}
|
||||
#
|
||||
# This is identical to variable output syntax, like {{ foo }}, but works
|
||||
# inside {% liquid %} tags. The full syntax is supported, including filters:
|
||||
#
|
||||
# {% echo user | link %}
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category syntax
|
||||
# @liquid_name echo
|
||||
# @liquid_summary
|
||||
# Outputs an expression.
|
||||
# @liquid_description
|
||||
# Using the `echo` tag is the same as wrapping an expression in curly brackets (`{{` and `}}`). However, unlike the curly
|
||||
# bracket method, you can use the `echo` tag inside [`liquid` tags](/api/liquid/tags/liquid).
|
||||
#
|
||||
# > Tip:
|
||||
# > You can use [filters](/api/liquid/filters) on expressions inside `echo` tags.
|
||||
# @liquid_syntax
|
||||
# {% liquid
|
||||
# echo expression
|
||||
# %}
|
||||
# @liquid_syntax_keyword expression The expression to be output.
|
||||
class Echo < Tag
|
||||
attr_reader :variable
|
||||
|
||||
def self.migrate(tag_name, markup, tokenizer, parse_context)
|
||||
Variable.migrate(markup, parse_context)
|
||||
end
|
||||
|
||||
def initialize(tag_name, markup, parse_context)
|
||||
super
|
||||
@variable = Variable.new(markup, parse_context)
|
||||
@@ -20,6 +33,12 @@ module Liquid
|
||||
def render(context)
|
||||
@variable.render_to_output_buffer(context, +'')
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
[@node.variable]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('echo', Echo)
|
||||
|
||||
+89
-62
@@ -1,75 +1,88 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# "For" iterates over an array or collection.
|
||||
# Several useful variables are available to you within the loop.
|
||||
#
|
||||
# == Basic usage:
|
||||
# {% for item in collection %}
|
||||
# {{ forloop.index }}: {{ item.name }}
|
||||
# {% endfor %}
|
||||
#
|
||||
# == Advanced usage:
|
||||
# {% for item in collection %}
|
||||
# <div {% if forloop.first %}class="first"{% endif %}>
|
||||
# Item {{ forloop.index }}: {{ item.name }}
|
||||
# </div>
|
||||
# {% else %}
|
||||
# There is nothing in the collection.
|
||||
# {% endfor %}
|
||||
#
|
||||
# You can also define a limit and offset much like SQL. Remember
|
||||
# that offset starts at 0 for the first item.
|
||||
#
|
||||
# {% for item in collection limit:5 offset:10 %}
|
||||
# {{ item.name }}
|
||||
# {% end %}
|
||||
#
|
||||
# To reverse the for loop simply use {% for item in collection reversed %} (note that the flag's spelling is different to the filter `reverse`)
|
||||
#
|
||||
# == Available variables:
|
||||
#
|
||||
# forloop.name:: 'item-collection'
|
||||
# forloop.length:: Length of the loop
|
||||
# forloop.index:: The current item's position in the collection;
|
||||
# forloop.index starts at 1.
|
||||
# This is helpful for non-programmers who start believe
|
||||
# the first item in an array is 1, not 0.
|
||||
# forloop.index0:: The current item's position in the collection
|
||||
# where the first item is 0
|
||||
# forloop.rindex:: Number of items remaining in the loop
|
||||
# (length - index) where 1 is the last item.
|
||||
# forloop.rindex0:: Number of items remaining in the loop
|
||||
# where 0 is the last item.
|
||||
# forloop.first:: Returns true if the item is the first item.
|
||||
# forloop.last:: Returns true if the item is the last item.
|
||||
# forloop.parentloop:: Provides access to the parent loop, if present.
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category iteration
|
||||
# @liquid_name for
|
||||
# @liquid_summary
|
||||
# Renders an expression for every item in an array.
|
||||
# @liquid_description
|
||||
# You can do a maximum of 50 iterations with a `for` loop. If you need to iterate over more than 50 items, then use the
|
||||
# [`paginate` tag](/api/liquid/tags/paginate) to split the items over multiple pages.
|
||||
#
|
||||
# > Tip:
|
||||
# > Every `for` loop has an associated [`forloop` object](/api/liquid/objects/forloop) with information about the loop.
|
||||
# @liquid_syntax
|
||||
# {% for variable in array %}
|
||||
# expression
|
||||
# {% endfor %}
|
||||
# @liquid_syntax_keyword variable The current item in the array.
|
||||
# @liquid_syntax_keyword array The array to iterate over.
|
||||
# @liquid_syntax_keyword expression The expression to render for each iteration.
|
||||
# @liquid_optional_param limit [number] The number of iterations to perform.
|
||||
# @liquid_optional_param offset [number] The 1-based index to start iterating at.
|
||||
# @liquid_optional_param range [untyped] A custom numeric range to iterate over.
|
||||
# @liquid_optional_param reversed [untyped] Iterate in reverse order.
|
||||
class For < Block
|
||||
Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
|
||||
Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)(\s*reversed)?/o
|
||||
|
||||
attr_reader :collection_name, :variable_name, :limit, :from
|
||||
|
||||
def self.migrate(tag_name, markup, tokenizer, parse_context)
|
||||
new_markup = migrate_with_selected_parser(tag_name, markup, tokenizer, parse_context)
|
||||
new_body = migrate_body(tag_name, tokenizer, parse_context)
|
||||
[new_markup, new_body]
|
||||
end
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
@from = @limit = nil
|
||||
parse_with_selected_parser(markup)
|
||||
@for_block = BlockBody.new
|
||||
@for_block = new_body
|
||||
@else_block = nil
|
||||
end
|
||||
|
||||
def parse(tokens)
|
||||
return unless parse_body(@for_block, tokens)
|
||||
parse_body(@else_block, tokens)
|
||||
if parse_body(@for_block, tokens)
|
||||
parse_body(@else_block, tokens)
|
||||
end
|
||||
if blank?
|
||||
@else_block&.remove_blank_strings
|
||||
@for_block.remove_blank_strings
|
||||
end
|
||||
@else_block&.freeze
|
||||
@for_block.freeze
|
||||
end
|
||||
|
||||
def nodelist
|
||||
@else_block ? [@for_block, @else_block] : [@for_block]
|
||||
end
|
||||
|
||||
def self.migrate_body(start_tag_name, tokenizer, parse_context)
|
||||
result = +""
|
||||
|
||||
new_body, delimiter_tag = super(start_tag_name, tokenizer, parse_context)
|
||||
result << new_body
|
||||
|
||||
else_tag = delimiter_tag
|
||||
else_body = nil
|
||||
while delimiter_tag
|
||||
raise SyntaxError unless delimiter_tag.tag_name == 'else'
|
||||
else_tag = delimiter_tag
|
||||
else_body, delimiter_tag = super(start_tag_name, tokenizer, parse_context)
|
||||
end
|
||||
if else_tag
|
||||
result << else_tag.replaced_markup("") # markup was ignored in else tags
|
||||
result << else_body
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
def unknown_tag(tag, markup, tokens)
|
||||
return super unless tag == 'else'
|
||||
@else_block = BlockBody.new
|
||||
@else_block = new_body
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
@@ -86,13 +99,20 @@ module Liquid
|
||||
|
||||
protected
|
||||
|
||||
private_class_method def self.lax_migrate(tag_name, markup, tokenizer, parse_context)
|
||||
match = markup.match(Syntax) || raise(SyntaxError)
|
||||
new_collection_name = Expression.lax_migrate(match[2])
|
||||
new_markup = Utils.match_captures_replace(match, { 2 => new_collection_name }.compact)
|
||||
new_markup << Utils.migrate_tag_attributes(markup)
|
||||
end
|
||||
|
||||
def lax_parse(markup)
|
||||
if markup =~ Syntax
|
||||
@variable_name = Regexp.last_match(1)
|
||||
collection_name = Regexp.last_match(2)
|
||||
@reversed = !!Regexp.last_match(3)
|
||||
@name = "#{@variable_name}-#{collection_name}"
|
||||
@collection_name = Expression.parse(collection_name)
|
||||
@variable_name = Regexp.last_match(1)
|
||||
collection_name = Regexp.last_match(2)
|
||||
@reversed = !!Regexp.last_match(3)
|
||||
@name = "#{@variable_name}-#{collection_name}"
|
||||
@collection_name = parse_expression(collection_name)
|
||||
markup.scan(TagAttributes) do |key, value|
|
||||
set_attribute(key, value)
|
||||
end
|
||||
@@ -101,20 +121,27 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
private_class_method def self.strict_migrate(tag_name, markup, tokenizer, parse_context)
|
||||
markup
|
||||
end
|
||||
|
||||
def strict_parse(markup)
|
||||
p = Parser.new(markup)
|
||||
@variable_name = p.consume(:id)
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in") unless p.id?('in')
|
||||
collection_name = p.expression
|
||||
@name = "#{@variable_name}-#{collection_name}"
|
||||
@collection_name = Expression.parse(collection_name)
|
||||
|
||||
collection_name = p.expression
|
||||
@collection_name = parse_expression(collection_name)
|
||||
|
||||
@name = "#{@variable_name}-#{collection_name}"
|
||||
@reversed = p.id?('reversed')
|
||||
|
||||
while p.look(:id) && p.look(:colon, 1)
|
||||
unless attribute = p.id?('limit') || p.id?('offset')
|
||||
while p.look(:comma) || p.look(:id)
|
||||
p.consume?(:comma)
|
||||
unless (attribute = p.id?('limit') || p.id?('offset'))
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_attribute")
|
||||
end
|
||||
p.consume
|
||||
p.consume(:colon)
|
||||
set_attribute(attribute, p.expression)
|
||||
end
|
||||
p.consume(:end_of_string)
|
||||
@@ -156,7 +183,7 @@ module Liquid
|
||||
|
||||
def render_segment(context, output, segment)
|
||||
for_stack = context.registers[:for_stack] ||= []
|
||||
length = segment.length
|
||||
length = segment.length
|
||||
|
||||
context.stack do
|
||||
loop_vars = Liquid::ForloopDrop.new(@name, length, for_stack[-1])
|
||||
@@ -191,10 +218,10 @@ module Liquid
|
||||
@from = if expr == 'continue'
|
||||
:continue
|
||||
else
|
||||
Expression.parse(expr)
|
||||
parse_expression(expr)
|
||||
end
|
||||
when 'limit'
|
||||
@limit = Expression.parse(expr)
|
||||
@limit = parse_expression(expr)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
+108
-20
@@ -1,23 +1,58 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# If is the conditional block
|
||||
#
|
||||
# {% if user.admin %}
|
||||
# Admin user!
|
||||
# {% else %}
|
||||
# Not admin user
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category conditional
|
||||
# @liquid_name if
|
||||
# @liquid_summary
|
||||
# Renders an expression if a specific condition is `true`.
|
||||
# @liquid_syntax
|
||||
# {% if condition %}
|
||||
# expression
|
||||
# {% endif %}
|
||||
#
|
||||
# There are {% if count < 5 %} less {% else %} more {% endif %} items than you need.
|
||||
#
|
||||
# @liquid_syntax_keyword condition The condition to evaluate.
|
||||
# @liquid_syntax_keyword expression The expression to render if the condition is met.
|
||||
class If < Block
|
||||
Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o
|
||||
Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o
|
||||
ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o
|
||||
BOOLEAN_OPERATORS = %w(and or).freeze
|
||||
BOOLEAN_OPERATORS = %w(and or).freeze
|
||||
|
||||
attr_reader :blocks
|
||||
|
||||
def self.migrate(tag_name, markup, tokenizer, parse_context)
|
||||
new_markup = migrate_with_selected_parser(tag_name, markup, tokenizer, parse_context)
|
||||
new_body = migrate_body(tag_name, tokenizer, parse_context)
|
||||
[new_markup, new_body]
|
||||
end
|
||||
|
||||
def self.migrate_body(start_tag_name, tokenizer, parse_context)
|
||||
result = +""
|
||||
|
||||
loop do
|
||||
new_body, delimiter_tag = super(start_tag_name, tokenizer, parse_context)
|
||||
result << new_body
|
||||
|
||||
break unless delimiter_tag
|
||||
|
||||
case delimiter_tag.tag_name
|
||||
when "else"
|
||||
result << delimiter_tag.replaced_markup("") # markup was ignored on else tags
|
||||
when "elsif"
|
||||
new_markup = migrate_with_selected_parser(delimiter_tag.tag_name, delimiter_tag.markup, tokenizer, parse_context)
|
||||
result << delimiter_tag.replaced_markup(new_markup)
|
||||
else
|
||||
raise SyntaxError
|
||||
end
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
private_class_method def self.strict_migrate(tag_name, markup, tokenizer, parse_context)
|
||||
markup
|
||||
end
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
@blocks = []
|
||||
@@ -31,10 +66,17 @@ module Liquid
|
||||
def parse(tokens)
|
||||
while parse_body(@blocks.last.attachment, tokens)
|
||||
end
|
||||
@blocks.reverse_each do |block|
|
||||
block.attachment.remove_blank_strings if blank?
|
||||
block.attachment.freeze
|
||||
end
|
||||
end
|
||||
|
||||
ELSE_TAG_NAMES = ['elsif', 'else'].freeze
|
||||
private_constant :ELSE_TAG_NAMES
|
||||
|
||||
def unknown_tag(tag, markup, tokens)
|
||||
if ['elsif', 'else'].include?(tag)
|
||||
if ELSE_TAG_NAMES.include?(tag)
|
||||
push_block(tag, markup)
|
||||
else
|
||||
super
|
||||
@@ -43,7 +85,11 @@ module Liquid
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
@blocks.each do |block|
|
||||
if block.evaluate(context)
|
||||
result = Liquid::Utils.to_liquid_value(
|
||||
block.evaluate(context)
|
||||
)
|
||||
|
||||
if result
|
||||
return block.attachment.render_to_output_buffer(context, output)
|
||||
end
|
||||
end
|
||||
@@ -61,21 +107,29 @@ module Liquid
|
||||
end
|
||||
|
||||
@blocks.push(block)
|
||||
block.attach(BlockBody.new)
|
||||
block.attach(new_body)
|
||||
end
|
||||
|
||||
def parse_expression(markup)
|
||||
Condition.parse_expression(parse_context, markup)
|
||||
end
|
||||
|
||||
private_class_method def self.lax_migrate_expression(markup)
|
||||
Expression.lax_migrate(markup)
|
||||
end
|
||||
|
||||
def lax_parse(markup)
|
||||
expressions = markup.scan(ExpressionsAndOperators)
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop =~ Syntax
|
||||
|
||||
condition = Condition.new(Expression.parse(Regexp.last_match(1)), Regexp.last_match(2), Expression.parse(Regexp.last_match(3)))
|
||||
condition = Condition.new(parse_expression(Regexp.last_match(1)), Regexp.last_match(2), parse_expression(Regexp.last_match(3)))
|
||||
|
||||
until expressions.empty?
|
||||
operator = expressions.pop.to_s.strip
|
||||
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop.to_s =~ Syntax
|
||||
|
||||
new_condition = Condition.new(Expression.parse(Regexp.last_match(1)), Regexp.last_match(2), Expression.parse(Regexp.last_match(3)))
|
||||
new_condition = Condition.new(parse_expression(Regexp.last_match(1)), Regexp.last_match(2), parse_expression(Regexp.last_match(3)))
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.if") unless BOOLEAN_OPERATORS.include?(operator)
|
||||
new_condition.send(operator, condition)
|
||||
condition = new_condition
|
||||
@@ -84,6 +138,40 @@ module Liquid
|
||||
condition
|
||||
end
|
||||
|
||||
private_class_method def self.lax_migrate(tag_name, markup, tokenizer, parse_context)
|
||||
expressions = markup.scan(ExpressionsAndOperators)
|
||||
|
||||
new_markup = lax_migrate_condition(expressions.pop)
|
||||
until expressions.empty?
|
||||
operator = expressions.pop
|
||||
new_left_markup = lax_migrate_condition(expressions.pop)
|
||||
new_markup = new_left_markup << operator << new_markup
|
||||
end
|
||||
|
||||
new_markup
|
||||
end
|
||||
|
||||
private_class_method def self.lax_migrate_condition(markup)
|
||||
Utils.migrate_stripped(markup) do |markup|
|
||||
match = markup.match(Syntax)
|
||||
left = lax_migrate_expression(match[1])
|
||||
op = match[2]
|
||||
right_capture = match[3]
|
||||
if op
|
||||
right = lax_migrate_expression(right_capture) if right_capture
|
||||
elsif right_capture
|
||||
right = "" # remove right capture, since it is ignored with no operator
|
||||
end
|
||||
new_markup = Utils.match_captures_replace(match, { 1 => left, 2 => op, 3 => right }.compact)
|
||||
new_markup.prepend(' ') if match.begin(0) > 0
|
||||
new_markup << ' ' if match.end(0) < markup.length
|
||||
if op && !right # missing right operand missing
|
||||
new_markup << " nil" # replace with nil, which it was semantically treated as
|
||||
end
|
||||
new_markup
|
||||
end
|
||||
end
|
||||
|
||||
def strict_parse(markup)
|
||||
p = Parser.new(markup)
|
||||
condition = parse_binary_comparisons(p)
|
||||
@@ -94,7 +182,7 @@ module Liquid
|
||||
def parse_binary_comparisons(p)
|
||||
condition = parse_comparison(p)
|
||||
first_condition = condition
|
||||
while op = (p.id?('and') || p.id?('or'))
|
||||
while (op = (p.id?('and') || p.id?('or')))
|
||||
child_condition = parse_comparison(p)
|
||||
condition.send(op, child_condition)
|
||||
condition = child_condition
|
||||
@@ -103,9 +191,9 @@ module Liquid
|
||||
end
|
||||
|
||||
def parse_comparison(p)
|
||||
a = Expression.parse(p.expression)
|
||||
if op = p.consume?(:comparison)
|
||||
b = Expression.parse(p.expression)
|
||||
a = parse_expression(p.expression)
|
||||
if (op = p.consume?(:comparison))
|
||||
b = parse_expression(p.expression)
|
||||
Condition.new(a, op, b)
|
||||
else
|
||||
Condition.new(a)
|
||||
|
||||
@@ -2,6 +2,15 @@
|
||||
|
||||
module Liquid
|
||||
class Ifchanged < Block
|
||||
def self.migrate(tag_name, _markup, tokenizer, parse_context)
|
||||
new_markup = "" # markup was ignored
|
||||
|
||||
new_body, unknown_tag = migrate_body(tag_name, tokenizer, parse_context)
|
||||
raise SyntaxError if unknown_tag
|
||||
|
||||
[new_markup, new_body]
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
block_output = +''
|
||||
super(context, block_output)
|
||||
|
||||
+46
-24
@@ -1,39 +1,61 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# Include allows templates to relate with other templates
|
||||
#
|
||||
# Simply include another template:
|
||||
#
|
||||
# {% include 'product' %}
|
||||
#
|
||||
# Include a template with a local variable:
|
||||
#
|
||||
# {% include 'product' with products[0] %}
|
||||
#
|
||||
# Include a template for a collection:
|
||||
#
|
||||
# {% include 'product' for products %}
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category theme
|
||||
# @liquid_name include
|
||||
# @liquid_summary
|
||||
# Renders a [snippet](/themes/architecture#snippets).
|
||||
# @liquid_description
|
||||
# Inside the snippet, you can access and alter variables that are [created](/api/liquid/tags/variable-tags) outside of the
|
||||
# snippet.
|
||||
# @liquid_syntax
|
||||
# {% include 'filename' %}
|
||||
# @liquid_syntax_keyword filename The name of the snippet to render, without the `.liquid` extension.
|
||||
# @liquid_deprecated
|
||||
# Deprecated because the way that variables are handled reduces performance and makes code harder to both read and maintain.
|
||||
#
|
||||
# The `include` tag has been replaced by [`render`](/api/liquid/tags/render).
|
||||
class Include < Tag
|
||||
Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/o
|
||||
prepend Tag::Disableable
|
||||
|
||||
SYNTAX = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
|
||||
Syntax = SYNTAX
|
||||
|
||||
attr_reader :template_name_expr, :variable_name_expr, :attributes
|
||||
|
||||
def self.migrate(_tag_name, markup, _tokenizer, parse_context)
|
||||
match = markup.match(SYNTAX) || raise(SyntaxError)
|
||||
|
||||
template_name = Expression.lax_migrate(match[1])
|
||||
variable_name = Expression.lax_migrate(match[3]) if match[3]
|
||||
|
||||
new_markup = Utils.match_captures_replace(match, { 1 => template_name, 3 => variable_name }.compact)
|
||||
new_markup << Utils.migrate_tag_attributes(markup)
|
||||
|
||||
# replace scanned over characters with a space to ensure there is a space
|
||||
# to separate the tag name and the variable name
|
||||
new_markup.prepend(" ") if match.begin(0) > 0
|
||||
|
||||
new_markup
|
||||
end
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
|
||||
if markup =~ Syntax
|
||||
if markup =~ SYNTAX
|
||||
|
||||
template_name = Regexp.last_match(1)
|
||||
variable_name = Regexp.last_match(3)
|
||||
|
||||
@variable_name_expr = variable_name ? Expression.parse(variable_name) : nil
|
||||
@template_name_expr = Expression.parse(template_name)
|
||||
@attributes = {}
|
||||
@alias_name = Regexp.last_match(5)
|
||||
@variable_name_expr = variable_name ? parse_expression(variable_name) : nil
|
||||
@template_name_expr = parse_expression(template_name)
|
||||
@attributes = {}
|
||||
|
||||
markup.scan(TagAttributes) do |key, value|
|
||||
@attributes[key] = Expression.parse(value)
|
||||
@attributes[key] = parse_expression(value)
|
||||
end
|
||||
|
||||
else
|
||||
@@ -46,7 +68,7 @@ module Liquid
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
template_name = context.evaluate(@template_name_expr)
|
||||
raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name
|
||||
raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name.is_a?(String)
|
||||
|
||||
partial = PartialCache.load(
|
||||
template_name,
|
||||
@@ -54,7 +76,7 @@ module Liquid
|
||||
parse_context: parse_context
|
||||
)
|
||||
|
||||
context_variable_name = template_name.split('/').last
|
||||
context_variable_name = @alias_name || template_name.split('/').last
|
||||
|
||||
variable = if @variable_name_expr
|
||||
context.evaluate(@variable_name_expr)
|
||||
@@ -63,10 +85,10 @@ module Liquid
|
||||
end
|
||||
|
||||
old_template_name = context.template_name
|
||||
old_partial = context.partial
|
||||
old_partial = context.partial
|
||||
begin
|
||||
context.template_name = template_name
|
||||
context.partial = true
|
||||
context.partial = true
|
||||
context.stack do
|
||||
@attributes.each do |key, value|
|
||||
context[key] = context.evaluate(value)
|
||||
@@ -84,7 +106,7 @@ module Liquid
|
||||
end
|
||||
ensure
|
||||
context.template_name = old_template_name
|
||||
context.partial = old_partial
|
||||
context.partial = old_partial
|
||||
end
|
||||
|
||||
output
|
||||
|
||||
@@ -1,30 +1,39 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# increment is used in a place where one needs to insert a counter
|
||||
# into a template, and needs the counter to survive across
|
||||
# multiple instantiations of the template.
|
||||
# (To achieve the survival, the application must keep the context)
|
||||
#
|
||||
# if the variable does not exist, it is created with value 0.
|
||||
#
|
||||
# Hello: {% increment variable %}
|
||||
#
|
||||
# gives you:
|
||||
#
|
||||
# Hello: 0
|
||||
# Hello: 1
|
||||
# Hello: 2
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category variable
|
||||
# @liquid_name increment
|
||||
# @liquid_summary
|
||||
# Creates a new variable, with a default value of 0, that's increased by 1 with each subsequent call.
|
||||
# @liquid_description
|
||||
# Variables that are declared with `increment` are unique to the [layout](/themes/architecture/layouts), [template](/themes/architecture/templates),
|
||||
# or [section](/themes/architecture/sections) file that they're created in. However, the variable is shared across
|
||||
# [snippets](/themes/architecture#snippets) included in the file.
|
||||
#
|
||||
# Similarly, variables that are created with `increment` are independent from those created with [`assign`](/api/liquid/tags/assign)
|
||||
# and [`capture`](/api/liquid/tags/capture). However, `increment` and [`decrement`](/api/liquid/tags/decrement) share
|
||||
# variables.
|
||||
# @liquid_syntax
|
||||
# {% increment variable_name %}
|
||||
# @liquid_syntax_keyword variable_name The name of the variable being incremented.
|
||||
class Increment < Tag
|
||||
attr_reader :variable_name
|
||||
|
||||
def self.migrate(_tag_name, markup, _tokenizer, _parse_context)
|
||||
markup # no characters ignored, it just uses anything for the variable name
|
||||
end
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
@variable = markup.strip
|
||||
@variable_name = markup.strip
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
value = context.environments.first[@variable] ||= 0
|
||||
context.environments.first[@variable] = value + 1
|
||||
counter_environment = context.environments.first
|
||||
value = counter_environment[@variable_name] || 0
|
||||
counter_environment[@variable_name] = value + 1
|
||||
|
||||
output << value.to_s
|
||||
output
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class InlineComment < Tag
|
||||
def self.migrate(_tag_name, markup, _tokenizer, _parse_context)
|
||||
markup
|
||||
end
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
|
||||
# Semantically, a comment should only ignore everything after it on the line.
|
||||
# Currently, this implementation doesn't support mixing a comment with another tag
|
||||
# but we need to reserve future support for this and prevent the introduction
|
||||
# of inline comments from being backward incompatible change.
|
||||
#
|
||||
# As such, we're forcing users to put a # symbol on every line otherwise this
|
||||
# tag will throw an error.
|
||||
if markup.match?(/\n\s*[^#\s]/)
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.inline_comment_invalid")
|
||||
end
|
||||
end
|
||||
|
||||
def render_to_output_buffer(_context, output)
|
||||
output
|
||||
end
|
||||
|
||||
def blank?
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('#', InlineComment)
|
||||
end
|
||||
+41
-5
@@ -1,10 +1,46 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category syntax
|
||||
# @liquid_name raw
|
||||
# @liquid_summary
|
||||
# Outputs any Liquid code as text instead of rendering it.
|
||||
# @liquid_syntax
|
||||
# {% raw %}
|
||||
# expression
|
||||
# {% endraw %}
|
||||
# @liquid_syntax_keyword expression The expression to be output without being rendered.
|
||||
class Raw < Block
|
||||
Syntax = /\A\s*\z/
|
||||
FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
|
||||
|
||||
def self.migrate(tag_name, markup, tokenizer, parse_context)
|
||||
raise SyntaxError unless Syntax.match?(markup)
|
||||
|
||||
new_body = migrate_body(tag_name, tokenizer, parse_context)
|
||||
[markup, new_body]
|
||||
end
|
||||
|
||||
def self.migrate_body(start_tag_name, tokenizer, parse_context)
|
||||
block_delimiter = "end#{start_tag_name}"
|
||||
|
||||
body = +''
|
||||
while (token = tokenizer.shift)
|
||||
match = token.match(FullTokenPossiblyInvalid)
|
||||
if match && block_delimiter == match[2]
|
||||
body << Utils.match_captures_replace(match, { 3 => "" })
|
||||
return body
|
||||
end
|
||||
body << token unless token.empty?
|
||||
end
|
||||
|
||||
raise SyntaxError
|
||||
end
|
||||
|
||||
attr_reader :body
|
||||
|
||||
def initialize(tag_name, markup, parse_context)
|
||||
super
|
||||
|
||||
@@ -13,15 +49,15 @@ module Liquid
|
||||
|
||||
def parse(tokens)
|
||||
@body = +''
|
||||
while token = tokens.shift
|
||||
if token =~ FullTokenPossiblyInvalid
|
||||
while (token = tokens.shift)
|
||||
if token =~ FullTokenPossiblyInvalid && block_delimiter == Regexp.last_match(2)
|
||||
@body << Regexp.last_match(1) if Regexp.last_match(1) != ""
|
||||
return if block_delimiter == Regexp.last_match(2)
|
||||
return
|
||||
end
|
||||
@body << token unless token.empty?
|
||||
end
|
||||
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_never_closed", block_name: block_name)
|
||||
raise_tag_never_closed(block_name)
|
||||
end
|
||||
|
||||
def render_to_output_buffer(_context, output)
|
||||
@@ -40,7 +76,7 @@ module Liquid
|
||||
protected
|
||||
|
||||
def ensure_valid_markup(tag_name, markup, parse_context)
|
||||
unless markup =~ Syntax
|
||||
unless Syntax.match?(markup)
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_unexpected_args", tag: tag_name)
|
||||
end
|
||||
end
|
||||
|
||||
+86
-13
@@ -1,10 +1,53 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category theme
|
||||
# @liquid_name render
|
||||
# @liquid_summary
|
||||
# Renders a [snippet](/themes/architecture#snippets) or [app block](/themes/architecture/sections/section-schema#render-app-blocks).
|
||||
# @liquid_description
|
||||
# Inside snippets and app blocks, you can't directly access variables that are [created](/api/liquid/tags/variable-tags) outside
|
||||
# of the snippet or app block. However, you can [specify variables as parameters](/api/liquid/tags/render#render-passing-variables-to-a-snippet)
|
||||
# to pass outside variables to snippets.
|
||||
#
|
||||
# While you can't directly access created variables, you can access global objects, as well as any objects that are
|
||||
# directly accessible outside the snippet or app block. For example, a snippet or app block inside the [product template](/themes/architecture/templates/product)
|
||||
# can access the [`product` object](/api/liquid/objects/product), and a snippet or app block inside a [section](/themes/architecture/sections)
|
||||
# can access the [`section` object](/api/liquid/objects/section).
|
||||
#
|
||||
# Outside a snippet or app block, you can't access variables created inside the snippet or app block.
|
||||
#
|
||||
# > Note:
|
||||
# > When you render a snippet using the `render` tag, you can't use the [`include` tag](/api/liquid/tags/include)
|
||||
# > inside the snippet.
|
||||
# @liquid_syntax
|
||||
# {% render 'filename' %}
|
||||
# @liquid_syntax_keyword filename The name of the snippet to render, without the `.liquid` extension.
|
||||
class Render < Tag
|
||||
SYNTAX = /(#{QuotedString})#{QuotedFragment}*/o
|
||||
FOR = 'for'
|
||||
SYNTAX = /(#{QuotedString}+)(\s+(with|#{FOR})\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
|
||||
|
||||
attr_reader :template_name_expr, :attributes
|
||||
def self.migrate(_tag_name, markup, _tokenizer, parse_context)
|
||||
match = markup.match(SYNTAX)
|
||||
|
||||
template_name = Expression.lax_migrate(match[1])
|
||||
variable_name = Expression.lax_migrate(match[4]) if match[4]
|
||||
|
||||
new_markup = Utils.match_captures_replace(match, { 1 => template_name, 4 => variable_name }.compact)
|
||||
new_markup << Utils.migrate_tag_attributes(markup)
|
||||
|
||||
# replace scanned over characters with a space to ensure there is a space
|
||||
# to separate the tag name and the variable name
|
||||
new_markup.prepend(" ") if match.begin(0) > 0
|
||||
|
||||
new_markup
|
||||
end
|
||||
|
||||
disable_tags "include"
|
||||
|
||||
attr_reader :template_name_expr, :variable_name_expr, :attributes, :alias_name
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
@@ -12,19 +55,32 @@ module Liquid
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.render") unless markup =~ SYNTAX
|
||||
|
||||
template_name = Regexp.last_match(1)
|
||||
with_or_for = Regexp.last_match(3)
|
||||
variable_name = Regexp.last_match(4)
|
||||
|
||||
@template_name_expr = Expression.parse(template_name)
|
||||
@alias_name = Regexp.last_match(6)
|
||||
@variable_name_expr = variable_name ? parse_expression(variable_name) : nil
|
||||
@template_name_expr = parse_expression(template_name)
|
||||
@is_for_loop = (with_or_for == FOR)
|
||||
|
||||
@attributes = {}
|
||||
markup.scan(TagAttributes) do |key, value|
|
||||
@attributes[key] = Expression.parse(value)
|
||||
@attributes[key] = parse_expression(value)
|
||||
end
|
||||
end
|
||||
|
||||
def for_loop?
|
||||
@is_for_loop
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
# Though we evaluate this here we will only ever parse it as a string literal.
|
||||
template_name = context.evaluate(@template_name_expr)
|
||||
raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name
|
||||
render_tag(context, output)
|
||||
end
|
||||
|
||||
def render_tag(context, output)
|
||||
# The expression should be a String literal, which parses to a String object
|
||||
template_name = @template_name_expr
|
||||
raise ::ArgumentError unless template_name.is_a?(String)
|
||||
|
||||
partial = PartialCache.load(
|
||||
template_name,
|
||||
@@ -32,13 +88,29 @@ module Liquid
|
||||
parse_context: parse_context
|
||||
)
|
||||
|
||||
inner_context = context.new_isolated_subcontext
|
||||
inner_context.template_name = template_name
|
||||
inner_context.partial = true
|
||||
@attributes.each do |key, value|
|
||||
inner_context[key] = context.evaluate(value)
|
||||
context_variable_name = @alias_name || template_name.split('/').last
|
||||
|
||||
render_partial_func = ->(var, forloop) {
|
||||
inner_context = context.new_isolated_subcontext
|
||||
inner_context.template_name = template_name
|
||||
inner_context.partial = true
|
||||
inner_context['forloop'] = forloop if forloop
|
||||
|
||||
@attributes.each do |key, value|
|
||||
inner_context[key] = context.evaluate(value)
|
||||
end
|
||||
inner_context[context_variable_name] = var unless var.nil?
|
||||
partial.render_to_output_buffer(inner_context, output)
|
||||
forloop&.send(:increment!)
|
||||
}
|
||||
|
||||
variable = @variable_name_expr ? context.evaluate(@variable_name_expr) : nil
|
||||
if @is_for_loop && variable.respond_to?(:each) && variable.respond_to?(:count)
|
||||
forloop = Liquid::ForloopDrop.new(template_name, variable.count, nil)
|
||||
variable.each { |var| render_partial_func.call(var, forloop) }
|
||||
else
|
||||
render_partial_func.call(variable, nil)
|
||||
end
|
||||
partial.render_to_output_buffer(inner_context, output)
|
||||
|
||||
output
|
||||
end
|
||||
@@ -47,6 +119,7 @@ module Liquid
|
||||
def children
|
||||
[
|
||||
@node.template_name_expr,
|
||||
@node.variable_name_expr,
|
||||
] + @node.attributes.values
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,19 +1,55 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category iteration
|
||||
# @liquid_name tablerow
|
||||
# @liquid_summary
|
||||
# Generates HTML table rows for every item in an array.
|
||||
# @liquid_description
|
||||
# The `tablerow` tag must be wrapped in HTML `<table>` and `</table>` tags.
|
||||
#
|
||||
# > Tip:
|
||||
# > Every `tablerow` loop has an associated [`tablerowloop` object](/api/liquid/objects/tablerowloop) with information about the loop.
|
||||
# @liquid_syntax
|
||||
# {% tablerow variable in array %}
|
||||
# expression
|
||||
# {% endtablerow %}
|
||||
# @liquid_syntax_keyword variable The current item in the array.
|
||||
# @liquid_syntax_keyword array The array to iterate over.
|
||||
# @liquid_syntax_keyword expression The expression to render.
|
||||
# @liquid_optional_param cols [number] The number of columns that the table should have.
|
||||
# @liquid_optional_param limit [number] The number of iterations to perform.
|
||||
# @liquid_optional_param offset [number] The 1-based index to start iterating at.
|
||||
# @liquid_optional_param range [untyped] A custom numeric range to iterate over.
|
||||
class TableRow < Block
|
||||
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
|
||||
|
||||
def self.migrate(tag_name, markup, tokenizer, parse_context)
|
||||
match = markup.match(Syntax) || raise(SyntaxError)
|
||||
|
||||
new_collection_name = Expression.lax_migrate(match[2])
|
||||
|
||||
new_markup = Utils.match_captures_replace(match, { 2 => new_collection_name }.compact)
|
||||
new_markup << Utils.migrate_tag_attributes(markup)
|
||||
|
||||
new_body, unknown_tag = migrate_body(tag_name, tokenizer, parse_context)
|
||||
raise SyntaxError if unknown_tag
|
||||
|
||||
[new_markup, new_body]
|
||||
end
|
||||
|
||||
attr_reader :variable_name, :collection_name, :attributes
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
if markup =~ Syntax
|
||||
@variable_name = Regexp.last_match(1)
|
||||
@collection_name = Expression.parse(Regexp.last_match(2))
|
||||
@attributes = {}
|
||||
@variable_name = Regexp.last_match(1)
|
||||
@collection_name = parse_expression(Regexp.last_match(2))
|
||||
@attributes = {}
|
||||
markup.scan(TagAttributes) do |key, value|
|
||||
@attributes[key] = Expression.parse(value)
|
||||
@attributes[key] = parse_expression(value)
|
||||
end
|
||||
else
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.table_row")
|
||||
@@ -24,13 +60,12 @@ module Liquid
|
||||
(collection = context.evaluate(@collection_name)) || (return '')
|
||||
|
||||
from = @attributes.key?('offset') ? context.evaluate(@attributes['offset']).to_i : 0
|
||||
to = @attributes.key?('limit') ? from + context.evaluate(@attributes['limit']).to_i : nil
|
||||
to = @attributes.key?('limit') ? from + context.evaluate(@attributes['limit']).to_i : nil
|
||||
|
||||
collection = Utils.slice_collection(collection, from, to)
|
||||
length = collection.length
|
||||
|
||||
length = collection.length
|
||||
|
||||
cols = context.evaluate(@attributes['cols']).to_i
|
||||
cols = @attributes.key?('cols') ? context.evaluate(@attributes['cols']).to_i : length
|
||||
|
||||
output << "<tr class=\"row1\">\n"
|
||||
context.stack do
|
||||
|
||||
@@ -3,21 +3,40 @@
|
||||
require_relative 'if'
|
||||
|
||||
module Liquid
|
||||
# Unless is a conditional just like 'if' but works on the inverse logic.
|
||||
#
|
||||
# {% unless x < 0 %} x is greater than zero {% endunless %}
|
||||
#
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category conditional
|
||||
# @liquid_name unless
|
||||
# @liquid_summary
|
||||
# Renders an expression unless a specific condition is `true`.
|
||||
# @liquid_description
|
||||
# > Tip:
|
||||
# > Similar to the [`if` tag](/api/liquid/tags/if), you can use `elsif` to add more conditions to an `unless` tag.
|
||||
# @liquid_syntax
|
||||
# {% unless condition %}
|
||||
# expression
|
||||
# {% endunless %}
|
||||
# @liquid_syntax_keyword condition The condition to evaluate.
|
||||
# @liquid_syntax_keyword expression The expression to render unless the condition is met.
|
||||
class Unless < If
|
||||
def render_to_output_buffer(context, output)
|
||||
# First condition is interpreted backwards ( if not )
|
||||
first_block = @blocks.first
|
||||
unless first_block.evaluate(context)
|
||||
result = Liquid::Utils.to_liquid_value(
|
||||
first_block.evaluate(context)
|
||||
)
|
||||
|
||||
unless result
|
||||
return first_block.attachment.render_to_output_buffer(context, output)
|
||||
end
|
||||
|
||||
# After the first condition unless works just like if
|
||||
@blocks[1..-1].each do |block|
|
||||
if block.evaluate(context)
|
||||
result = Liquid::Utils.to_liquid_value(
|
||||
block.evaluate(context)
|
||||
)
|
||||
|
||||
if result
|
||||
return block.attachment.render_to_output_buffer(context, output)
|
||||
end
|
||||
end
|
||||
|
||||
+53
-70
@@ -18,13 +18,11 @@ module Liquid
|
||||
attr_accessor :root
|
||||
attr_reader :resource_limits, :warnings
|
||||
|
||||
@@file_system = BlankFileSystem.new
|
||||
|
||||
class TagRegistry
|
||||
include Enumerable
|
||||
|
||||
def initialize
|
||||
@tags = {}
|
||||
@tags = {}
|
||||
@cache = {}
|
||||
end
|
||||
|
||||
@@ -63,79 +61,68 @@ module Liquid
|
||||
# :lax acts like liquid 2.5 and silently ignores malformed tags in most cases.
|
||||
# :warn is the default and will give deprecation warnings when invalid syntax is used.
|
||||
# :strict will enforce correct syntax.
|
||||
attr_writer :error_mode
|
||||
|
||||
# Sets how strict the taint checker should be.
|
||||
# :lax is the default, and ignores the taint flag completely
|
||||
# :warn adds a warning, but does not interrupt the rendering
|
||||
# :error raises an error when tainted output is used
|
||||
attr_writer :taint_mode
|
||||
attr_accessor :error_mode
|
||||
Template.error_mode = :lax
|
||||
|
||||
attr_accessor :default_exception_renderer
|
||||
Template.default_exception_renderer = lambda do |exception|
|
||||
exception
|
||||
end
|
||||
|
||||
def file_system
|
||||
@@file_system
|
||||
end
|
||||
attr_accessor :file_system
|
||||
Template.file_system = BlankFileSystem.new
|
||||
|
||||
def file_system=(obj)
|
||||
@@file_system = obj
|
||||
end
|
||||
attr_accessor :tags
|
||||
Template.tags = TagRegistry.new
|
||||
private :tags=
|
||||
|
||||
def register_tag(name, klass)
|
||||
tags[name.to_s] = klass
|
||||
end
|
||||
|
||||
def tags
|
||||
@tags ||= TagRegistry.new
|
||||
end
|
||||
|
||||
def error_mode
|
||||
@error_mode ||= :lax
|
||||
end
|
||||
|
||||
def taint_mode
|
||||
@taint_mode ||= :lax
|
||||
end
|
||||
|
||||
# Pass a module with filter methods which should be available
|
||||
# to all liquid views. Good for registering the standard library
|
||||
def register_filter(mod)
|
||||
Strainer.global_filter(mod)
|
||||
StrainerFactory.add_global_filter(mod)
|
||||
end
|
||||
|
||||
def default_resource_limits
|
||||
@default_resource_limits ||= {}
|
||||
end
|
||||
attr_accessor :default_resource_limits
|
||||
Template.default_resource_limits = {}
|
||||
private :default_resource_limits=
|
||||
|
||||
# creates a new <tt>Template</tt> object from liquid source code
|
||||
# To enable profiling, pass in <tt>profile: true</tt> as an option.
|
||||
# See Liquid::Profiler for more information
|
||||
def parse(source, options = {})
|
||||
template = Template.new
|
||||
template.parse(source, options)
|
||||
new.parse(source, options)
|
||||
end
|
||||
|
||||
def migrate(source, parse_options = {})
|
||||
parse(source, parse_options) # raise if source has syntax errors
|
||||
new.migrate(source, parse_options)
|
||||
end
|
||||
end
|
||||
|
||||
def initialize
|
||||
@rethrow_errors = false
|
||||
@resource_limits = ResourceLimits.new(self.class.default_resource_limits)
|
||||
@rethrow_errors = false
|
||||
@resource_limits = ResourceLimits.new(Template.default_resource_limits)
|
||||
end
|
||||
|
||||
# Parse source code.
|
||||
# Returns self for easy chaining
|
||||
def parse(source, options = {})
|
||||
@options = options
|
||||
@profiling = options[:profile]
|
||||
@line_numbers = options[:line_numbers] || @profiling
|
||||
parse_context = options.is_a?(ParseContext) ? options : ParseContext.new(options)
|
||||
@root = Document.parse(tokenize(source), parse_context)
|
||||
@warnings = parse_context.warnings
|
||||
parse_context = configure_options(options)
|
||||
tokenizer = parse_context.new_tokenizer(source, start_line_number: @line_numbers && 1)
|
||||
@root = Document.parse(tokenizer, parse_context)
|
||||
self
|
||||
end
|
||||
|
||||
def migrate(source, parse_options = {})
|
||||
parse_context = configure_options(parse_options.merge(disable_liquid_c_nodes: true))
|
||||
tokenizer = parse_context.new_tokenizer(source, start_line_number: @line_numbers && 1)
|
||||
Document.migrate(tokenizer, parse_context)
|
||||
end
|
||||
|
||||
def registers
|
||||
@registers ||= {}
|
||||
end
|
||||
@@ -174,12 +161,12 @@ module Liquid
|
||||
c = args.shift
|
||||
|
||||
if @rethrow_errors
|
||||
c.exception_renderer = ->(_e) { raise }
|
||||
c.exception_renderer = Liquid::RAISE_EXCEPTION_LAMBDA
|
||||
end
|
||||
|
||||
c
|
||||
when Liquid::Drop
|
||||
drop = args.shift
|
||||
drop = args.shift
|
||||
drop.context = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits)
|
||||
when Hash
|
||||
Context.new([args.shift, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits)
|
||||
@@ -194,9 +181,12 @@ module Liquid
|
||||
case args.last
|
||||
when Hash
|
||||
options = args.pop
|
||||
output = options[:output] if options[:output]
|
||||
output = options[:output] if options[:output]
|
||||
static_registers = context.registers.static
|
||||
|
||||
registers.merge!(options[:registers]) if options[:registers].is_a?(Hash)
|
||||
options[:registers]&.each do |key, register|
|
||||
static_registers[key] = register
|
||||
end
|
||||
|
||||
apply_options_to_context(context, options)
|
||||
when Module, Array
|
||||
@@ -206,12 +196,13 @@ module Liquid
|
||||
# Retrying a render resets resource usage
|
||||
context.resource_limits.reset
|
||||
|
||||
if @profiling && context.profiler.nil?
|
||||
@profiler = context.profiler = Liquid::Profiler.new
|
||||
end
|
||||
|
||||
begin
|
||||
# render the nodelist.
|
||||
# for performance reasons we get an array back here. join will make a string out of it.
|
||||
with_profiling(context) do
|
||||
@root.render_to_output_buffer(context, output || +'')
|
||||
end
|
||||
@root.render_to_output_buffer(context, output || +'')
|
||||
rescue Liquid::MemoryError => e
|
||||
context.handle_error(e)
|
||||
ensure
|
||||
@@ -230,33 +221,25 @@ module Liquid
|
||||
|
||||
private
|
||||
|
||||
def tokenize(source)
|
||||
Tokenizer.new(source, @line_numbers)
|
||||
end
|
||||
|
||||
def with_profiling(context)
|
||||
if @profiling && !context.partial
|
||||
def configure_options(options)
|
||||
if (profiling = options[:profile])
|
||||
raise "Profiler not loaded, require 'liquid/profiler' first" unless defined?(Liquid::Profiler)
|
||||
|
||||
@profiler = Profiler.new
|
||||
@profiler.start
|
||||
|
||||
begin
|
||||
yield
|
||||
ensure
|
||||
@profiler.stop
|
||||
end
|
||||
else
|
||||
yield
|
||||
end
|
||||
|
||||
@options = options
|
||||
@profiling = profiling
|
||||
@line_numbers = options[:line_numbers] || @profiling
|
||||
parse_context = options.is_a?(ParseContext) ? options : ParseContext.new(options)
|
||||
@warnings = parse_context.warnings
|
||||
parse_context
|
||||
end
|
||||
|
||||
def apply_options_to_context(context, options)
|
||||
context.add_filters(options[:filters]) if options[:filters]
|
||||
context.global_filter = options[:global_filter] if options[:global_filter]
|
||||
context.global_filter = options[:global_filter] if options[:global_filter]
|
||||
context.exception_renderer = options[:exception_renderer] if options[:exception_renderer]
|
||||
context.strict_variables = options[:strict_variables] if options[:strict_variables]
|
||||
context.strict_filters = options[:strict_filters] if options[:strict_filters]
|
||||
context.strict_variables = options[:strict_variables] if options[:strict_variables]
|
||||
context.strict_filters = options[:strict_filters] if options[:strict_filters]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class TemplateFactory
|
||||
def for(_template_name)
|
||||
Liquid::Template.new
|
||||
end
|
||||
end
|
||||
end
|
||||
+16
-6
@@ -5,14 +5,18 @@ module Liquid
|
||||
attr_reader :line_number, :for_liquid_tag
|
||||
|
||||
def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false)
|
||||
@source = source
|
||||
@line_number = line_number || (line_numbers ? 1 : nil)
|
||||
@source = source.to_s.to_str
|
||||
@line_number = line_number || (line_numbers ? 1 : nil)
|
||||
@for_liquid_tag = for_liquid_tag
|
||||
@tokens = tokenize
|
||||
@offset = 0
|
||||
@tokens = tokenize
|
||||
end
|
||||
|
||||
def shift
|
||||
(token = @tokens.shift) || return
|
||||
token = @tokens[@offset]
|
||||
return nil unless token
|
||||
|
||||
@offset += 1
|
||||
|
||||
if @line_number
|
||||
@line_number += @for_liquid_tag ? 1 : token.count("\n")
|
||||
@@ -21,17 +25,23 @@ module Liquid
|
||||
token
|
||||
end
|
||||
|
||||
def more?
|
||||
@offset < @tokens.length
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def tokenize
|
||||
return [] if @source.to_s.empty?
|
||||
return [] if @source.empty?
|
||||
|
||||
return @source.split("\n") if @for_liquid_tag
|
||||
|
||||
tokens = @source.split(TemplateParser)
|
||||
|
||||
# removes the rogue empty element at the beginning of the array
|
||||
tokens.shift if tokens[0]&.empty?
|
||||
if tokens[0]&.empty?
|
||||
@offset += 1
|
||||
end
|
||||
|
||||
tokens
|
||||
end
|
||||
|
||||
+53
-2
@@ -12,7 +12,7 @@ module Liquid
|
||||
|
||||
def self.slice_collection_using_each(collection, from, to)
|
||||
segments = []
|
||||
index = 0
|
||||
index = 0
|
||||
|
||||
# Maintains Ruby 1.8.7 String#each behaviour on 1.9
|
||||
if collection.is_a?(String)
|
||||
@@ -52,7 +52,7 @@ module Liquid
|
||||
when Numeric
|
||||
obj
|
||||
when String
|
||||
obj.strip =~ /\A-?\d+\.\d+\z/ ? BigDecimal(obj) : obj.to_i
|
||||
/\A-?\d+\.\d+\z/.match?(obj.strip) ? BigDecimal(obj) : obj.to_i
|
||||
else
|
||||
if obj.respond_to?(:to_number)
|
||||
obj.to_number
|
||||
@@ -81,5 +81,56 @@ module Liquid
|
||||
rescue ::ArgumentError
|
||||
nil
|
||||
end
|
||||
|
||||
def self.to_liquid_value(obj)
|
||||
# Enable "obj" to represent itself as a primitive value like integer, string, or boolean
|
||||
return obj.to_liquid_value if obj.respond_to?(:to_liquid_value)
|
||||
|
||||
# Otherwise return the object itself
|
||||
obj
|
||||
end
|
||||
|
||||
def self.migrate_stripped(markup)
|
||||
match = markup.match(/\A\s*(.*?)\s*\z/m)
|
||||
new_markup = yield match[1]
|
||||
Utils.match_captures_replace(match, 1 => new_markup)
|
||||
end
|
||||
|
||||
def self.migrate_tag_attributes(markup)
|
||||
attributes = []
|
||||
markup.scan(/\s*,?\s*#{TagAttributes}/) do
|
||||
tag_match = Regexp.last_match
|
||||
new_value_markup = Expression.lax_migrate(tag_match[2])
|
||||
attribute_markup = Utils.match_captures_replace(tag_match, { 2 => new_value_markup })
|
||||
unless attribute_markup.match?(/\A[,\s]/)
|
||||
attribute_markup.prepend(", ")
|
||||
end
|
||||
attributes << attribute_markup
|
||||
end
|
||||
return "" if attributes.empty?
|
||||
attributes.join
|
||||
end
|
||||
|
||||
# @api private
|
||||
def self.match_capture_replace(match, capture_number, replacement_string)
|
||||
match_captures_replace(match, { capture_number => replacement_string })
|
||||
end
|
||||
|
||||
def self.match_captures_replace(match, replacements = {})
|
||||
new_string = match[0].dup
|
||||
capture_numbers = replacements.keys
|
||||
unless capture_numbers.all?(Integer)
|
||||
raise TypeError, "Currently, only numbered captures are supported"
|
||||
end
|
||||
# replace from later captures first, to avoid affecting the position for following replacements
|
||||
match_begin = match.begin(0)
|
||||
capture_numbers.sort.reverse_each do |capture_number|
|
||||
replacement_string = replacements.fetch(capture_number)
|
||||
capture_start = match.begin(capture_number)
|
||||
capture_length = match.end(capture_number) - capture_start
|
||||
new_string[capture_start - match_begin, capture_length] = replacement_string
|
||||
end
|
||||
new_string
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+104
-40
@@ -12,10 +12,10 @@ module Liquid
|
||||
# {{ user | link }}
|
||||
#
|
||||
class Variable
|
||||
FilterMarkupRegex = /#{FilterSeparator}\s*(.*)/om
|
||||
FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o
|
||||
FilterArgsRegex = /(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o
|
||||
JustTagAttributes = /\A#{TagAttributes}\z/o
|
||||
FilterMarkupRegex = /#{FilterSeparator}\s*(.*)/om
|
||||
FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o
|
||||
FilterArgsRegex = /(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o
|
||||
JustTagAttributes = /\A#{TagAttributes}\z/o
|
||||
MarkupWithQuotedFragment = /(#{QuotedFragment})(.*)/om
|
||||
|
||||
attr_accessor :filters, :name, :line_number
|
||||
@@ -25,12 +25,12 @@ module Liquid
|
||||
include ParserSwitching
|
||||
|
||||
def initialize(markup, parse_context)
|
||||
@markup = markup
|
||||
@name = nil
|
||||
@markup = markup
|
||||
@name = nil
|
||||
@parse_context = parse_context
|
||||
@line_number = parse_context.line_number
|
||||
@line_number = parse_context.line_number
|
||||
|
||||
parse_with_selected_parser(markup)
|
||||
strict_parse_with_error_mode_fallback(markup)
|
||||
end
|
||||
|
||||
def raw
|
||||
@@ -41,13 +41,86 @@ module Liquid
|
||||
"in \"{{#{markup}}}\""
|
||||
end
|
||||
|
||||
STRICT_PARSE_CONTEXT = ParseContext.new(error_mode: :strict)
|
||||
private_constant :STRICT_PARSE_CONTEXT
|
||||
|
||||
def self.migrate(markup, parse_context)
|
||||
new(markup, STRICT_PARSE_CONTEXT)
|
||||
# TODO: migrate non-integer range expression literals
|
||||
markup
|
||||
rescue Liquid::SyntaxError
|
||||
raise if parse_context.error_mode == :strict
|
||||
lax_migrate(markup, parse_context)
|
||||
end
|
||||
|
||||
def self.lax_migrate(markup, parse_context)
|
||||
# unanchored match that may skip over characters preceding the name expression
|
||||
markup_match = markup.match(MarkupWithQuotedFragment)
|
||||
unless markup_match
|
||||
# Treated as a blank variable (e.g. `{{ -}}`), which outputs nothing
|
||||
# but may still have an effect on whitespace trimming
|
||||
return ""
|
||||
end
|
||||
|
||||
name_markup = markup_match[1]
|
||||
filters_markup = markup_match[2]
|
||||
|
||||
new_name_markup = Expression.lax_migrate(name_markup)
|
||||
|
||||
new_filter_markup = ""
|
||||
# unanchored match that may skip over characters preceding the pipe for the first filter
|
||||
if (filters_match = filters_markup.match(/\s*#{FilterMarkupRegex}/o))
|
||||
filters = filters_match[1].scan(FilterParser) # may skip over unterminated quote characters
|
||||
filters.map! do |f|
|
||||
filter_match = f.match(/\A(\s*)\W*(\w+)(\s*)/)
|
||||
next unless filter_match
|
||||
|
||||
# omit non-word characters preceding the filter name that the lax parser skips over
|
||||
transformed_filter = +"#{filter_match[1]}#{filter_match[2]}#{filter_match[3]}"
|
||||
|
||||
filter_args = []
|
||||
f.scan(/#{FilterArgsRegex}\s*/o) do # may skip over characters before the argument separator
|
||||
filter_arg_match = Regexp.last_match
|
||||
new_filter_arg = lax_migrate_filter_argument(filter_arg_match[1])
|
||||
filter_arg_string = Utils.match_captures_replace(filter_arg_match, 1 => new_filter_arg)
|
||||
filter_arg_string = filter_arg_string[1...] # remove separator character
|
||||
filter_args << filter_arg_string
|
||||
end
|
||||
|
||||
unless filter_args.empty?
|
||||
transformed_filter << ":" << filter_args.join(",")
|
||||
end
|
||||
|
||||
transformed_filter
|
||||
end
|
||||
filters.compact!
|
||||
new_filters_markup = filters.join('|')
|
||||
|
||||
# include pipe separator along with whitespace surrounding it
|
||||
new_filter_markup = Utils.match_captures_replace(filters_match, 1 => new_filters_markup)
|
||||
end
|
||||
|
||||
new_markup = Utils.match_captures_replace(markup_match, 1 => new_name_markup, 2 => new_filter_markup)
|
||||
new_markup.prepend(" ") if markup_match.begin(0) > 0
|
||||
new_markup
|
||||
end
|
||||
|
||||
def self.lax_migrate_filter_argument(unparsed_arg)
|
||||
if (match = unparsed_arg.match(JustTagAttributes))
|
||||
new_value_markup = Expression.lax_migrate(match[2])
|
||||
Utils.match_captures_replace(match, 2 => new_value_markup)
|
||||
else
|
||||
Expression.lax_migrate(unparsed_arg)
|
||||
end
|
||||
end
|
||||
|
||||
def lax_parse(markup)
|
||||
@filters = []
|
||||
return unless markup =~ MarkupWithQuotedFragment
|
||||
|
||||
name_markup = Regexp.last_match(1)
|
||||
name_markup = Regexp.last_match(1)
|
||||
filter_markup = Regexp.last_match(2)
|
||||
@name = Expression.parse(name_markup)
|
||||
@name = parse_context.parse_expression(name_markup)
|
||||
if filter_markup =~ FilterMarkupRegex
|
||||
filters = Regexp.last_match(1).scan(FilterParser)
|
||||
filters.each do |f|
|
||||
@@ -63,7 +136,9 @@ module Liquid
|
||||
@filters = []
|
||||
p = Parser.new(markup)
|
||||
|
||||
@name = Expression.parse(p.expression)
|
||||
return if p.look(:end_of_string)
|
||||
|
||||
@name = parse_context.parse_expression(p.expression)
|
||||
while p.consume?(:pipe)
|
||||
filtername = p.consume(:id)
|
||||
filterargs = p.consume?(:colon) ? parse_filterargs(p) : []
|
||||
@@ -81,14 +156,14 @@ module Liquid
|
||||
end
|
||||
|
||||
def render(context)
|
||||
obj = @filters.inject(context.evaluate(@name)) do |output, (filter_name, filter_args, filter_kwargs)|
|
||||
obj = context.evaluate(@name)
|
||||
|
||||
@filters.each do |filter_name, filter_args, filter_kwargs|
|
||||
filter_args = evaluate_filter_expressions(context, filter_args, filter_kwargs)
|
||||
context.invoke(filter_name, output, *filter_args)
|
||||
obj = context.invoke(filter_name, obj, *filter_args)
|
||||
end
|
||||
|
||||
obj = context.apply_global_filter(obj)
|
||||
taint_check(context, obj)
|
||||
obj
|
||||
context.apply_global_filter(obj)
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
@@ -104,17 +179,25 @@ module Liquid
|
||||
output
|
||||
end
|
||||
|
||||
def disabled?(_context)
|
||||
false
|
||||
end
|
||||
|
||||
def disabled_tags
|
||||
[]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse_filter_expressions(filter_name, unparsed_args)
|
||||
filter_args = []
|
||||
filter_args = []
|
||||
keyword_args = nil
|
||||
unparsed_args.each do |a|
|
||||
if matches = a.match(JustTagAttributes)
|
||||
keyword_args ||= {}
|
||||
keyword_args[matches[1]] = Expression.parse(matches[2])
|
||||
if (matches = a.match(JustTagAttributes))
|
||||
keyword_args ||= {}
|
||||
keyword_args[matches[1]] = parse_context.parse_expression(matches[2])
|
||||
else
|
||||
filter_args << Expression.parse(a)
|
||||
filter_args << parse_context.parse_expression(a)
|
||||
end
|
||||
end
|
||||
result = [filter_name, filter_args]
|
||||
@@ -134,25 +217,6 @@ module Liquid
|
||||
parsed_args
|
||||
end
|
||||
|
||||
def taint_check(context, obj)
|
||||
return unless obj.tainted?
|
||||
return if Template.taint_mode == :lax
|
||||
|
||||
@markup =~ QuotedFragment
|
||||
name = Regexp.last_match(0)
|
||||
|
||||
error = TaintedError.new("variable '#{name}' is tainted and was not escaped")
|
||||
error.line_number = line_number
|
||||
error.template_name = context.template_name
|
||||
|
||||
case Template.taint_mode
|
||||
when :warn
|
||||
context.warnings << error
|
||||
when :error
|
||||
raise error
|
||||
end
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
[@node.name] + @node.filters.flatten
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
module Liquid
|
||||
class VariableLookup
|
||||
SQUARE_BRACKETED = /\A\[(.*)\]\z/m
|
||||
COMMAND_METHODS = ['size', 'first', 'last'].freeze
|
||||
|
||||
attr_reader :name, :lookups
|
||||
@@ -11,35 +10,79 @@ module Liquid
|
||||
new(markup)
|
||||
end
|
||||
|
||||
LITERALS = Expression::LITERALS.keys.freeze
|
||||
private_constant :LITERALS
|
||||
|
||||
def self.lax_migrate(markup)
|
||||
new_markup = nil
|
||||
last_match = nil
|
||||
first_match = nil
|
||||
markup.scan(VariableParser) do |lookup|
|
||||
last_match = Regexp.last_match
|
||||
first_match ||= last_match
|
||||
new_markup ||= +""
|
||||
if lookup&.start_with?('[') && lookup&.end_with?(']')
|
||||
new_markup << "[" << Expression.lax_migrate(lookup[1..-2]) << "]"
|
||||
elsif !lookup.match?(/\A#{Liquid::Lexer::IDENTIFIER}\z/)
|
||||
# quote non-strictly valid identifiers
|
||||
new_markup << "['" << lookup << "']"
|
||||
else
|
||||
new_markup << "." unless new_markup.empty?
|
||||
new_markup << lookup
|
||||
end
|
||||
end
|
||||
|
||||
case new_markup
|
||||
when nil
|
||||
# `markup.scan(VariableParser)` may skip over all characters
|
||||
new_markup ||= " nil "
|
||||
when Expression::INTEGERS_REGEX, Expression::RANGES_REGEX, Expression::FLOATS_REGEX, *LITERALS
|
||||
# Quote variable lookups that match literals after characters are skipped by regex scanning
|
||||
new_markup = "['#{new_markup}']"
|
||||
else
|
||||
new_markup.prepend(" ") if first_match.begin(0) > 0
|
||||
new_markup << ' ' if last_match.end(0) < markup.length
|
||||
end
|
||||
|
||||
new_markup
|
||||
end
|
||||
|
||||
def initialize(markup)
|
||||
lookups = markup.scan(VariableParser)
|
||||
|
||||
name = lookups.shift
|
||||
if name =~ SQUARE_BRACKETED
|
||||
name = Expression.parse(Regexp.last_match(1))
|
||||
if name&.start_with?('[') && name&.end_with?(']')
|
||||
name = Expression.parse(name[1..-2])
|
||||
end
|
||||
@name = name
|
||||
|
||||
@lookups = lookups
|
||||
@lookups = lookups
|
||||
@command_flags = 0
|
||||
|
||||
@lookups.each_index do |i|
|
||||
lookup = lookups[i]
|
||||
if lookup =~ SQUARE_BRACKETED
|
||||
lookups[i] = Expression.parse(Regexp.last_match(1))
|
||||
if lookup&.start_with?('[') && lookup&.end_with?(']')
|
||||
lookups[i] = Expression.parse(lookup[1..-2])
|
||||
elsif COMMAND_METHODS.include?(lookup)
|
||||
@command_flags |= 1 << i
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def lookup_command?(lookup_index)
|
||||
@command_flags & (1 << lookup_index) != 0
|
||||
end
|
||||
|
||||
def evaluate(context)
|
||||
name = context.evaluate(@name)
|
||||
name = context.evaluate(@name)
|
||||
object = context.find_variable(name)
|
||||
|
||||
@lookups.each_index do |i|
|
||||
key = context.evaluate(@lookups[i])
|
||||
|
||||
# Cast "key" to its liquid value to enable it to act as a primitive value
|
||||
key = Liquid::Utils.to_liquid_value(key)
|
||||
|
||||
# If object is a hash- or array-like object we look for the
|
||||
# presence of the key and if its available we return it
|
||||
if object.respond_to?(:[]) &&
|
||||
@@ -47,13 +90,13 @@ module Liquid
|
||||
(object.respond_to?(:fetch) && key.is_a?(Integer)))
|
||||
|
||||
# if its a proc we will replace the entry with the proc
|
||||
res = context.lookup_and_evaluate(object, key)
|
||||
res = context.lookup_and_evaluate(object, key)
|
||||
object = res.to_liquid
|
||||
|
||||
# Some special cases. If the part wasn't in square brackets and
|
||||
# no key with the same name was found we interpret following calls
|
||||
# as commands and call them on the current object
|
||||
elsif @command_flags & (1 << i) != 0 && object.respond_to?(key)
|
||||
elsif lookup_command?(i) && object.respond_to?(key)
|
||||
object = object.send(key).to_liquid
|
||||
|
||||
# No key was present with the desired value and it wasn't one of the directly supported
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
VERSION = "4.0.3"
|
||||
VERSION = "5.4.0"
|
||||
end
|
||||
|
||||
+5
-4
@@ -17,16 +17,17 @@ Gem::Specification.new do |s|
|
||||
s.license = "MIT"
|
||||
# s.description = "A secure, non-evaling end user template engine with aesthetic markup."
|
||||
|
||||
s.required_ruby_version = ">= 2.4.0"
|
||||
s.required_ruby_version = ">= 2.7.0"
|
||||
s.required_rubygems_version = ">= 1.3.7"
|
||||
|
||||
s.test_files = Dir.glob("{test}/**/*")
|
||||
s.files = Dir.glob("{lib}/**/*") + %w(LICENSE README.md)
|
||||
s.metadata['allowed_push_host'] = 'https://rubygems.org'
|
||||
|
||||
s.files = Dir.glob("{lib}/**/*") + %w(LICENSE README.md)
|
||||
|
||||
s.extra_rdoc_files = ["History.md", "README.md"]
|
||||
|
||||
s.require_path = "lib"
|
||||
|
||||
s.add_development_dependency('rake', '~> 11.3')
|
||||
s.add_development_dependency('rake', '~> 13.0')
|
||||
s.add_development_dependency('minitest')
|
||||
end
|
||||
|
||||
@@ -7,7 +7,7 @@ Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
|
||||
profiler = ThemeRunner.new
|
||||
|
||||
Benchmark.ips do |x|
|
||||
x.time = 10
|
||||
x.time = 10
|
||||
x.warmup = 5
|
||||
|
||||
puts
|
||||
|
||||
@@ -21,8 +21,8 @@ class Profiler
|
||||
end
|
||||
|
||||
def profile(phase, &block)
|
||||
print LOG_LABEL
|
||||
print "#{phase}.. ".ljust(10)
|
||||
print(LOG_LABEL)
|
||||
print("#{phase}.. ".ljust(10))
|
||||
report = MemoryProfiler.report(&block)
|
||||
puts 'Done.'
|
||||
@headings << phase.capitalize
|
||||
|
||||
@@ -15,7 +15,7 @@ profiler.run
|
||||
end
|
||||
end
|
||||
|
||||
if profile_type == :cpu && graph_filename = ENV['GRAPH_FILENAME']
|
||||
if profile_type == :cpu && (graph_filename = ENV['GRAPH_FILENAME'])
|
||||
File.open(graph_filename, 'w') do |f|
|
||||
StackProf::Report.new(results).print_graphviz(nil, f)
|
||||
end
|
||||
|
||||
@@ -8,7 +8,7 @@ class CommentForm < Liquid::Block
|
||||
|
||||
if markup =~ Syntax
|
||||
@variable_name = Regexp.last_match(1)
|
||||
@attributes = {}
|
||||
@attributes = {}
|
||||
else
|
||||
raise SyntaxError, "Syntax Error in 'comment_form' - Valid syntax: comment_form [article]"
|
||||
end
|
||||
|
||||
@@ -3,11 +3,19 @@
|
||||
require 'yaml'
|
||||
|
||||
module Database
|
||||
DATABASE_FILE_PATH = "#{__dir__}/vision.database.yml"
|
||||
|
||||
# Load the standard vision toolkit database and re-arrage it to be simply exportable
|
||||
# to liquid as assigns. All this is based on Shopify
|
||||
def self.tables
|
||||
@tables ||= begin
|
||||
db = YAML.load_file("#{__dir__}/vision.database.yml")
|
||||
db =
|
||||
if YAML.respond_to?(:unsafe_load_file) # Only Psych 4+ can use unsafe_load_file
|
||||
# unsafe_load_file is needed for YAML references
|
||||
YAML.unsafe_load_file(DATABASE_FILE_PATH)
|
||||
else
|
||||
YAML.load_file(DATABASE_FILE_PATH)
|
||||
end
|
||||
|
||||
# From vision source
|
||||
db['products'].each do |product|
|
||||
@@ -32,8 +40,8 @@ module Database
|
||||
db['article'] = db['blog']['articles'].first
|
||||
|
||||
db['cart'] = {
|
||||
'total_price' => db['line_items'].values.inject(0) { |sum, item| sum += item['line_price'] * item['quantity'] },
|
||||
'item_count' => db['line_items'].values.inject(0) { |sum, item| sum += item['quantity'] },
|
||||
'total_price' => db['line_items'].values.inject(0) { |sum, item| sum + item['line_price'] * item['quantity'] },
|
||||
'item_count' => db['line_items'].values.inject(0) { |sum, item| sum + item['quantity'] },
|
||||
'items' => db['line_items'].values,
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ class Paginate < Liquid::Block
|
||||
|
||||
if markup =~ Syntax
|
||||
@collection_name = Regexp.last_match(1)
|
||||
@page_size = if Regexp.last_match(2)
|
||||
@page_size = if Regexp.last_match(2)
|
||||
Regexp.last_match(3).to_i
|
||||
else
|
||||
20
|
||||
|
||||
@@ -31,7 +31,7 @@ module ShopFilter
|
||||
|
||||
def link_to_vendor(vendor)
|
||||
if vendor
|
||||
link_to vendor, url_for_vendor(vendor), vendor
|
||||
link_to(vendor, url_for_vendor(vendor), vendor)
|
||||
else
|
||||
'Unknown Vendor'
|
||||
end
|
||||
@@ -39,7 +39,7 @@ module ShopFilter
|
||||
|
||||
def link_to_type(type)
|
||||
if type
|
||||
link_to type, url_for_type(type), type
|
||||
link_to(type, url_for_type(type), type)
|
||||
else
|
||||
'Unknown Vendor'
|
||||
end
|
||||
@@ -60,7 +60,7 @@ module ShopFilter
|
||||
|
||||
case style
|
||||
when 'original'
|
||||
return '/files/shops/random_number/' + url
|
||||
'/files/shops/random_number/' + url
|
||||
when 'grande', 'large', 'medium', 'compact', 'small', 'thumb', 'icon'
|
||||
"/files/shops/random_number/products/#{Regexp.last_match(1)}_#{style}.#{Regexp.last_match(2)}"
|
||||
else
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# This profiler run simulates Shopify.
|
||||
# We are looking in the tests directory for liquid files and render them within the designated layout file.
|
||||
# We will also export a substantial database to liquid which the templates can render values of.
|
||||
# All this is to make the benchmark as non syntetic as possible. All templates and tests are lifted from
|
||||
# All this is to make the benchmark as non synthetic as possible. All templates and tests are lifted from
|
||||
# direct real-world usage and the profiler measures code that looks very similar to the way it looks in
|
||||
# Shopify which is likely the biggest user of liquid in the world which something to the tune of several
|
||||
# million Template#render calls a day.
|
||||
@@ -58,9 +58,9 @@ class ThemeRunner
|
||||
# `render` is called to benchmark just the render portion of liquid
|
||||
def render
|
||||
@compiled_tests.each do |test|
|
||||
tmpl = test[:tmpl]
|
||||
tmpl = test[:tmpl]
|
||||
assigns = test[:assigns]
|
||||
layout = test[:layout]
|
||||
layout = test[:layout]
|
||||
|
||||
if layout
|
||||
assigns['content_for_layout'] = tmpl.render!(assigns)
|
||||
@@ -73,10 +73,14 @@ class ThemeRunner
|
||||
|
||||
private
|
||||
|
||||
def render_layout(template, layout, assigns)
|
||||
assigns['content_for_layout'] = template.render!(assigns)
|
||||
layout&.render!(assigns)
|
||||
end
|
||||
|
||||
def compile_and_render(template, layout, assigns, page_template, template_file)
|
||||
compiled_test = compile_test(template, layout, assigns, page_template, template_file)
|
||||
assigns['content_for_layout'] = compiled_test[:tmpl].render!(assigns)
|
||||
compiled_test[:layout].render!(assigns) if layout
|
||||
render_layout(compiled_test[:tmpl], compiled_test[:layout], compiled_test[:assigns])
|
||||
end
|
||||
|
||||
def compile_all_tests
|
||||
@@ -88,7 +92,7 @@ class ThemeRunner
|
||||
end
|
||||
|
||||
def compile_test(template, layout, assigns, page_template, template_file)
|
||||
tmpl = init_template(page_template, template_file)
|
||||
tmpl = init_template(page_template, template_file)
|
||||
parsed_template = tmpl.parse(template).dup
|
||||
|
||||
if layout
|
||||
@@ -113,9 +117,9 @@ class ThemeRunner
|
||||
|
||||
# set up a new Liquid::Template object for use in `compile_and_render` and `compile_test`
|
||||
def init_template(page_template, template_file)
|
||||
tmpl = Liquid::Template.new
|
||||
tmpl.assigns['page_title'] = 'Page title'
|
||||
tmpl.assigns['template'] = page_template
|
||||
tmpl = Liquid::Template.new
|
||||
tmpl.assigns['page_title'] = 'Page title'
|
||||
tmpl.assigns['template'] = page_template
|
||||
tmpl.registers[:file_system] = ThemeRunner::FileSystem.new(File.dirname(template_file))
|
||||
tmpl
|
||||
end
|
||||
|
||||
@@ -6,45 +6,103 @@ class AssignTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def test_assign_with_hyphen_in_variable_name
|
||||
template_source = <<-END_TEMPLATE
|
||||
{% assign this-thing = 'Print this-thing' %}
|
||||
{{ this-thing }}
|
||||
template_source = <<~END_TEMPLATE
|
||||
{% assign this-thing = 'Print this-thing' -%}
|
||||
{{ this-thing -}}
|
||||
END_TEMPLATE
|
||||
template = Template.parse(template_source)
|
||||
rendered = template.render!
|
||||
assert_equal "Print this-thing", rendered.strip
|
||||
assert_template_result("Print this-thing", template_source)
|
||||
end
|
||||
|
||||
def test_assigned_variable
|
||||
assert_template_result('.foo.',
|
||||
'{% assign foo = values %}.{{ foo[0] }}.',
|
||||
'values' => %w(foo bar baz))
|
||||
{ 'values' => %w(foo bar baz) })
|
||||
|
||||
assert_template_result('.bar.',
|
||||
'{% assign foo = values %}.{{ foo[1] }}.',
|
||||
'values' => %w(foo bar baz))
|
||||
{ 'values' => %w(foo bar baz) })
|
||||
end
|
||||
|
||||
def test_assign_with_filter
|
||||
assert_template_result('.bar.',
|
||||
'{% assign foo = values | split: "," %}.{{ foo[1] }}.',
|
||||
'values' => "foo,bar,baz")
|
||||
{ 'values' => "foo,bar,baz" })
|
||||
end
|
||||
|
||||
def test_assign_syntax_error
|
||||
assert_match_syntax_error(/assign/,
|
||||
'{% assign foo not values %}.',
|
||||
'values' => "foo,bar,baz")
|
||||
assert_match_syntax_error(/assign/, '{% assign foo not values %}.')
|
||||
end
|
||||
|
||||
def test_assign_uses_error_mode
|
||||
with_error_mode(:strict) do
|
||||
assert_raises(SyntaxError) do
|
||||
Template.parse("{% assign foo = ('X' | downcase) %}")
|
||||
end
|
||||
assert_match_syntax_error("Expected dotdot but found pipe in ",
|
||||
"{% assign foo = ('X' | downcase) %}", error_mode: :strict)
|
||||
assert_template_result("", "{% assign foo = ('X' | downcase) %}", error_mode: :lax)
|
||||
end
|
||||
|
||||
def test_expression_with_whitespace_in_square_brackets
|
||||
source = "{% assign r = a[ 'b' ] %}{{ r }}"
|
||||
assert_template_result('result', source, { 'a' => { 'b' => 'result' } })
|
||||
end
|
||||
|
||||
def test_assign_score_exceeding_resource_limit
|
||||
t = Template.parse("{% assign foo = 42 %}{% assign bar = 23 %}")
|
||||
t.resource_limits.assign_score_limit = 1
|
||||
assert_equal("Liquid error: Memory limits exceeded", t.render)
|
||||
assert(t.resource_limits.reached?)
|
||||
|
||||
t.resource_limits.assign_score_limit = 2
|
||||
assert_equal("", t.render!)
|
||||
refute_nil(t.resource_limits.assign_score)
|
||||
end
|
||||
|
||||
def test_assign_score_exceeding_limit_from_composite_object
|
||||
t = Template.parse("{% assign foo = 'aaaa' | reverse %}")
|
||||
|
||||
t.resource_limits.assign_score_limit = 3
|
||||
assert_equal("Liquid error: Memory limits exceeded", t.render)
|
||||
assert(t.resource_limits.reached?)
|
||||
|
||||
t.resource_limits.assign_score_limit = 5
|
||||
assert_equal("", t.render!)
|
||||
end
|
||||
|
||||
def test_assign_score_of_int
|
||||
assert_equal(1, assign_score_of(123))
|
||||
end
|
||||
|
||||
def test_assign_score_of_string_counts_bytes
|
||||
assert_equal(3, assign_score_of('123'))
|
||||
assert_equal(5, assign_score_of('12345'))
|
||||
assert_equal(9, assign_score_of('すごい'))
|
||||
end
|
||||
|
||||
def test_assign_score_of_array
|
||||
assert_equal(1, assign_score_of([]))
|
||||
assert_equal(2, assign_score_of([123]))
|
||||
assert_equal(6, assign_score_of([123, 'abcd']))
|
||||
end
|
||||
|
||||
def test_assign_score_of_hash
|
||||
assert_equal(1, assign_score_of({}))
|
||||
assert_equal(5, assign_score_of('int' => 123))
|
||||
assert_equal(12, assign_score_of('int' => 123, 'str' => 'abcd'))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
class ObjectWrapperDrop < Liquid::Drop
|
||||
def initialize(obj)
|
||||
@obj = obj
|
||||
end
|
||||
with_error_mode(:lax) do
|
||||
assert Template.parse("{% assign foo = ('X' | downcase) %}")
|
||||
|
||||
def value
|
||||
@obj
|
||||
end
|
||||
end
|
||||
end # AssignTest
|
||||
|
||||
def assign_score_of(obj)
|
||||
context = Liquid::Context.new('drop' => ObjectWrapperDrop.new(obj))
|
||||
Liquid::Template.parse('{% assign obj = drop.value %}').render!(context)
|
||||
context.resource_limits.assign_score
|
||||
end
|
||||
end
|
||||
|
||||
@@ -9,12 +9,6 @@ class FoobarTag < Liquid::Tag
|
||||
end
|
||||
end
|
||||
|
||||
class BlankTestFileSystem
|
||||
def read_template_file(template_path)
|
||||
template_path
|
||||
end
|
||||
end
|
||||
|
||||
class BlankTest < Minitest::Test
|
||||
include Liquid
|
||||
N = 10
|
||||
@@ -33,7 +27,7 @@ class BlankTest < Minitest::Test
|
||||
|
||||
def test_new_tags_are_not_blank_by_default
|
||||
with_custom_tag('foobar', FoobarTag) do
|
||||
assert_template_result(" " * N, wrap_in_for("{% foobar %}"))
|
||||
assert_equal(" " * N, Liquid::Template.parse(wrap_in_for("{% foobar %}")).render!)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -95,10 +89,12 @@ class BlankTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_include_is_blank
|
||||
Liquid::Template.file_system = BlankTestFileSystem.new
|
||||
assert_template_result "foobar" * (N + 1), wrap("{% include 'foobar' %}")
|
||||
assert_template_result " foobar " * (N + 1), wrap("{% include ' foobar ' %}")
|
||||
assert_template_result " " * (N + 1), wrap(" {% include ' ' %} ")
|
||||
assert_template_result("foobar" * (N + 1), wrap("{% include 'foobar' %}"),
|
||||
partials: { 'foobar' => 'foobar' })
|
||||
assert_template_result(" foobar " * (N + 1), wrap("{% include ' foobar ' %}"),
|
||||
partials: { ' foobar ' => ' foobar ' })
|
||||
assert_template_result(" " * (N + 1), wrap(" {% include ' ' %} "),
|
||||
partials: { ' ' => ' ' })
|
||||
end
|
||||
|
||||
def test_case_is_blank
|
||||
|
||||
@@ -6,9 +6,51 @@ class BlockTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def test_unexpected_end_tag
|
||||
exc = assert_raises(SyntaxError) do
|
||||
Template.parse("{% if true %}{% endunless %}")
|
||||
source = '{% if true %}{% endunless %}'
|
||||
assert_match_syntax_error("Liquid syntax error (line 1): 'endunless' is not a valid delimiter for if tags. use endif", source)
|
||||
end
|
||||
|
||||
def test_with_custom_tag
|
||||
with_custom_tag('testtag', Block) do
|
||||
assert(Liquid::Template.parse("{% testtag %} {% endtesttag %}"))
|
||||
end
|
||||
end
|
||||
|
||||
def test_custom_block_tags_have_a_default_render_to_output_buffer_method_for_backwards_compatibility
|
||||
klass1 = Class.new(Block) do
|
||||
def render(*)
|
||||
'hello'
|
||||
end
|
||||
end
|
||||
|
||||
with_custom_tag('blabla', klass1) do
|
||||
template = Liquid::Template.parse("{% blabla %} bla {% endblabla %}")
|
||||
|
||||
assert_equal('hello', template.render)
|
||||
|
||||
buf = +''
|
||||
output = template.render({}, output: buf)
|
||||
assert_equal('hello', output)
|
||||
assert_equal('hello', buf)
|
||||
assert_equal(buf.object_id, output.object_id)
|
||||
end
|
||||
|
||||
klass2 = Class.new(klass1) do
|
||||
def render(*)
|
||||
'foo' + super + 'bar'
|
||||
end
|
||||
end
|
||||
|
||||
with_custom_tag('blabla', klass2) do
|
||||
template = Liquid::Template.parse("{% blabla %} foo {% endblabla %}")
|
||||
|
||||
assert_equal('foohellobar', template.render)
|
||||
|
||||
buf = +''
|
||||
output = template.render({}, output: buf)
|
||||
assert_equal('foohellobar', output)
|
||||
assert_equal('foohellobar', buf)
|
||||
assert_equal(buf.object_id, output.object_id)
|
||||
end
|
||||
assert_equal exc.message, "Liquid syntax error: 'endunless' is not a valid delimiter for if tags. use endif"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,43 +10,43 @@ class CaptureTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_capture_with_hyphen_in_variable_name
|
||||
template_source = <<-END_TEMPLATE
|
||||
{% capture this-thing %}Print this-thing{% endcapture %}
|
||||
{{ this-thing }}
|
||||
template_source = <<~END_TEMPLATE
|
||||
{% capture this-thing %}Print this-thing{% endcapture -%}
|
||||
{{ this-thing -}}
|
||||
END_TEMPLATE
|
||||
template = Template.parse(template_source)
|
||||
rendered = template.render!
|
||||
assert_equal "Print this-thing", rendered.strip
|
||||
assert_template_result("Print this-thing", template_source)
|
||||
end
|
||||
|
||||
def test_capture_to_variable_from_outer_scope_if_existing
|
||||
template_source = <<-END_TEMPLATE
|
||||
{% assign var = '' %}
|
||||
{% if true %}
|
||||
{% capture var %}first-block-string{% endcapture %}
|
||||
{% endif %}
|
||||
{% if true %}
|
||||
{% capture var %}test-string{% endcapture %}
|
||||
{% endif %}
|
||||
{{var}}
|
||||
template_source = <<~END_TEMPLATE
|
||||
{% assign var = '' -%}
|
||||
{% if true -%}
|
||||
{% capture var %}first-block-string{% endcapture -%}
|
||||
{% endif -%}
|
||||
{% if true -%}
|
||||
{% capture var %}test-string{% endcapture -%}
|
||||
{% endif -%}
|
||||
{{var-}}
|
||||
END_TEMPLATE
|
||||
template = Template.parse(template_source)
|
||||
rendered = template.render!
|
||||
assert_equal "test-string", rendered.gsub(/\s/, '')
|
||||
assert_template_result("test-string", template_source)
|
||||
end
|
||||
|
||||
def test_assigning_from_capture
|
||||
template_source = <<-END_TEMPLATE
|
||||
{% assign first = '' %}
|
||||
{% assign second = '' %}
|
||||
{% for number in (1..3) %}
|
||||
{% capture first %}{{number}}{% endcapture %}
|
||||
{% assign second = first %}
|
||||
{% endfor %}
|
||||
{{ first }}-{{ second }}
|
||||
template_source = <<~END_TEMPLATE
|
||||
{% assign first = '' -%}
|
||||
{% assign second = '' -%}
|
||||
{% for number in (1..3) -%}
|
||||
{% capture first %}{{number}}{% endcapture -%}
|
||||
{% assign second = first -%}
|
||||
{% endfor -%}
|
||||
{{ first }}-{{ second -}}
|
||||
END_TEMPLATE
|
||||
template = Template.parse(template_source)
|
||||
rendered = template.render!
|
||||
assert_equal "3-3", rendered.gsub(/\s/, '')
|
||||
assert_template_result("3-3", template_source)
|
||||
end
|
||||
end # CaptureTest
|
||||
|
||||
def test_increment_assign_score_by_bytes_not_characters
|
||||
t = Template.parse("{% capture foo %}すごい{% endcapture %}")
|
||||
t.render!
|
||||
assert_equal(9, t.resource_limits.assign_score)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,9 +2,584 @@
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class HundredCentes
|
||||
def to_liquid
|
||||
100
|
||||
end
|
||||
end
|
||||
|
||||
class CentsDrop < Liquid::Drop
|
||||
def amount
|
||||
HundredCentes.new
|
||||
end
|
||||
|
||||
def non_zero?
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
class ContextSensitiveDrop < Liquid::Drop
|
||||
def test
|
||||
@context['test']
|
||||
end
|
||||
end
|
||||
|
||||
class Category
|
||||
attr_accessor :name
|
||||
|
||||
def initialize(name)
|
||||
@name = name
|
||||
end
|
||||
|
||||
def to_liquid
|
||||
CategoryDrop.new(self)
|
||||
end
|
||||
end
|
||||
|
||||
class CategoryDrop < Liquid::Drop
|
||||
attr_accessor :category, :context
|
||||
|
||||
def initialize(category)
|
||||
@category = category
|
||||
end
|
||||
end
|
||||
|
||||
class CounterDrop < Liquid::Drop
|
||||
def count
|
||||
@count ||= 0
|
||||
@count += 1
|
||||
end
|
||||
end
|
||||
|
||||
class ArrayLike
|
||||
def fetch(index)
|
||||
end
|
||||
|
||||
def [](index)
|
||||
@counts ||= []
|
||||
@counts[index] ||= 0
|
||||
@counts[index] += 1
|
||||
end
|
||||
|
||||
def to_liquid
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
class ContextTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def setup
|
||||
@context = Liquid::Context.new
|
||||
end
|
||||
|
||||
def test_variables
|
||||
@context['string'] = 'string'
|
||||
assert_equal('string', @context['string'])
|
||||
|
||||
@context['num'] = 5
|
||||
assert_equal(5, @context['num'])
|
||||
|
||||
@context['time'] = Time.parse('2006-06-06 12:00:00')
|
||||
assert_equal(Time.parse('2006-06-06 12:00:00'), @context['time'])
|
||||
|
||||
@context['date'] = Date.today
|
||||
assert_equal(Date.today, @context['date'])
|
||||
|
||||
now = Time.now
|
||||
@context['datetime'] = now
|
||||
assert_equal(now, @context['datetime'])
|
||||
|
||||
@context['bool'] = true
|
||||
assert_equal(true, @context['bool'])
|
||||
|
||||
@context['bool'] = false
|
||||
assert_equal(false, @context['bool'])
|
||||
|
||||
@context['nil'] = nil
|
||||
assert_nil(@context['nil'])
|
||||
assert_nil(@context['nil'])
|
||||
end
|
||||
|
||||
def test_variables_not_existing
|
||||
assert_template_result("true", "{% if does_not_exist == nil %}true{% endif %}")
|
||||
end
|
||||
|
||||
def test_scoping
|
||||
@context.push
|
||||
@context.pop
|
||||
|
||||
assert_raises(Liquid::ContextError) do
|
||||
@context.pop
|
||||
end
|
||||
|
||||
assert_raises(Liquid::ContextError) do
|
||||
@context.push
|
||||
@context.pop
|
||||
@context.pop
|
||||
end
|
||||
end
|
||||
|
||||
def test_length_query
|
||||
assert_template_result("true", "{% if numbers.size == 4 %}true{% endif %}",
|
||||
{ "numbers" => [1, 2, 3, 4] })
|
||||
|
||||
assert_template_result("true", "{% if numbers.size == 4 %}true{% endif %}",
|
||||
{ "numbers" => { 1 => 1, 2 => 2, 3 => 3, 4 => 4 } })
|
||||
|
||||
assert_template_result("true", "{% if numbers.size == 1000 %}true{% endif %}",
|
||||
{ "numbers" => { 1 => 1, 2 => 2, 3 => 3, 4 => 4, 'size' => 1000 } })
|
||||
end
|
||||
|
||||
def test_hyphenated_variable
|
||||
assert_template_result("godz", "{{ oh-my }}", { "oh-my" => 'godz' })
|
||||
end
|
||||
|
||||
def test_add_filter
|
||||
filter = Module.new do
|
||||
def hi(output)
|
||||
output + ' hi!'
|
||||
end
|
||||
end
|
||||
|
||||
context = Context.new
|
||||
context.add_filters(filter)
|
||||
assert_equal('hi? hi!', context.invoke(:hi, 'hi?'))
|
||||
|
||||
context = Context.new
|
||||
assert_equal('hi?', context.invoke(:hi, 'hi?'))
|
||||
|
||||
context.add_filters(filter)
|
||||
assert_equal('hi? hi!', context.invoke(:hi, 'hi?'))
|
||||
end
|
||||
|
||||
def test_only_intended_filters_make_it_there
|
||||
filter = Module.new do
|
||||
def hi(output)
|
||||
output + ' hi!'
|
||||
end
|
||||
end
|
||||
|
||||
context = Context.new
|
||||
assert_equal("Wookie", context.invoke("hi", "Wookie"))
|
||||
|
||||
context.add_filters(filter)
|
||||
assert_equal("Wookie hi!", context.invoke("hi", "Wookie"))
|
||||
end
|
||||
|
||||
def test_add_item_in_outer_scope
|
||||
@context['test'] = 'test'
|
||||
@context.push
|
||||
assert_equal('test', @context['test'])
|
||||
@context.pop
|
||||
assert_equal('test', @context['test'])
|
||||
end
|
||||
|
||||
def test_add_item_in_inner_scope
|
||||
@context.push
|
||||
@context['test'] = 'test'
|
||||
assert_equal('test', @context['test'])
|
||||
@context.pop
|
||||
assert_nil(@context['test'])
|
||||
end
|
||||
|
||||
def test_hierachical_data
|
||||
assigns = { 'hash' => { "name" => 'tobi' } }
|
||||
assert_template_result("tobi", "{{ hash.name }}", assigns)
|
||||
assert_template_result("tobi", '{{ hash["name"] }}', assigns)
|
||||
end
|
||||
|
||||
def test_keywords
|
||||
assert_template_result("pass", "{% if true == expect %}pass{% endif %}", { "expect" => true })
|
||||
assert_template_result("pass", "{% if false == expect %}pass{% endif %}", { "expect" => false })
|
||||
end
|
||||
|
||||
def test_digits
|
||||
assert_template_result("pass", "{% if 100 == expect %}pass{% endif %}", { "expect" => 100 })
|
||||
assert_template_result("pass", "{% if 100.00 == expect %}pass{% endif %}", { "expect" => 100.00 })
|
||||
end
|
||||
|
||||
def test_strings
|
||||
assert_template_result("hello!", '{{ "hello!" }}')
|
||||
assert_template_result("hello!", "{{ 'hello!' }}")
|
||||
end
|
||||
|
||||
def test_merge
|
||||
@context.merge("test" => "test")
|
||||
assert_equal('test', @context['test'])
|
||||
@context.merge("test" => "newvalue", "foo" => "bar")
|
||||
assert_equal('newvalue', @context['test'])
|
||||
assert_equal('bar', @context['foo'])
|
||||
end
|
||||
|
||||
def test_array_notation
|
||||
assigns = { "test" => ["a", "b"] }
|
||||
assert_template_result("a", "{{ test[0] }}", assigns)
|
||||
assert_template_result("b", "{{ test[1] }}", assigns)
|
||||
assert_template_result("pass", "{% if test[2] == nil %}pass{% endif %}", assigns)
|
||||
end
|
||||
|
||||
def test_recoursive_array_notation
|
||||
assigns = { "test" => { 'test' => [1, 2, 3, 4, 5] } }
|
||||
assert_template_result("1", "{{ test.test[0] }}", assigns)
|
||||
|
||||
assigns = { "test" => [{ 'test' => 'worked' }] }
|
||||
assert_template_result("worked", "{{ test[0].test }}", assigns)
|
||||
end
|
||||
|
||||
def test_hash_to_array_transition
|
||||
assigns = { 'colors' => {
|
||||
'Blue' => ['003366', '336699', '6699CC', '99CCFF'],
|
||||
'Green' => ['003300', '336633', '669966', '99CC99'],
|
||||
'Yellow' => ['CC9900', 'FFCC00', 'FFFF99', 'FFFFCC'],
|
||||
'Red' => ['660000', '993333', 'CC6666', 'FF9999'],
|
||||
} }
|
||||
|
||||
assert_template_result("003366", "{{ colors.Blue[0] }}", assigns)
|
||||
assert_template_result("FF9999", "{{ colors.Red[3] }}", assigns)
|
||||
end
|
||||
|
||||
def test_try_first
|
||||
assigns = { 'test' => [1, 2, 3, 4, 5] }
|
||||
assert_template_result("1", "{{ test.first }}", assigns)
|
||||
assert_template_result("pass", "{% if test.last == 5 %}pass{% endif %}", assigns)
|
||||
|
||||
assigns = { "test" => { "test" => [1, 2, 3, 4, 5] } }
|
||||
assert_template_result("1", "{{ test.test.first }}", assigns)
|
||||
assert_template_result("5", "{{ test.test.last }}", assigns)
|
||||
|
||||
assigns = { "test" => [1] }
|
||||
assert_template_result("1", "{{ test.first }}", assigns)
|
||||
assert_template_result("1", "{{ test.last }}", assigns)
|
||||
end
|
||||
|
||||
def test_access_hashes_with_hash_notation
|
||||
assigns = { 'products' => { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] } }
|
||||
assert_template_result("5", '{{ products["count"] }}', assigns)
|
||||
assert_template_result("deepsnow", '{{ products["tags"][0] }}', assigns)
|
||||
assert_template_result("deepsnow", '{{ products["tags"].first }}', assigns)
|
||||
|
||||
assigns = { 'product' => { 'variants' => [{ 'title' => 'draft151cm' }, { 'title' => 'element151cm' }] } }
|
||||
assert_template_result("draft151cm", '{{ product["variants"][0]["title"] }}', assigns)
|
||||
assert_template_result("element151cm", '{{ product["variants"][1]["title"] }}', assigns)
|
||||
assert_template_result("draft151cm", '{{ product["variants"][0]["title"] }}', assigns)
|
||||
assert_template_result("element151cm", '{{ product["variants"].last["title"] }}', assigns)
|
||||
end
|
||||
|
||||
def test_access_variable_with_hash_notation
|
||||
assert_template_result('baz', '{{ ["foo"] }}', { "foo" => "baz" })
|
||||
assert_template_result('baz', '{{ [bar] }}', { 'foo' => 'baz', 'bar' => 'foo' })
|
||||
end
|
||||
|
||||
def test_access_hashes_with_hash_access_variables
|
||||
assigns = {
|
||||
'var' => 'tags',
|
||||
'nested' => { 'var' => 'tags' },
|
||||
'products' => { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] },
|
||||
}
|
||||
|
||||
assert_template_result('deepsnow', '{{ products[var].first }}', assigns)
|
||||
assert_template_result('freestyle', '{{ products[nested.var].last }}', assigns)
|
||||
end
|
||||
|
||||
def test_hash_notation_only_for_hash_access
|
||||
assigns = { "array" => [1, 2, 3, 4, 5] }
|
||||
assert_template_result("1", "{{ array.first }}", assigns)
|
||||
assert_template_result("pass", '{% if array["first"] == nil %}pass{% endif %}', assigns)
|
||||
|
||||
assert_template_result("Hello", '{{ hash["first"] }}', { "hash" => { "first" => "Hello" } })
|
||||
end
|
||||
|
||||
def test_first_can_appear_in_middle_of_callchain
|
||||
assigns = { "product" => { 'variants' => [{ 'title' => 'draft151cm' }, { 'title' => 'element151cm' }] } }
|
||||
|
||||
assert_template_result('draft151cm', '{{ product.variants[0].title }}', assigns)
|
||||
assert_template_result('element151cm', '{{ product.variants[1].title }}', assigns)
|
||||
assert_template_result('draft151cm', '{{ product.variants.first.title }}', assigns)
|
||||
assert_template_result('element151cm', '{{ product.variants.last.title }}', assigns)
|
||||
end
|
||||
|
||||
def test_cents
|
||||
@context.merge("cents" => HundredCentes.new)
|
||||
assert_equal(100, @context['cents'])
|
||||
end
|
||||
|
||||
def test_nested_cents
|
||||
@context.merge("cents" => { 'amount' => HundredCentes.new })
|
||||
assert_equal(100, @context['cents.amount'])
|
||||
|
||||
@context.merge("cents" => { 'cents' => { 'amount' => HundredCentes.new } })
|
||||
assert_equal(100, @context['cents.cents.amount'])
|
||||
end
|
||||
|
||||
def test_cents_through_drop
|
||||
@context.merge("cents" => CentsDrop.new)
|
||||
assert_equal(100, @context['cents.amount'])
|
||||
end
|
||||
|
||||
def test_nested_cents_through_drop
|
||||
@context.merge("vars" => { "cents" => CentsDrop.new })
|
||||
assert_equal(100, @context['vars.cents.amount'])
|
||||
end
|
||||
|
||||
def test_drop_methods_with_question_marks
|
||||
@context.merge("cents" => CentsDrop.new)
|
||||
assert(@context['cents.non_zero?'])
|
||||
end
|
||||
|
||||
def test_context_from_within_drop
|
||||
@context.merge("test" => '123', "vars" => ContextSensitiveDrop.new)
|
||||
assert_equal('123', @context['vars.test'])
|
||||
end
|
||||
|
||||
def test_nested_context_from_within_drop
|
||||
@context.merge("test" => '123', "vars" => { "local" => ContextSensitiveDrop.new })
|
||||
assert_equal('123', @context['vars.local.test'])
|
||||
end
|
||||
|
||||
def test_ranges
|
||||
assert_template_result("1..5", '{{ (1..5) }}')
|
||||
assert_template_result("pass", '{% if (1..5) == expect %}pass{% endif %}', { "expect" => (1..5) })
|
||||
|
||||
assigns = { "test" => '5' }
|
||||
assert_template_result("1..5", "{{ (1..test) }}", assigns)
|
||||
assert_template_result("5..5", "{{ (test..test) }}", assigns)
|
||||
end
|
||||
|
||||
def test_cents_through_drop_nestedly
|
||||
@context.merge("cents" => { "cents" => CentsDrop.new })
|
||||
assert_equal(100, @context['cents.cents.amount'])
|
||||
|
||||
@context.merge("cents" => { "cents" => { "cents" => CentsDrop.new } })
|
||||
assert_equal(100, @context['cents.cents.cents.amount'])
|
||||
end
|
||||
|
||||
def test_drop_with_variable_called_only_once
|
||||
@context['counter'] = CounterDrop.new
|
||||
|
||||
assert_equal(1, @context['counter.count'])
|
||||
assert_equal(2, @context['counter.count'])
|
||||
assert_equal(3, @context['counter.count'])
|
||||
end
|
||||
|
||||
def test_drop_with_key_called_only_once
|
||||
@context['counter'] = CounterDrop.new
|
||||
|
||||
assert_equal(1, @context['counter["count"]'])
|
||||
assert_equal(2, @context['counter["count"]'])
|
||||
assert_equal(3, @context['counter["count"]'])
|
||||
end
|
||||
|
||||
def test_proc_as_variable
|
||||
@context['dynamic'] = proc { 'Hello' }
|
||||
|
||||
assert_equal('Hello', @context['dynamic'])
|
||||
end
|
||||
|
||||
def test_lambda_as_variable
|
||||
@context['dynamic'] = proc { 'Hello' }
|
||||
|
||||
assert_equal('Hello', @context['dynamic'])
|
||||
end
|
||||
|
||||
def test_nested_lambda_as_variable
|
||||
@context['dynamic'] = { "lambda" => proc { 'Hello' } }
|
||||
|
||||
assert_equal('Hello', @context['dynamic.lambda'])
|
||||
end
|
||||
|
||||
def test_array_containing_lambda_as_variable
|
||||
@context['dynamic'] = [1, 2, proc { 'Hello' }, 4, 5]
|
||||
|
||||
assert_equal('Hello', @context['dynamic[2]'])
|
||||
end
|
||||
|
||||
def test_lambda_is_called_once
|
||||
@global = 0
|
||||
|
||||
@context['callcount'] = proc {
|
||||
@global += 1
|
||||
@global.to_s
|
||||
}
|
||||
|
||||
assert_equal('1', @context['callcount'])
|
||||
assert_equal('1', @context['callcount'])
|
||||
assert_equal('1', @context['callcount'])
|
||||
end
|
||||
|
||||
def test_nested_lambda_is_called_once
|
||||
@global = 0
|
||||
|
||||
@context['callcount'] = { "lambda" => proc {
|
||||
@global += 1
|
||||
@global.to_s
|
||||
} }
|
||||
|
||||
assert_equal('1', @context['callcount.lambda'])
|
||||
assert_equal('1', @context['callcount.lambda'])
|
||||
assert_equal('1', @context['callcount.lambda'])
|
||||
end
|
||||
|
||||
def test_lambda_in_array_is_called_once
|
||||
@global = 0
|
||||
|
||||
@context['callcount'] = [1, 2, proc {
|
||||
@global += 1
|
||||
@global.to_s
|
||||
}, 4, 5]
|
||||
|
||||
assert_equal('1', @context['callcount[2]'])
|
||||
assert_equal('1', @context['callcount[2]'])
|
||||
assert_equal('1', @context['callcount[2]'])
|
||||
end
|
||||
|
||||
def test_access_to_context_from_proc
|
||||
@context.registers[:magic] = 345392
|
||||
|
||||
@context['magic'] = proc { @context.registers[:magic] }
|
||||
|
||||
assert_equal(345392, @context['magic'])
|
||||
end
|
||||
|
||||
def test_to_liquid_and_context_at_first_level
|
||||
@context['category'] = Category.new("foobar")
|
||||
assert_kind_of(CategoryDrop, @context['category'])
|
||||
assert_equal(@context, @context['category'].context)
|
||||
end
|
||||
|
||||
def test_interrupt_avoids_object_allocations
|
||||
@context.interrupt? # ruby 3.0.0 allocates on the first call
|
||||
assert_no_object_allocations do
|
||||
@context.interrupt?
|
||||
end
|
||||
end
|
||||
|
||||
def test_context_initialization_with_a_proc_in_environment
|
||||
contx = Context.new([test: ->(c) { c['poutine'] }], test: :foo)
|
||||
|
||||
assert(contx)
|
||||
assert_nil(contx['poutine'])
|
||||
end
|
||||
|
||||
def test_apply_global_filter
|
||||
global_filter_proc = ->(output) { "#{output} filtered" }
|
||||
|
||||
context = Context.new
|
||||
context.global_filter = global_filter_proc
|
||||
|
||||
assert_equal('hi filtered', context.apply_global_filter('hi'))
|
||||
end
|
||||
|
||||
def test_static_environments_are_read_with_lower_priority_than_environments
|
||||
context = Context.build(
|
||||
static_environments: { 'shadowed' => 'static', 'unshadowed' => 'static' },
|
||||
environments: { 'shadowed' => 'dynamic' }
|
||||
)
|
||||
|
||||
assert_equal('dynamic', context['shadowed'])
|
||||
assert_equal('static', context['unshadowed'])
|
||||
end
|
||||
|
||||
def test_apply_global_filter_when_no_global_filter_exist
|
||||
context = Context.new
|
||||
assert_equal('hi', context.apply_global_filter('hi'))
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_does_not_inherit_variables
|
||||
super_context = Context.new
|
||||
super_context['my_variable'] = 'some value'
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
|
||||
assert_nil(subcontext['my_variable'])
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_inherits_static_environment
|
||||
super_context = Context.build(static_environments: { 'my_environment_value' => 'my value' })
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
|
||||
assert_equal('my value', subcontext['my_environment_value'])
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_inherits_resource_limits
|
||||
resource_limits = ResourceLimits.new({})
|
||||
super_context = Context.new({}, {}, {}, false, resource_limits)
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
assert_equal(resource_limits, subcontext.resource_limits)
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_inherits_exception_renderer
|
||||
super_context = Context.new
|
||||
super_context.exception_renderer = ->(_e) { 'my exception message' }
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
assert_equal('my exception message', subcontext.handle_error(Liquid::Error.new))
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_does_not_inherit_non_static_registers
|
||||
registers = {
|
||||
my_register: :my_value,
|
||||
}
|
||||
super_context = Context.new({}, {}, Registers.new(registers))
|
||||
super_context.registers[:my_register] = :my_alt_value
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
assert_equal(:my_value, subcontext.registers[:my_register])
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_inherits_static_registers
|
||||
super_context = Context.build(registers: { my_register: :my_value })
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
assert_equal(:my_value, subcontext.registers[:my_register])
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_registers_do_not_pollute_context
|
||||
super_context = Context.build(registers: { my_register: :my_value })
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
subcontext.registers[:my_register] = :my_alt_value
|
||||
assert_equal(:my_value, super_context.registers[:my_register])
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_inherits_filters
|
||||
my_filter = Module.new do
|
||||
def my_filter(*)
|
||||
'my filter result'
|
||||
end
|
||||
end
|
||||
|
||||
super_context = Context.new
|
||||
super_context.add_filters([my_filter])
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
template = Template.parse('{{ 123 | my_filter }}')
|
||||
assert_equal('my filter result', template.render(subcontext))
|
||||
end
|
||||
|
||||
def test_disables_tag_specified
|
||||
context = Context.new
|
||||
context.with_disabled_tags(%w(foo bar)) do
|
||||
assert_equal(true, context.tag_disabled?("foo"))
|
||||
assert_equal(true, context.tag_disabled?("bar"))
|
||||
assert_equal(false, context.tag_disabled?("unknown"))
|
||||
end
|
||||
end
|
||||
|
||||
def test_disables_nested_tags
|
||||
context = Context.new
|
||||
context.with_disabled_tags(["foo"]) do
|
||||
context.with_disabled_tags(["foo"]) do
|
||||
assert_equal(true, context.tag_disabled?("foo"))
|
||||
assert_equal(false, context.tag_disabled?("bar"))
|
||||
end
|
||||
context.with_disabled_tags(["bar"]) do
|
||||
assert_equal(true, context.tag_disabled?("foo"))
|
||||
assert_equal(true, context.tag_disabled?("bar"))
|
||||
context.with_disabled_tags(["foo"]) do
|
||||
assert_equal(true, context.tag_disabled?("foo"))
|
||||
assert_equal(true, context.tag_disabled?("bar"))
|
||||
end
|
||||
end
|
||||
assert_equal(true, context.tag_disabled?("foo"))
|
||||
assert_equal(false, context.tag_disabled?("bar"))
|
||||
end
|
||||
end
|
||||
|
||||
def test_override_global_filter
|
||||
global = Module.new do
|
||||
def notice(output)
|
||||
@@ -19,16 +594,44 @@ class ContextTest < Minitest::Test
|
||||
end
|
||||
|
||||
with_global_filter(global) do
|
||||
assert_equal 'Global test', Template.parse("{{'test' | notice }}").render!
|
||||
assert_equal 'Local test', Template.parse("{{'test' | notice }}").render!({}, filters: [local])
|
||||
assert_equal('Global test', Template.parse("{{'test' | notice }}").render!)
|
||||
assert_equal('Local test', Template.parse("{{'test' | notice }}").render!({}, filters: [local]))
|
||||
end
|
||||
end
|
||||
|
||||
def test_has_key_will_not_add_an_error_for_missing_keys
|
||||
with_error_mode :strict do
|
||||
with_error_mode(:strict) do
|
||||
context = Context.new
|
||||
context.key?('unknown')
|
||||
assert_empty context.errors
|
||||
assert_empty(context.errors)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_context_always_uses_static_registers
|
||||
registers = {
|
||||
my_register: :my_value,
|
||||
}
|
||||
c = Context.new({}, {}, registers)
|
||||
assert_instance_of(Registers, c.registers)
|
||||
assert_equal(:my_value, c.registers[:my_register])
|
||||
|
||||
r = Registers.new(registers)
|
||||
c = Context.new({}, {}, r)
|
||||
assert_instance_of(Registers, c.registers)
|
||||
assert_equal(:my_value, c.registers[:my_register])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def assert_no_object_allocations
|
||||
unless RUBY_ENGINE == 'ruby'
|
||||
skip("stackprof needed to count object allocations")
|
||||
end
|
||||
require 'stackprof'
|
||||
|
||||
profile = StackProf.run(mode: :object) do
|
||||
yield
|
||||
end
|
||||
assert_equal(0, profile[:samples])
|
||||
end
|
||||
end # ContextTest
|
||||
|
||||
@@ -6,16 +6,12 @@ class DocumentTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def test_unexpected_outer_tag
|
||||
exc = assert_raises(SyntaxError) do
|
||||
Template.parse("{% else %}")
|
||||
end
|
||||
assert_equal exc.message, "Liquid syntax error: Unexpected outer 'else' tag"
|
||||
source = "{% else %}"
|
||||
assert_match_syntax_error("Liquid syntax error (line 1): Unexpected outer 'else' tag", source)
|
||||
end
|
||||
|
||||
def test_unknown_tag
|
||||
exc = assert_raises(SyntaxError) do
|
||||
Template.parse("{% foo %}")
|
||||
end
|
||||
assert_equal exc.message, "Liquid syntax error: Unknown tag 'foo'"
|
||||
source = "{% foo %}"
|
||||
assert_match_syntax_error("Liquid syntax error (line 1): Unknown tag 'foo'", source)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -49,10 +49,6 @@ class ProductDrop < Liquid::Drop
|
||||
ContextDrop.new
|
||||
end
|
||||
|
||||
def user_input
|
||||
(+"foo").taint
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def callmenot
|
||||
@@ -111,172 +107,151 @@ class DropsTest < Minitest::Test
|
||||
|
||||
def test_product_drop
|
||||
tpl = Liquid::Template.parse(' ')
|
||||
assert_equal ' ', tpl.render!('product' => ProductDrop.new)
|
||||
end
|
||||
|
||||
def test_rendering_raises_on_tainted_attr
|
||||
with_taint_mode(:error) do
|
||||
tpl = Liquid::Template.parse('{{ product.user_input }}')
|
||||
assert_raises TaintedError do
|
||||
tpl.render!('product' => ProductDrop.new)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_rendering_warns_on_tainted_attr
|
||||
with_taint_mode(:warn) do
|
||||
tpl = Liquid::Template.parse('{{ product.user_input }}')
|
||||
context = Context.new('product' => ProductDrop.new)
|
||||
tpl.render!(context)
|
||||
assert_equal [Liquid::TaintedError], context.warnings.map(&:class)
|
||||
assert_equal "variable 'product.user_input' is tainted and was not escaped", context.warnings.first.to_s(false)
|
||||
end
|
||||
end
|
||||
|
||||
def test_rendering_doesnt_raise_on_escaped_tainted_attr
|
||||
with_taint_mode(:error) do
|
||||
tpl = Liquid::Template.parse('{{ product.user_input | escape }}')
|
||||
tpl.render!('product' => ProductDrop.new)
|
||||
end
|
||||
assert_equal(' ', tpl.render!('product' => ProductDrop.new))
|
||||
end
|
||||
|
||||
def test_drop_does_only_respond_to_whitelisted_methods
|
||||
assert_equal "", Liquid::Template.parse("{{ product.inspect }}").render!('product' => ProductDrop.new)
|
||||
assert_equal "", Liquid::Template.parse("{{ product.pretty_inspect }}").render!('product' => ProductDrop.new)
|
||||
assert_equal "", Liquid::Template.parse("{{ product.whatever }}").render!('product' => ProductDrop.new)
|
||||
assert_equal "", Liquid::Template.parse('{{ product | map: "inspect" }}').render!('product' => ProductDrop.new)
|
||||
assert_equal "", Liquid::Template.parse('{{ product | map: "pretty_inspect" }}').render!('product' => ProductDrop.new)
|
||||
assert_equal "", Liquid::Template.parse('{{ product | map: "whatever" }}').render!('product' => ProductDrop.new)
|
||||
assert_equal("", Liquid::Template.parse("{{ product.inspect }}").render!('product' => ProductDrop.new))
|
||||
assert_equal("", Liquid::Template.parse("{{ product.pretty_inspect }}").render!('product' => ProductDrop.new))
|
||||
assert_equal("", Liquid::Template.parse("{{ product.whatever }}").render!('product' => ProductDrop.new))
|
||||
assert_equal("", Liquid::Template.parse('{{ product | map: "inspect" }}').render!('product' => ProductDrop.new))
|
||||
assert_equal("", Liquid::Template.parse('{{ product | map: "pretty_inspect" }}').render!('product' => ProductDrop.new))
|
||||
assert_equal("", Liquid::Template.parse('{{ product | map: "whatever" }}').render!('product' => ProductDrop.new))
|
||||
end
|
||||
|
||||
def test_drops_respond_to_to_liquid
|
||||
assert_equal "text1", Liquid::Template.parse("{{ product.to_liquid.texts.text }}").render!('product' => ProductDrop.new)
|
||||
assert_equal "text1", Liquid::Template.parse('{{ product | map: "to_liquid" | map: "texts" | map: "text" }}').render!('product' => ProductDrop.new)
|
||||
assert_equal("text1", Liquid::Template.parse("{{ product.to_liquid.texts.text }}").render!('product' => ProductDrop.new))
|
||||
assert_equal("text1", Liquid::Template.parse('{{ product | map: "to_liquid" | map: "texts" | map: "text" }}').render!('product' => ProductDrop.new))
|
||||
end
|
||||
|
||||
def test_text_drop
|
||||
output = Liquid::Template.parse(' {{ product.texts.text }} ').render!('product' => ProductDrop.new)
|
||||
assert_equal ' text1 ', output
|
||||
assert_equal(' text1 ', output)
|
||||
end
|
||||
|
||||
def test_catchall_unknown_method
|
||||
output = Liquid::Template.parse(' {{ product.catchall.unknown }} ').render!('product' => ProductDrop.new)
|
||||
assert_equal ' catchall_method: unknown ', output
|
||||
assert_equal(' catchall_method: unknown ', output)
|
||||
end
|
||||
|
||||
def test_catchall_integer_argument_drop
|
||||
output = Liquid::Template.parse(' {{ product.catchall[8] }} ').render!('product' => ProductDrop.new)
|
||||
assert_equal ' catchall_method: 8 ', output
|
||||
assert_equal(' catchall_method: 8 ', output)
|
||||
end
|
||||
|
||||
def test_text_array_drop
|
||||
output = Liquid::Template.parse('{% for text in product.texts.array %} {{text}} {% endfor %}').render!('product' => ProductDrop.new)
|
||||
assert_equal ' text1 text2 ', output
|
||||
assert_equal(' text1 text2 ', output)
|
||||
end
|
||||
|
||||
def test_context_drop
|
||||
output = Liquid::Template.parse(' {{ context.bar }} ').render!('context' => ContextDrop.new, 'bar' => "carrot")
|
||||
assert_equal ' carrot ', output
|
||||
assert_equal(' carrot ', output)
|
||||
end
|
||||
|
||||
def test_context_drop_array_with_map
|
||||
output = Liquid::Template.parse(' {{ contexts | map: "bar" }} ').render!('contexts' => [ContextDrop.new, ContextDrop.new], 'bar' => "carrot")
|
||||
assert_equal(' carrotcarrot ', output)
|
||||
end
|
||||
|
||||
def test_nested_context_drop
|
||||
output = Liquid::Template.parse(' {{ product.context.foo }} ').render!('product' => ProductDrop.new, 'foo' => "monkey")
|
||||
assert_equal ' monkey ', output
|
||||
assert_equal(' monkey ', output)
|
||||
end
|
||||
|
||||
def test_protected
|
||||
output = Liquid::Template.parse(' {{ product.callmenot }} ').render!('product' => ProductDrop.new)
|
||||
assert_equal ' ', output
|
||||
assert_equal(' ', output)
|
||||
end
|
||||
|
||||
def test_object_methods_not_allowed
|
||||
[:dup, :clone, :singleton_class, :eval, :class_eval, :inspect].each do |method|
|
||||
output = Liquid::Template.parse(" {{ product.#{method} }} ").render!('product' => ProductDrop.new)
|
||||
assert_equal ' ', output
|
||||
assert_equal(' ', output)
|
||||
end
|
||||
end
|
||||
|
||||
def test_scope
|
||||
assert_equal '1', Liquid::Template.parse('{{ context.scopes }}').render!('context' => ContextDrop.new)
|
||||
assert_equal '2', Liquid::Template.parse('{%for i in dummy%}{{ context.scopes }}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1])
|
||||
assert_equal '3', Liquid::Template.parse('{%for i in dummy%}{%for i in dummy%}{{ context.scopes }}{%endfor%}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1])
|
||||
assert_equal('1', Liquid::Template.parse('{{ context.scopes }}').render!('context' => ContextDrop.new))
|
||||
assert_equal('2', Liquid::Template.parse('{%for i in dummy%}{{ context.scopes }}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1]))
|
||||
assert_equal('3', Liquid::Template.parse('{%for i in dummy%}{%for i in dummy%}{{ context.scopes }}{%endfor%}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1]))
|
||||
end
|
||||
|
||||
def test_scope_though_proc
|
||||
assert_equal '1', Liquid::Template.parse('{{ s }}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] })
|
||||
assert_equal '2', Liquid::Template.parse('{%for i in dummy%}{{ s }}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] }, 'dummy' => [1])
|
||||
assert_equal '3', Liquid::Template.parse('{%for i in dummy%}{%for i in dummy%}{{ s }}{%endfor%}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] }, 'dummy' => [1])
|
||||
assert_equal('1', Liquid::Template.parse('{{ s }}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] }))
|
||||
assert_equal('2', Liquid::Template.parse('{%for i in dummy%}{{ s }}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] }, 'dummy' => [1]))
|
||||
assert_equal('3', Liquid::Template.parse('{%for i in dummy%}{%for i in dummy%}{{ s }}{%endfor%}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] }, 'dummy' => [1]))
|
||||
end
|
||||
|
||||
def test_scope_with_assigns
|
||||
assert_equal 'variable', Liquid::Template.parse('{% assign a = "variable"%}{{a}}').render!('context' => ContextDrop.new)
|
||||
assert_equal 'variable', Liquid::Template.parse('{% assign a = "variable"%}{%for i in dummy%}{{a}}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1])
|
||||
assert_equal 'test', Liquid::Template.parse('{% assign header_gif = "test"%}{{header_gif}}').render!('context' => ContextDrop.new)
|
||||
assert_equal 'test', Liquid::Template.parse("{% assign header_gif = 'test'%}{{header_gif}}").render!('context' => ContextDrop.new)
|
||||
assert_equal('variable', Liquid::Template.parse('{% assign a = "variable"%}{{a}}').render!('context' => ContextDrop.new))
|
||||
assert_equal('variable', Liquid::Template.parse('{% assign a = "variable"%}{%for i in dummy%}{{a}}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1]))
|
||||
assert_equal('test', Liquid::Template.parse('{% assign header_gif = "test"%}{{header_gif}}').render!('context' => ContextDrop.new))
|
||||
assert_equal('test', Liquid::Template.parse("{% assign header_gif = 'test'%}{{header_gif}}").render!('context' => ContextDrop.new))
|
||||
end
|
||||
|
||||
def test_scope_from_tags
|
||||
assert_equal '1', Liquid::Template.parse('{% for i in context.scopes_as_array %}{{i}}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1])
|
||||
assert_equal '12', Liquid::Template.parse('{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1])
|
||||
assert_equal '123', Liquid::Template.parse('{%for a in dummy%}{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1])
|
||||
assert_equal('1', Liquid::Template.parse('{% for i in context.scopes_as_array %}{{i}}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1]))
|
||||
assert_equal('12', Liquid::Template.parse('{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1]))
|
||||
assert_equal('123', Liquid::Template.parse('{%for a in dummy%}{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1]))
|
||||
end
|
||||
|
||||
def test_access_context_from_drop
|
||||
assert_equal '123', Liquid::Template.parse('{%for a in dummy%}{{ context.loop_pos }}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1, 2, 3])
|
||||
assert_equal('123', Liquid::Template.parse('{%for a in dummy%}{{ context.loop_pos }}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1, 2, 3]))
|
||||
end
|
||||
|
||||
def test_enumerable_drop
|
||||
assert_equal '123', Liquid::Template.parse('{% for c in collection %}{{c}}{% endfor %}').render!('collection' => EnumerableDrop.new)
|
||||
assert_equal('123', Liquid::Template.parse('{% for c in collection %}{{c}}{% endfor %}').render!('collection' => EnumerableDrop.new))
|
||||
end
|
||||
|
||||
def test_enumerable_drop_size
|
||||
assert_equal '3', Liquid::Template.parse('{{collection.size}}').render!('collection' => EnumerableDrop.new)
|
||||
assert_equal('3', Liquid::Template.parse('{{collection.size}}').render!('collection' => EnumerableDrop.new))
|
||||
end
|
||||
|
||||
def test_enumerable_drop_will_invoke_liquid_method_missing_for_clashing_method_names
|
||||
["select", "each", "map", "cycle"].each do |method|
|
||||
assert_equal method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new)
|
||||
assert_equal method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new)
|
||||
assert_equal method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new)
|
||||
assert_equal method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new)
|
||||
assert_equal(method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new))
|
||||
assert_equal(method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new))
|
||||
assert_equal(method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new))
|
||||
assert_equal(method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new))
|
||||
end
|
||||
end
|
||||
|
||||
def test_some_enumerable_methods_still_get_invoked
|
||||
[:count, :max].each do |method|
|
||||
assert_equal "3", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new)
|
||||
assert_equal "3", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new)
|
||||
assert_equal "3", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new)
|
||||
assert_equal "3", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new)
|
||||
assert_equal("3", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new))
|
||||
assert_equal("3", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new))
|
||||
assert_equal("3", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new))
|
||||
assert_equal("3", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new))
|
||||
end
|
||||
|
||||
assert_equal "yes", Liquid::Template.parse("{% if collection contains 3 %}yes{% endif %}").render!('collection' => RealEnumerableDrop.new)
|
||||
assert_equal("yes", Liquid::Template.parse("{% if collection contains 3 %}yes{% endif %}").render!('collection' => RealEnumerableDrop.new))
|
||||
|
||||
[:min, :first].each do |method|
|
||||
assert_equal "1", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new)
|
||||
assert_equal "1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new)
|
||||
assert_equal "1", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new)
|
||||
assert_equal "1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new)
|
||||
assert_equal("1", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new))
|
||||
assert_equal("1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new))
|
||||
assert_equal("1", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new))
|
||||
assert_equal("1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new))
|
||||
end
|
||||
end
|
||||
|
||||
def test_empty_string_value_access
|
||||
assert_equal '', Liquid::Template.parse('{{ product[value] }}').render!('product' => ProductDrop.new, 'value' => '')
|
||||
assert_equal('', Liquid::Template.parse('{{ product[value] }}').render!('product' => ProductDrop.new, 'value' => ''))
|
||||
end
|
||||
|
||||
def test_nil_value_access
|
||||
assert_equal '', Liquid::Template.parse('{{ product[value] }}').render!('product' => ProductDrop.new, 'value' => nil)
|
||||
assert_equal('', Liquid::Template.parse('{{ product[value] }}').render!('product' => ProductDrop.new, 'value' => nil))
|
||||
end
|
||||
|
||||
def test_default_to_s_on_drops
|
||||
assert_equal 'ProductDrop', Liquid::Template.parse("{{ product }}").render!('product' => ProductDrop.new)
|
||||
assert_equal 'EnumerableDrop', Liquid::Template.parse('{{ collection }}').render!('collection' => EnumerableDrop.new)
|
||||
assert_equal('ProductDrop', Liquid::Template.parse("{{ product }}").render!('product' => ProductDrop.new))
|
||||
assert_equal('EnumerableDrop', Liquid::Template.parse('{{ collection }}').render!('collection' => EnumerableDrop.new))
|
||||
end
|
||||
|
||||
def test_invokable_methods
|
||||
assert_equal %w(to_liquid catchall user_input context texts).to_set, ProductDrop.invokable_methods
|
||||
assert_equal %w(to_liquid scopes_as_array loop_pos scopes).to_set, ContextDrop.invokable_methods
|
||||
assert_equal %w(to_liquid size max min first count).to_set, EnumerableDrop.invokable_methods
|
||||
assert_equal %w(to_liquid max min sort count first).to_set, RealEnumerableDrop.invokable_methods
|
||||
assert_equal(%w(to_liquid catchall context texts).to_set, ProductDrop.invokable_methods)
|
||||
assert_equal(%w(to_liquid scopes_as_array loop_pos scopes).to_set, ContextDrop.invokable_methods)
|
||||
assert_equal(%w(to_liquid size max min first count).to_set, EnumerableDrop.invokable_methods)
|
||||
assert_equal(%w(to_liquid max min sort count first).to_set, RealEnumerableDrop.invokable_methods)
|
||||
end
|
||||
end # DropsTest
|
||||
|
||||
@@ -35,37 +35,35 @@ class ErrorHandlingTest < Minitest::Test
|
||||
TEXT
|
||||
|
||||
output = Liquid::Template.parse(template, line_numbers: true).render('errors' => ErrorDrop.new)
|
||||
assert_equal expected, output
|
||||
assert_equal(expected, output)
|
||||
end
|
||||
|
||||
def test_standard_error
|
||||
template = Liquid::Template.parse(' {{ errors.standard_error }} ')
|
||||
assert_equal ' Liquid error: standard error ', template.render('errors' => ErrorDrop.new)
|
||||
assert_equal(' Liquid error: standard error ', template.render('errors' => ErrorDrop.new))
|
||||
|
||||
assert_equal 1, template.errors.size
|
||||
assert_equal StandardError, template.errors.first.class
|
||||
assert_equal(1, template.errors.size)
|
||||
assert_equal(StandardError, template.errors.first.class)
|
||||
end
|
||||
|
||||
def test_syntax
|
||||
template = Liquid::Template.parse(' {{ errors.syntax_error }} ')
|
||||
assert_equal ' Liquid syntax error: syntax error ', template.render('errors' => ErrorDrop.new)
|
||||
assert_equal(' Liquid syntax error: syntax error ', template.render('errors' => ErrorDrop.new))
|
||||
|
||||
assert_equal 1, template.errors.size
|
||||
assert_equal SyntaxError, template.errors.first.class
|
||||
assert_equal(1, template.errors.size)
|
||||
assert_equal(SyntaxError, template.errors.first.class)
|
||||
end
|
||||
|
||||
def test_argument
|
||||
template = Liquid::Template.parse(' {{ errors.argument_error }} ')
|
||||
assert_equal ' Liquid error: argument error ', template.render('errors' => ErrorDrop.new)
|
||||
assert_equal(' Liquid error: argument error ', template.render('errors' => ErrorDrop.new))
|
||||
|
||||
assert_equal 1, template.errors.size
|
||||
assert_equal ArgumentError, template.errors.first.class
|
||||
assert_equal(1, template.errors.size)
|
||||
assert_equal(ArgumentError, template.errors.first.class)
|
||||
end
|
||||
|
||||
def test_missing_endtag_parse_time_error
|
||||
assert_raises(Liquid::SyntaxError) do
|
||||
Liquid::Template.parse(' {% for a in b %} ... ')
|
||||
end
|
||||
assert_match_syntax_error(/: 'for' tag was never closed\z/, ' {% for a in b %} ... ')
|
||||
end
|
||||
|
||||
def test_unrecognized_operator
|
||||
@@ -78,39 +76,32 @@ class ErrorHandlingTest < Minitest::Test
|
||||
|
||||
def test_lax_unrecognized_operator
|
||||
template = Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', error_mode: :lax)
|
||||
assert_equal ' Liquid error: Unknown operator =! ', template.render
|
||||
assert_equal 1, template.errors.size
|
||||
assert_equal Liquid::ArgumentError, template.errors.first.class
|
||||
assert_equal(' Liquid error: Unknown operator =! ', template.render)
|
||||
assert_equal(1, template.errors.size)
|
||||
assert_equal(Liquid::ArgumentError, template.errors.first.class)
|
||||
end
|
||||
|
||||
def test_with_line_numbers_adds_numbers_to_parser_errors
|
||||
err = assert_raises(SyntaxError) do
|
||||
Liquid::Template.parse('
|
||||
foobar
|
||||
source = <<~LIQUID
|
||||
foobar
|
||||
|
||||
{% "cat" | foobar %}
|
||||
{% "cat" | foobar %}
|
||||
|
||||
bla
|
||||
',
|
||||
line_numbers: true)
|
||||
end
|
||||
|
||||
assert_match(/Liquid syntax error \(line 4\)/, err.message)
|
||||
bla
|
||||
LIQUID
|
||||
assert_match_syntax_error(/Liquid syntax error \(line 3\)/, source)
|
||||
end
|
||||
|
||||
def test_with_line_numbers_adds_numbers_to_parser_errors_with_whitespace_trim
|
||||
err = assert_raises(SyntaxError) do
|
||||
Liquid::Template.parse('
|
||||
foobar
|
||||
source = <<~LIQUID
|
||||
foobar
|
||||
|
||||
{%- "cat" | foobar -%}
|
||||
{%- "cat" | foobar -%}
|
||||
|
||||
bla
|
||||
',
|
||||
line_numbers: true)
|
||||
end
|
||||
bla
|
||||
LIQUID
|
||||
|
||||
assert_match(/Liquid syntax error \(line 4\)/, err.message)
|
||||
assert_match_syntax_error(/Liquid syntax error \(line 3\)/, source)
|
||||
end
|
||||
|
||||
def test_parsing_warn_with_line_numbers_adds_numbers_to_lexer_errors
|
||||
@@ -124,8 +115,8 @@ class ErrorHandlingTest < Minitest::Test
|
||||
error_mode: :warn,
|
||||
line_numbers: true)
|
||||
|
||||
assert_equal ['Liquid syntax error (line 4): Unexpected character = in "1 =! 2"'],
|
||||
template.warnings.map(&:message)
|
||||
assert_equal(['Liquid syntax error (line 4): Unexpected character = in "1 =! 2"'],
|
||||
template.warnings.map(&:message))
|
||||
end
|
||||
|
||||
def test_parsing_strict_with_line_numbers_adds_numbers_to_lexer_errors
|
||||
@@ -141,59 +132,56 @@ class ErrorHandlingTest < Minitest::Test
|
||||
line_numbers: true)
|
||||
end
|
||||
|
||||
assert_equal 'Liquid syntax error (line 4): Unexpected character = in "1 =! 2"', err.message
|
||||
assert_equal('Liquid syntax error (line 4): Unexpected character = in "1 =! 2"', err.message)
|
||||
end
|
||||
|
||||
def test_syntax_errors_in_nested_blocks_have_correct_line_number
|
||||
err = assert_raises(SyntaxError) do
|
||||
Liquid::Template.parse('
|
||||
foobar
|
||||
source = <<~LIQUID
|
||||
foobar
|
||||
|
||||
{% if 1 != 2 %}
|
||||
{% foo %}
|
||||
{% endif %}
|
||||
{% if 1 != 2 %}
|
||||
{% foo %}
|
||||
{% endif %}
|
||||
|
||||
bla
|
||||
',
|
||||
line_numbers: true)
|
||||
end
|
||||
bla
|
||||
LIQUID
|
||||
|
||||
assert_equal "Liquid syntax error (line 5): Unknown tag 'foo'", err.message
|
||||
assert_match_syntax_error("Liquid syntax error (line 4): Unknown tag 'foo'", source)
|
||||
end
|
||||
|
||||
def test_strict_error_messages
|
||||
err = assert_raises(SyntaxError) do
|
||||
Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', error_mode: :strict)
|
||||
end
|
||||
assert_equal 'Liquid syntax error: Unexpected character = in "1 =! 2"', err.message
|
||||
assert_equal('Liquid syntax error: Unexpected character = in "1 =! 2"', err.message)
|
||||
|
||||
err = assert_raises(SyntaxError) do
|
||||
Liquid::Template.parse('{{%%%}}', error_mode: :strict)
|
||||
end
|
||||
assert_equal 'Liquid syntax error: Unexpected character % in "{{%%%}}"', err.message
|
||||
assert_equal('Liquid syntax error: Unexpected character % in "{{%%%}}"', err.message)
|
||||
end
|
||||
|
||||
def test_warnings
|
||||
template = Liquid::Template.parse('{% if ~~~ %}{{%%%}}{% else %}{{ hello. }}{% endif %}', error_mode: :warn)
|
||||
assert_equal 3, template.warnings.size
|
||||
assert_equal 'Unexpected character ~ in "~~~"', template.warnings[0].to_s(false)
|
||||
assert_equal 'Unexpected character % in "{{%%%}}"', template.warnings[1].to_s(false)
|
||||
assert_equal 'Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].to_s(false)
|
||||
assert_equal '', template.render
|
||||
assert_equal(3, template.warnings.size)
|
||||
assert_equal('Unexpected character ~ in "~~~"', template.warnings[0].to_s(false))
|
||||
assert_equal('Unexpected character % in "{{%%%}}"', template.warnings[1].to_s(false))
|
||||
assert_equal('Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].to_s(false))
|
||||
assert_equal('', template.render)
|
||||
end
|
||||
|
||||
def test_warning_line_numbers
|
||||
template = Liquid::Template.parse("{% if ~~~ %}\n{{%%%}}{% else %}\n{{ hello. }}{% endif %}", error_mode: :warn, line_numbers: true)
|
||||
assert_equal 'Liquid syntax error (line 1): Unexpected character ~ in "~~~"', template.warnings[0].message
|
||||
assert_equal 'Liquid syntax error (line 2): Unexpected character % in "{{%%%}}"', template.warnings[1].message
|
||||
assert_equal 'Liquid syntax error (line 3): Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].message
|
||||
assert_equal 3, template.warnings.size
|
||||
assert_equal [1, 2, 3], template.warnings.map(&:line_number)
|
||||
assert_equal('Liquid syntax error (line 1): Unexpected character ~ in "~~~"', template.warnings[0].message)
|
||||
assert_equal('Liquid syntax error (line 2): Unexpected character % in "{{%%%}}"', template.warnings[1].message)
|
||||
assert_equal('Liquid syntax error (line 3): Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].message)
|
||||
assert_equal(3, template.warnings.size)
|
||||
assert_equal([1, 2, 3], template.warnings.map(&:line_number))
|
||||
end
|
||||
|
||||
# Liquid should not catch Exceptions that are not subclasses of StandardError, like Interrupt and NoMemoryError
|
||||
def test_exceptions_propagate
|
||||
assert_raises Exception do
|
||||
assert_raises(Exception) do
|
||||
template = Liquid::Template.parse('{{ errors.exception }}')
|
||||
template.render('errors' => ErrorDrop.new)
|
||||
end
|
||||
@@ -204,35 +192,41 @@ class ErrorHandlingTest < Minitest::Test
|
||||
|
||||
output = template.render('errors' => ErrorDrop.new)
|
||||
|
||||
assert_equal 'This is a runtime error: Liquid error (line 1): internal', output
|
||||
assert_equal [Liquid::InternalError], template.errors.map(&:class)
|
||||
assert_equal('This is a runtime error: Liquid error (line 1): internal', output)
|
||||
assert_equal([Liquid::InternalError], template.errors.map(&:class))
|
||||
end
|
||||
|
||||
def test_setting_default_exception_renderer
|
||||
old_exception_renderer = Liquid::Template.default_exception_renderer
|
||||
exceptions = []
|
||||
Liquid::Template.default_exception_renderer = ->(e) { exceptions << e; '' }
|
||||
Liquid::Template.default_exception_renderer = ->(e) {
|
||||
exceptions << e
|
||||
''
|
||||
}
|
||||
template = Liquid::Template.parse('This is a runtime error: {{ errors.argument_error }}')
|
||||
|
||||
output = template.render('errors' => ErrorDrop.new)
|
||||
|
||||
assert_equal 'This is a runtime error: ', output
|
||||
assert_equal [Liquid::ArgumentError], template.errors.map(&:class)
|
||||
assert_equal('This is a runtime error: ', output)
|
||||
assert_equal([Liquid::ArgumentError], template.errors.map(&:class))
|
||||
ensure
|
||||
Liquid::Template.default_exception_renderer = old_exception_renderer if old_exception_renderer
|
||||
end
|
||||
|
||||
def test_exception_renderer_exposing_non_liquid_error
|
||||
template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true)
|
||||
template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true)
|
||||
exceptions = []
|
||||
handler = ->(e) { exceptions << e; e.cause }
|
||||
handler = ->(e) {
|
||||
exceptions << e
|
||||
e.cause
|
||||
}
|
||||
|
||||
output = template.render({ 'errors' => ErrorDrop.new }, exception_renderer: handler)
|
||||
|
||||
assert_equal 'This is a runtime error: runtime error', output
|
||||
assert_equal [Liquid::InternalError], exceptions.map(&:class)
|
||||
assert_equal exceptions, template.errors
|
||||
assert_equal '#<RuntimeError: runtime error>', exceptions.first.cause.inspect
|
||||
assert_equal('This is a runtime error: runtime error', output)
|
||||
assert_equal([Liquid::InternalError], exceptions.map(&:class))
|
||||
assert_equal(exceptions, template.errors)
|
||||
assert_equal('#<RuntimeError: runtime error>', exceptions.first.cause.inspect)
|
||||
end
|
||||
|
||||
class TestFileSystem
|
||||
@@ -246,12 +240,21 @@ class ErrorHandlingTest < Minitest::Test
|
||||
|
||||
begin
|
||||
Liquid::Template.file_system = TestFileSystem.new
|
||||
|
||||
template = Liquid::Template.parse("Argument error:\n{% include 'product' %}", line_numbers: true)
|
||||
page = template.render('errors' => ErrorDrop.new)
|
||||
page = template.render('errors' => ErrorDrop.new)
|
||||
ensure
|
||||
Liquid::Template.file_system = old_file_system
|
||||
end
|
||||
assert_equal "Argument error:\nLiquid error (product line 1): argument error", page
|
||||
assert_equal "product", template.errors.first.template_name
|
||||
assert_equal("Argument error:\nLiquid error (product line 1): argument error", page)
|
||||
assert_equal("product", template.errors.first.template_name)
|
||||
end
|
||||
|
||||
def test_bug_compatible_silencing_of_errors_in_blank_nodes
|
||||
output = Liquid::Template.parse("{% assign x = 0 %}{% if 1 < '2' %}not blank{% assign x = 3 %}{% endif %}{{ x }}").render
|
||||
assert_equal("Liquid error: comparison of Integer with String failed0", output)
|
||||
|
||||
output = Liquid::Template.parse("{% assign x = 0 %}{% if 1 < '2' %}{% assign x = 3 %}{% endif %}{{ x }}").render
|
||||
assert_equal("0", output)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class ExpressionTest < Minitest::Test
|
||||
def test_keyword_literals
|
||||
assert_template_result("true", "{{ true }}")
|
||||
assert_expression_result(true, "true")
|
||||
end
|
||||
|
||||
def test_string
|
||||
assert_template_result("single quoted", "{{'single quoted'}}")
|
||||
assert_template_result("double quoted", '{{"double quoted"}}')
|
||||
assert_template_result("spaced", "{{ 'spaced' }}")
|
||||
assert_template_result("spaced2", "{{ 'spaced2' }}")
|
||||
end
|
||||
|
||||
def test_int
|
||||
assert_template_result("456", "{{ 456 }}")
|
||||
assert_expression_result(123, "123")
|
||||
assert_expression_result(12, "012")
|
||||
end
|
||||
|
||||
def test_float
|
||||
assert_template_result("2.5", "{{ 2.5 }}")
|
||||
assert_expression_result(1.5, "1.5")
|
||||
end
|
||||
|
||||
def test_range
|
||||
assert_template_result("3..4", "{{ ( 3 .. 4 ) }}")
|
||||
assert_expression_result(1..2, "(1..2)")
|
||||
|
||||
assert_match_syntax_error(
|
||||
"Liquid syntax error (line 1): Invalid expression type 'false' in range expression",
|
||||
"{{ (false..true) }}"
|
||||
)
|
||||
assert_match_syntax_error(
|
||||
"Liquid syntax error (line 1): Invalid expression type '(1..2)' in range expression",
|
||||
"{{ ((1..2)..3) }}"
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def assert_expression_result(expect, markup, **assigns)
|
||||
liquid = "{% if expect == #{markup} %}pass{% else %}got {{ #{markup} }}{% endif %}"
|
||||
assert_template_result("pass", liquid, { "expect" => expect, **assigns })
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,24 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class FilterKwargTest < Minitest::Test
|
||||
module KwargFilter
|
||||
def html_tag(_tag, attributes)
|
||||
attributes
|
||||
.map { |key, value| "#{key}='#{value}'" }
|
||||
.join(' ')
|
||||
end
|
||||
end
|
||||
|
||||
include Liquid
|
||||
|
||||
def test_can_parse_data_kwargs
|
||||
with_global_filter(KwargFilter) do
|
||||
assert_equal(
|
||||
"data-src='src' data-widths='100, 200'",
|
||||
Template.parse("{{ 'img' | html_tag: data-src: 'src', data-widths: '100, 200' }}").render(nil, nil)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -41,13 +41,13 @@ class FiltersTest < Minitest::Test
|
||||
@context['var'] = 1000
|
||||
@context.add_filters(MoneyFilter)
|
||||
|
||||
assert_equal ' 1000$ ', Template.parse("{{var | money}}").render(@context)
|
||||
assert_equal(' 1000$ ', Template.parse("{{var | money}}").render(@context))
|
||||
end
|
||||
|
||||
def test_underscore_in_filter_name
|
||||
@context['var'] = 1000
|
||||
@context.add_filters(MoneyFilter)
|
||||
assert_equal ' 1000$ ', Template.parse("{{var | money_with_underscore}}").render(@context)
|
||||
assert_equal(' 1000$ ', Template.parse("{{var | money_with_underscore}}").render(@context))
|
||||
end
|
||||
|
||||
def test_second_filter_overwrites_first
|
||||
@@ -55,103 +55,94 @@ class FiltersTest < Minitest::Test
|
||||
@context.add_filters(MoneyFilter)
|
||||
@context.add_filters(CanadianMoneyFilter)
|
||||
|
||||
assert_equal ' 1000$ CAD ', Template.parse("{{var | money}}").render(@context)
|
||||
assert_equal(' 1000$ CAD ', Template.parse("{{var | money}}").render(@context))
|
||||
end
|
||||
|
||||
def test_size
|
||||
@context['var'] = 'abcd'
|
||||
@context.add_filters(MoneyFilter)
|
||||
|
||||
assert_equal '4', Template.parse("{{var | size}}").render(@context)
|
||||
assert_template_result("4", "{{var | size}}", { "var" => 'abcd' })
|
||||
end
|
||||
|
||||
def test_join
|
||||
@context['var'] = [1, 2, 3, 4]
|
||||
|
||||
assert_equal "1 2 3 4", Template.parse("{{var | join}}").render(@context)
|
||||
assert_template_result("1 2 3 4", "{{var | join}}", { "var" => [1, 2, 3, 4] })
|
||||
end
|
||||
|
||||
def test_sort
|
||||
@context['value'] = 3
|
||||
@context['numbers'] = [2, 1, 4, 3]
|
||||
@context['words'] = ['expected', 'as', 'alphabetic']
|
||||
@context['arrays'] = ['flower', 'are']
|
||||
@context['case_sensitive'] = ['sensitive', 'Expected', 'case']
|
||||
|
||||
assert_equal '1 2 3 4', Template.parse("{{numbers | sort | join}}").render(@context)
|
||||
assert_equal 'alphabetic as expected', Template.parse("{{words | sort | join}}").render(@context)
|
||||
assert_equal '3', Template.parse("{{value | sort}}").render(@context)
|
||||
assert_equal 'are flower', Template.parse("{{arrays | sort | join}}").render(@context)
|
||||
assert_equal 'Expected case sensitive', Template.parse("{{case_sensitive | sort | join}}").render(@context)
|
||||
assert_template_result("1 2 3 4", "{{numbers | sort | join}}", { "numbers" => [2, 1, 4, 3] })
|
||||
assert_template_result("alphabetic as expected", "{{words | sort | join}}",
|
||||
{ "words" => ['expected', 'as', 'alphabetic'] })
|
||||
assert_template_result("3", "{{value | sort}}", { "value" => 3 })
|
||||
assert_template_result('are flower', "{{arrays | sort | join}}", { 'arrays' => ['flower', 'are'] })
|
||||
assert_template_result("Expected case sensitive", "{{case_sensitive | sort | join}}",
|
||||
{ "case_sensitive" => ["sensitive", "Expected", "case"] })
|
||||
end
|
||||
|
||||
def test_sort_natural
|
||||
@context['words'] = ['case', 'Assert', 'Insensitive']
|
||||
@context['hashes'] = [{ 'a' => 'A' }, { 'a' => 'b' }, { 'a' => 'C' }]
|
||||
@context['objects'] = [TestObject.new('A'), TestObject.new('b'), TestObject.new('C')]
|
||||
|
||||
# Test strings
|
||||
assert_equal 'Assert case Insensitive', Template.parse("{{words | sort_natural | join}}").render(@context)
|
||||
assert_template_result("Assert case Insensitive", "{{words | sort_natural | join}}",
|
||||
{ "words" => ["case", "Assert", "Insensitive"] })
|
||||
|
||||
# Test hashes
|
||||
assert_equal 'A b C', Template.parse("{{hashes | sort_natural: 'a' | map: 'a' | join}}").render(@context)
|
||||
assert_template_result("A b C", "{{hashes | sort_natural: 'a' | map: 'a' | join}}",
|
||||
{ "hashes" => [{ "a" => "A" }, { "a" => "b" }, { "a" => "C" }] })
|
||||
|
||||
# Test objects
|
||||
assert_equal 'A b C', Template.parse("{{objects | sort_natural: 'a' | map: 'a' | join}}").render(@context)
|
||||
@context['objects'] = [TestObject.new('A'), TestObject.new('b'), TestObject.new('C')]
|
||||
assert_equal('A b C', Template.parse("{{objects | sort_natural: 'a' | map: 'a' | join}}").render(@context))
|
||||
end
|
||||
|
||||
def test_compact
|
||||
@context['words'] = ['a', nil, 'b', nil, 'c']
|
||||
@context['hashes'] = [{ 'a' => 'A' }, { 'a' => nil }, { 'a' => 'C' }]
|
||||
@context['objects'] = [TestObject.new('A'), TestObject.new(nil), TestObject.new('C')]
|
||||
|
||||
# Test strings
|
||||
assert_equal 'a b c', Template.parse("{{words | compact | join}}").render(@context)
|
||||
assert_template_result("a b c", "{{words | compact | join}}",
|
||||
{ "words" => ['a', nil, 'b', nil, 'c'] })
|
||||
|
||||
# Test hashes
|
||||
assert_equal 'A C', Template.parse("{{hashes | compact: 'a' | map: 'a' | join}}").render(@context)
|
||||
assert_template_result("A C", "{{hashes | compact: 'a' | map: 'a' | join}}",
|
||||
{ "hashes" => [{ "a" => "A" }, { "a" => nil }, { "a" => "C" }] })
|
||||
|
||||
# Test objects
|
||||
assert_equal 'A C', Template.parse("{{objects | compact: 'a' | map: 'a' | join}}").render(@context)
|
||||
@context['objects'] = [TestObject.new('A'), TestObject.new(nil), TestObject.new('C')]
|
||||
assert_equal('A C', Template.parse("{{objects | compact: 'a' | map: 'a' | join}}").render(@context))
|
||||
end
|
||||
|
||||
def test_strip_html
|
||||
@context['var'] = "<b>bla blub</a>"
|
||||
|
||||
assert_equal "bla blub", Template.parse("{{ var | strip_html }}").render(@context)
|
||||
assert_template_result("bla blub", "{{ var | strip_html }}", { "var" => "<b>bla blub</a>" })
|
||||
end
|
||||
|
||||
def test_strip_html_ignore_comments_with_html
|
||||
@context['var'] = "<!-- split and some <ul> tag --><b>bla blub</a>"
|
||||
|
||||
assert_equal "bla blub", Template.parse("{{ var | strip_html }}").render(@context)
|
||||
assert_template_result("bla blub", "{{ var | strip_html }}",
|
||||
{ "var" => "<!-- split and some <ul> tag --><b>bla blub</a>" })
|
||||
end
|
||||
|
||||
def test_capitalize
|
||||
@context['var'] = "blub"
|
||||
|
||||
assert_equal "Blub", Template.parse("{{ var | capitalize }}").render(@context)
|
||||
assert_template_result("Blub", "{{ var | capitalize }}", { "var" => "blub" })
|
||||
end
|
||||
|
||||
def test_nonexistent_filter_is_ignored
|
||||
@context['var'] = 1000
|
||||
|
||||
assert_equal '1000', Template.parse("{{ var | xyzzy }}").render(@context)
|
||||
assert_template_result("1000", "{{ var | xyzzy }}", { "var" => 1000 })
|
||||
end
|
||||
|
||||
def test_filter_with_keyword_arguments
|
||||
@context['surname'] = 'john'
|
||||
@context['input'] = 'hello %{first_name}, %{last_name}'
|
||||
@context['input'] = 'hello %{first_name}, %{last_name}'
|
||||
@context.add_filters(SubstituteFilter)
|
||||
output = Template.parse(%({{ input | substitute: first_name: surname, last_name: 'doe' }})).render(@context)
|
||||
assert_equal 'hello john, doe', output
|
||||
output = Template.parse(%({{ input | substitute: first_name: surname, last_name: 'doe' }})).render(@context)
|
||||
assert_equal('hello john, doe', output)
|
||||
end
|
||||
|
||||
def test_override_object_method_in_filter
|
||||
assert_equal "tap overridden", Template.parse("{{var | tap}}").render!({ 'var' => 1000 }, filters: [OverrideObjectMethodFilter])
|
||||
assert_equal("tap overridden", Template.parse("{{var | tap}}").render!({ 'var' => 1000 }, filters: [OverrideObjectMethodFilter]))
|
||||
|
||||
# tap still treated as a non-existent filter
|
||||
assert_equal "1000", Template.parse("{{var | tap}}").render!('var' => 1000)
|
||||
assert_equal("1000", Template.parse("{{var | tap}}").render!('var' => 1000))
|
||||
end
|
||||
|
||||
def test_liquid_argument_error
|
||||
source = "{{ '' | size: 'too many args' }}"
|
||||
exc = assert_raises(Liquid::ArgumentError) do
|
||||
Template.parse(source).render!
|
||||
end
|
||||
assert_match(/\ALiquid error: wrong number of arguments /, exc.message)
|
||||
assert_equal(exc.message, Template.parse(source).render)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -160,15 +151,15 @@ class FiltersInTemplate < Minitest::Test
|
||||
|
||||
def test_local_global
|
||||
with_global_filter(MoneyFilter) do
|
||||
assert_equal " 1000$ ", Template.parse("{{1000 | money}}").render!(nil, nil)
|
||||
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, filters: CanadianMoneyFilter)
|
||||
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, filters: [CanadianMoneyFilter])
|
||||
assert_equal(" 1000$ ", Template.parse("{{1000 | money}}").render!(nil, nil))
|
||||
assert_equal(" 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, filters: CanadianMoneyFilter))
|
||||
assert_equal(" 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, filters: [CanadianMoneyFilter]))
|
||||
end
|
||||
end
|
||||
|
||||
def test_local_filter_with_deprecated_syntax
|
||||
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, CanadianMoneyFilter)
|
||||
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, [CanadianMoneyFilter])
|
||||
assert_equal(" 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, CanadianMoneyFilter))
|
||||
assert_equal(" 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, [CanadianMoneyFilter]))
|
||||
end
|
||||
end # FiltersTest
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ class HashOrderingTest < Minitest::Test
|
||||
|
||||
def test_global_register_order
|
||||
with_global_filter(MoneyFilter, CanadianMoneyFilter) do
|
||||
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render(nil, nil)
|
||||
assert_equal(" 1000$ CAD ", Template.parse("{{1000 | money}}").render(nil, nil))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -33,93 +33,88 @@ class OutputTest < Minitest::Test
|
||||
|
||||
def setup
|
||||
@assigns = {
|
||||
'best_cars' => 'bmw',
|
||||
'car' => { 'bmw' => 'good', 'gm' => 'bad' },
|
||||
}
|
||||
end
|
||||
|
||||
def test_variable
|
||||
text = %( {{best_cars}} )
|
||||
|
||||
expected = %( bmw )
|
||||
assert_equal expected, Template.parse(text).render!(@assigns)
|
||||
assert_template_result(" bmw ", " {{best_cars}} ", { "best_cars" => "bmw" })
|
||||
end
|
||||
|
||||
def test_variable_traversing_with_two_brackets
|
||||
text = %({{ site.data.menu[include.menu][include.locale] }})
|
||||
assert_equal "it works!", Template.parse(text).render!(
|
||||
source = "{{ site.data.menu[include.menu][include.locale] }}"
|
||||
assert_template_result("it works!", source, {
|
||||
"site" => { "data" => { "menu" => { "foo" => { "bar" => "it works!" } } } },
|
||||
"include" => { "menu" => "foo", "locale" => "bar" }
|
||||
)
|
||||
"include" => { "menu" => "foo", "locale" => "bar" },
|
||||
})
|
||||
end
|
||||
|
||||
def test_variable_traversing
|
||||
text = %( {{car.bmw}} {{car.gm}} {{car.bmw}} )
|
||||
|
||||
expected = %( good bad good )
|
||||
assert_equal expected, Template.parse(text).render!(@assigns)
|
||||
source = " {{car.bmw}} {{car.gm}} {{car.bmw}} "
|
||||
assert_template_result(" good bad good ", source, @assigns)
|
||||
end
|
||||
|
||||
def test_variable_piping
|
||||
text = %( {{ car.gm | make_funny }} )
|
||||
text = %( {{ car.gm | make_funny }} )
|
||||
expected = %( LOL )
|
||||
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
|
||||
assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
|
||||
end
|
||||
|
||||
def test_variable_piping_with_input
|
||||
text = %( {{ car.gm | cite_funny }} )
|
||||
text = %( {{ car.gm | cite_funny }} )
|
||||
expected = %( LOL: bad )
|
||||
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
|
||||
assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
|
||||
end
|
||||
|
||||
def test_variable_piping_with_args
|
||||
text = %! {{ car.gm | add_smiley : ':-(' }} !
|
||||
text = %! {{ car.gm | add_smiley : ':-(' }} !
|
||||
expected = %| bad :-( |
|
||||
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
|
||||
assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
|
||||
end
|
||||
|
||||
def test_variable_piping_with_no_args
|
||||
text = %( {{ car.gm | add_smiley }} )
|
||||
text = %( {{ car.gm | add_smiley }} )
|
||||
expected = %| bad :-) |
|
||||
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
|
||||
assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
|
||||
end
|
||||
|
||||
def test_multiple_variable_piping_with_args
|
||||
text = %! {{ car.gm | add_smiley : ':-(' | add_smiley : ':-('}} !
|
||||
text = %! {{ car.gm | add_smiley : ':-(' | add_smiley : ':-('}} !
|
||||
expected = %| bad :-( :-( |
|
||||
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
|
||||
assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
|
||||
end
|
||||
|
||||
def test_variable_piping_with_multiple_args
|
||||
text = %( {{ car.gm | add_tag : 'span', 'bar'}} )
|
||||
text = %( {{ car.gm | add_tag : 'span', 'bar'}} )
|
||||
expected = %( <span id="bar">bad</span> )
|
||||
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
|
||||
assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
|
||||
end
|
||||
|
||||
def test_variable_piping_with_variable_args
|
||||
text = %( {{ car.gm | add_tag : 'span', car.bmw}} )
|
||||
text = %( {{ car.gm | add_tag : 'span', car.bmw}} )
|
||||
expected = %( <span id="good">bad</span> )
|
||||
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
|
||||
assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
|
||||
end
|
||||
|
||||
def test_multiple_pipings
|
||||
text = %( {{ best_cars | cite_funny | paragraph }} )
|
||||
assigns = { 'best_cars' => 'bmw' }
|
||||
text = %( {{ best_cars | cite_funny | paragraph }} )
|
||||
expected = %( <p>LOL: bmw</p> )
|
||||
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
|
||||
assert_equal(expected, Template.parse(text).render!(assigns, filters: [FunnyFilter]))
|
||||
end
|
||||
|
||||
def test_link_to
|
||||
text = %( {{ 'Typo' | link_to: 'http://typo.leetsoft.com' }} )
|
||||
text = %( {{ 'Typo' | link_to: 'http://typo.leetsoft.com' }} )
|
||||
expected = %( <a href="http://typo.leetsoft.com">Typo</a> )
|
||||
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
|
||||
assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
|
||||
end
|
||||
end # OutputTest
|
||||
|
||||
@@ -7,7 +7,7 @@ class ParsingQuirksTest < Minitest::Test
|
||||
|
||||
def test_parsing_css
|
||||
text = " div { font-weight: bold; } "
|
||||
assert_equal text, Template.parse(text).render!
|
||||
assert_equal(text, Template.parse(text).render!)
|
||||
end
|
||||
|
||||
def test_raise_on_single_close_bracet
|
||||
@@ -29,10 +29,10 @@ class ParsingQuirksTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_error_on_empty_filter
|
||||
assert Template.parse("{{test}}")
|
||||
assert(Template.parse("{{test}}"))
|
||||
|
||||
with_error_mode(:lax) do
|
||||
assert Template.parse("{{|test}}")
|
||||
assert(Template.parse("{{|test}}"))
|
||||
end
|
||||
|
||||
with_error_mode(:strict) do
|
||||
@@ -64,15 +64,15 @@ class ParsingQuirksTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_no_error_on_lax_empty_filter
|
||||
assert Template.parse("{{test |a|b|}}", error_mode: :lax)
|
||||
assert Template.parse("{{test}}", error_mode: :lax)
|
||||
assert Template.parse("{{|test|}}", error_mode: :lax)
|
||||
assert(Template.parse("{{test |a|b|}}", error_mode: :lax))
|
||||
assert(Template.parse("{{test}}", error_mode: :lax))
|
||||
assert(Template.parse("{{|test|}}", error_mode: :lax))
|
||||
end
|
||||
|
||||
def test_meaningless_parens_lax
|
||||
with_error_mode(:lax) do
|
||||
assigns = { 'b' => 'bar', 'c' => 'baz' }
|
||||
markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false"
|
||||
markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false"
|
||||
assert_template_result(' YES ', "{% if #{markup} %} YES {% endif %}", assigns)
|
||||
end
|
||||
end
|
||||
@@ -118,7 +118,17 @@ class ParsingQuirksTest < Minitest::Test
|
||||
end
|
||||
end
|
||||
|
||||
def test_blank_variable_markup
|
||||
assert_template_result('', "{{}}")
|
||||
end
|
||||
|
||||
def test_lookup_on_var_with_literal_name
|
||||
assigns = { "blank" => { "x" => "result" } }
|
||||
assert_template_result('result', "{{ blank.x }}", assigns)
|
||||
assert_template_result('result', "{{ blank['x'] }}", assigns)
|
||||
end
|
||||
|
||||
def test_contains_in_id
|
||||
assert_template_result(' YES ', '{% if containsallshipments == true %} YES {% endif %}', 'containsallshipments' => true)
|
||||
assert_template_result(' YES ', '{% if containsallshipments == true %} YES {% endif %}', { 'containsallshipments' => true })
|
||||
end
|
||||
end # ParsingQuirksTest
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class ProfilerTest < Minitest::Test
|
||||
class TestDrop < Liquid::Drop
|
||||
def initialize(value)
|
||||
super()
|
||||
@value = value
|
||||
end
|
||||
|
||||
def to_s
|
||||
artificial_execution_time
|
||||
|
||||
@value
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Monotonic clock precision fluctuate based on the operating system
|
||||
# By introducing a small sleep we ensure ourselves to register a non zero unit of time
|
||||
def artificial_execution_time
|
||||
sleep(Process.clock_getres(Process::CLOCK_MONOTONIC))
|
||||
end
|
||||
end
|
||||
|
||||
include Liquid
|
||||
|
||||
class ProfilingFileSystem
|
||||
def read_template_file(template_path)
|
||||
"Rendering template {% assign template_name = '#{template_path}'%}\n{{ template_name }}"
|
||||
end
|
||||
end
|
||||
|
||||
def setup
|
||||
Liquid::Template.file_system = ProfilingFileSystem.new
|
||||
end
|
||||
|
||||
def test_template_allows_flagging_profiling
|
||||
t = Template.parse("{{ 'a string' | upcase }}")
|
||||
t.render!
|
||||
|
||||
assert_nil(t.profiler)
|
||||
end
|
||||
|
||||
def test_parse_makes_available_simple_profiling
|
||||
t = Template.parse("{{ 'a string' | upcase }}", profile: true)
|
||||
t.render!
|
||||
|
||||
assert_equal(1, t.profiler.length)
|
||||
|
||||
node = t.profiler[0]
|
||||
assert_equal(" 'a string' | upcase ", node.code)
|
||||
end
|
||||
|
||||
def test_render_ignores_raw_strings_when_profiling
|
||||
t = Template.parse("This is raw string\nstuff\nNewline", profile: true)
|
||||
t.render!
|
||||
|
||||
assert_equal(0, t.profiler.length)
|
||||
end
|
||||
|
||||
def test_profiling_includes_line_numbers_of_liquid_nodes
|
||||
t = Template.parse("{{ 'a string' | upcase }}\n{% increment test %}", profile: true)
|
||||
t.render!
|
||||
assert_equal(2, t.profiler.length)
|
||||
|
||||
# {{ 'a string' | upcase }}
|
||||
assert_equal(1, t.profiler[0].line_number)
|
||||
# {{ increment test }}
|
||||
assert_equal(2, t.profiler[1].line_number)
|
||||
end
|
||||
|
||||
def test_profiling_includes_line_numbers_of_included_partials
|
||||
t = Template.parse("{% include 'a_template' %}", profile: true)
|
||||
t.render!
|
||||
|
||||
included_children = t.profiler[0].children
|
||||
|
||||
# {% assign template_name = 'a_template' %}
|
||||
assert_equal(1, included_children[0].line_number)
|
||||
# {{ template_name }}
|
||||
assert_equal(2, included_children[1].line_number)
|
||||
end
|
||||
|
||||
def test_profiling_render_tag
|
||||
t = Template.parse("{% render 'a_template' %}", profile: true)
|
||||
t.render!
|
||||
|
||||
render_children = t.profiler[0].children
|
||||
render_children.each do |timing|
|
||||
assert_equal('a_template', timing.partial)
|
||||
end
|
||||
assert_equal([1, 2], render_children.map(&:line_number))
|
||||
end
|
||||
|
||||
def test_profiling_times_the_rendering_of_tokens
|
||||
t = Template.parse("{% include 'a_template' %}", profile: true)
|
||||
t.render!
|
||||
|
||||
node = t.profiler[0]
|
||||
refute_nil(node.render_time)
|
||||
end
|
||||
|
||||
def test_profiling_times_the_entire_render
|
||||
t = Template.parse("{% include 'a_template' %}", profile: true)
|
||||
t.render!
|
||||
|
||||
assert(t.profiler.total_render_time >= 0, "Total render time was not calculated")
|
||||
end
|
||||
|
||||
class SleepTag < Liquid::Tag
|
||||
def initialize(tag_name, markup, parse_context)
|
||||
super
|
||||
@duration = Float(markup)
|
||||
end
|
||||
|
||||
def render_to_output_buffer(_context, _output)
|
||||
sleep(@duration)
|
||||
end
|
||||
end
|
||||
|
||||
def test_profiling_multiple_renders
|
||||
with_custom_tag('sleep', SleepTag) do
|
||||
context = Liquid::Context.new
|
||||
t = Liquid::Template.parse("{% sleep 0.001 %}", profile: true)
|
||||
context.template_name = 'index'
|
||||
t.render!(context)
|
||||
context.template_name = 'layout'
|
||||
first_render_time = context.profiler.total_time
|
||||
t.render!(context)
|
||||
|
||||
profiler = context.profiler
|
||||
children = profiler.children
|
||||
assert_operator(first_render_time, :>=, 0.001)
|
||||
assert_operator(profiler.total_time, :>=, 0.001 + first_render_time)
|
||||
assert_equal(["index", "layout"], children.map(&:template_name))
|
||||
assert_equal([nil, nil], children.map(&:code))
|
||||
assert_equal(profiler.total_time, children.map(&:total_time).reduce(&:+))
|
||||
end
|
||||
end
|
||||
|
||||
def test_profiling_uses_include_to_mark_children
|
||||
t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}", profile: true)
|
||||
t.render!
|
||||
|
||||
include_node = t.profiler[1]
|
||||
assert_equal(2, include_node.children.length)
|
||||
end
|
||||
|
||||
def test_profiling_marks_children_with_the_name_of_included_partial
|
||||
t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}", profile: true)
|
||||
t.render!
|
||||
|
||||
include_node = t.profiler[1]
|
||||
include_node.children.each do |child|
|
||||
assert_equal("a_template", child.partial)
|
||||
end
|
||||
end
|
||||
|
||||
def test_profiling_supports_multiple_templates
|
||||
t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}\n{% include 'b_template' %}", profile: true)
|
||||
t.render!
|
||||
|
||||
a_template = t.profiler[1]
|
||||
a_template.children.each do |child|
|
||||
assert_equal("a_template", child.partial)
|
||||
end
|
||||
|
||||
b_template = t.profiler[2]
|
||||
b_template.children.each do |child|
|
||||
assert_equal("b_template", child.partial)
|
||||
end
|
||||
end
|
||||
|
||||
def test_profiling_supports_rendering_the_same_partial_multiple_times
|
||||
t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}\n{% include 'a_template' %}", profile: true)
|
||||
t.render!
|
||||
|
||||
a_template1 = t.profiler[1]
|
||||
a_template1.children.each do |child|
|
||||
assert_equal("a_template", child.partial)
|
||||
end
|
||||
|
||||
a_template2 = t.profiler[2]
|
||||
a_template2.children.each do |child|
|
||||
assert_equal("a_template", child.partial)
|
||||
end
|
||||
end
|
||||
|
||||
def test_can_iterate_over_each_profiling_entry
|
||||
t = Template.parse("{{ 'a string' | upcase }}\n{% increment test %}", profile: true)
|
||||
t.render!
|
||||
|
||||
timing_count = 0
|
||||
t.profiler.each do |_timing|
|
||||
timing_count += 1
|
||||
end
|
||||
|
||||
assert_equal(2, timing_count)
|
||||
end
|
||||
|
||||
def test_profiling_marks_children_of_if_blocks
|
||||
t = Template.parse("{% if true %} {% increment test %} {{ test }} {% endif %}", profile: true)
|
||||
t.render!
|
||||
|
||||
assert_equal(1, t.profiler.length)
|
||||
assert_equal(2, t.profiler[0].children.length)
|
||||
end
|
||||
|
||||
def test_profiling_marks_children_of_for_blocks
|
||||
t = Template.parse("{% for item in collection %} {{ item }} {% endfor %}", profile: true)
|
||||
t.render!("collection" => ["one", "two"])
|
||||
|
||||
assert_equal(1, t.profiler.length)
|
||||
# Will profile each invocation of the for block
|
||||
assert_equal(2, t.profiler[0].children.length)
|
||||
end
|
||||
|
||||
def test_profiling_supports_self_time
|
||||
t = Template.parse("{% for item in collection %} {{ item }} {% endfor %}", profile: true)
|
||||
collection = [
|
||||
TestDrop.new("one"),
|
||||
TestDrop.new("two"),
|
||||
]
|
||||
output = t.render!("collection" => collection)
|
||||
assert_equal(" one two ", output)
|
||||
|
||||
leaf = t.profiler[0].children[0]
|
||||
assert_operator(leaf.self_time, :>, 0.0)
|
||||
end
|
||||
|
||||
def test_profiling_supports_total_time
|
||||
t = Template.parse("{% if true %} {{ test }} {% endif %}", profile: true)
|
||||
output = t.render!("test" => TestDrop.new("one"))
|
||||
assert_equal(" one ", output)
|
||||
|
||||
assert_operator(t.profiler[0].total_time, :>, 0.0)
|
||||
end
|
||||
end
|
||||
@@ -1,156 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class RenderProfilingTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
class ProfilingFileSystem
|
||||
def read_template_file(template_path)
|
||||
"Rendering template {% assign template_name = '#{template_path}'%}\n{{ template_name }}"
|
||||
end
|
||||
end
|
||||
|
||||
def setup
|
||||
Liquid::Template.file_system = ProfilingFileSystem.new
|
||||
end
|
||||
|
||||
def test_template_allows_flagging_profiling
|
||||
t = Template.parse("{{ 'a string' | upcase }}")
|
||||
t.render!
|
||||
|
||||
assert_nil t.profiler
|
||||
end
|
||||
|
||||
def test_parse_makes_available_simple_profiling
|
||||
t = Template.parse("{{ 'a string' | upcase }}", profile: true)
|
||||
t.render!
|
||||
|
||||
assert_equal 1, t.profiler.length
|
||||
|
||||
node = t.profiler[0]
|
||||
assert_equal " 'a string' | upcase ", node.code
|
||||
end
|
||||
|
||||
def test_render_ignores_raw_strings_when_profiling
|
||||
t = Template.parse("This is raw string\nstuff\nNewline", profile: true)
|
||||
t.render!
|
||||
|
||||
assert_equal 0, t.profiler.length
|
||||
end
|
||||
|
||||
def test_profiling_includes_line_numbers_of_liquid_nodes
|
||||
t = Template.parse("{{ 'a string' | upcase }}\n{% increment test %}", profile: true)
|
||||
t.render!
|
||||
assert_equal 2, t.profiler.length
|
||||
|
||||
# {{ 'a string' | upcase }}
|
||||
assert_equal 1, t.profiler[0].line_number
|
||||
# {{ increment test }}
|
||||
assert_equal 2, t.profiler[1].line_number
|
||||
end
|
||||
|
||||
def test_profiling_includes_line_numbers_of_included_partials
|
||||
t = Template.parse("{% include 'a_template' %}", profile: true)
|
||||
t.render!
|
||||
|
||||
included_children = t.profiler[0].children
|
||||
|
||||
# {% assign template_name = 'a_template' %}
|
||||
assert_equal 1, included_children[0].line_number
|
||||
# {{ template_name }}
|
||||
assert_equal 2, included_children[1].line_number
|
||||
end
|
||||
|
||||
def test_profiling_times_the_rendering_of_tokens
|
||||
t = Template.parse("{% include 'a_template' %}", profile: true)
|
||||
t.render!
|
||||
|
||||
node = t.profiler[0]
|
||||
refute_nil node.render_time
|
||||
end
|
||||
|
||||
def test_profiling_times_the_entire_render
|
||||
t = Template.parse("{% include 'a_template' %}", profile: true)
|
||||
t.render!
|
||||
|
||||
assert t.profiler.total_render_time >= 0, "Total render time was not calculated"
|
||||
end
|
||||
|
||||
def test_profiling_uses_include_to_mark_children
|
||||
t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}", profile: true)
|
||||
t.render!
|
||||
|
||||
include_node = t.profiler[1]
|
||||
assert_equal 2, include_node.children.length
|
||||
end
|
||||
|
||||
def test_profiling_marks_children_with_the_name_of_included_partial
|
||||
t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}", profile: true)
|
||||
t.render!
|
||||
|
||||
include_node = t.profiler[1]
|
||||
include_node.children.each do |child|
|
||||
assert_equal "a_template", child.partial
|
||||
end
|
||||
end
|
||||
|
||||
def test_profiling_supports_multiple_templates
|
||||
t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}\n{% include 'b_template' %}", profile: true)
|
||||
t.render!
|
||||
|
||||
a_template = t.profiler[1]
|
||||
a_template.children.each do |child|
|
||||
assert_equal "a_template", child.partial
|
||||
end
|
||||
|
||||
b_template = t.profiler[2]
|
||||
b_template.children.each do |child|
|
||||
assert_equal "b_template", child.partial
|
||||
end
|
||||
end
|
||||
|
||||
def test_profiling_supports_rendering_the_same_partial_multiple_times
|
||||
t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}\n{% include 'a_template' %}", profile: true)
|
||||
t.render!
|
||||
|
||||
a_template1 = t.profiler[1]
|
||||
a_template1.children.each do |child|
|
||||
assert_equal "a_template", child.partial
|
||||
end
|
||||
|
||||
a_template2 = t.profiler[2]
|
||||
a_template2.children.each do |child|
|
||||
assert_equal "a_template", child.partial
|
||||
end
|
||||
end
|
||||
|
||||
def test_can_iterate_over_each_profiling_entry
|
||||
t = Template.parse("{{ 'a string' | upcase }}\n{% increment test %}", profile: true)
|
||||
t.render!
|
||||
|
||||
timing_count = 0
|
||||
t.profiler.each do |_timing|
|
||||
timing_count += 1
|
||||
end
|
||||
|
||||
assert_equal 2, timing_count
|
||||
end
|
||||
|
||||
def test_profiling_marks_children_of_if_blocks
|
||||
t = Template.parse("{% if true %} {% increment test %} {{ test }} {% endif %}", profile: true)
|
||||
t.render!
|
||||
|
||||
assert_equal 1, t.profiler.length
|
||||
assert_equal 2, t.profiler[0].children.length
|
||||
end
|
||||
|
||||
def test_profiling_marks_children_of_for_blocks
|
||||
t = Template.parse("{% for item in collection %} {{ item }} {% endfor %}", profile: true)
|
||||
t.render!("collection" => ["one", "two"])
|
||||
|
||||
assert_equal 1, t.profiler.length
|
||||
# Will profile each invocation of the for block
|
||||
assert_equal 2, t.profiler[0].children.length
|
||||
end
|
||||
end
|
||||
@@ -16,65 +16,72 @@ class SecurityTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_no_instance_eval
|
||||
text = %( {{ '1+1' | instance_eval }} )
|
||||
text = %( {{ '1+1' | instance_eval }} )
|
||||
expected = %( 1+1 )
|
||||
|
||||
assert_equal expected, Template.parse(text).render!(@assigns)
|
||||
assert_equal(expected, Template.parse(text).render!(@assigns))
|
||||
end
|
||||
|
||||
def test_no_existing_instance_eval
|
||||
text = %( {{ '1+1' | __instance_eval__ }} )
|
||||
text = %( {{ '1+1' | __instance_eval__ }} )
|
||||
expected = %( 1+1 )
|
||||
|
||||
assert_equal expected, Template.parse(text).render!(@assigns)
|
||||
assert_equal(expected, Template.parse(text).render!(@assigns))
|
||||
end
|
||||
|
||||
def test_no_instance_eval_after_mixing_in_new_filter
|
||||
text = %( {{ '1+1' | instance_eval }} )
|
||||
text = %( {{ '1+1' | instance_eval }} )
|
||||
expected = %( 1+1 )
|
||||
|
||||
assert_equal expected, Template.parse(text).render!(@assigns)
|
||||
assert_equal(expected, Template.parse(text).render!(@assigns))
|
||||
end
|
||||
|
||||
def test_no_instance_eval_later_in_chain
|
||||
text = %( {{ '1+1' | add_one | instance_eval }} )
|
||||
text = %( {{ '1+1' | add_one | instance_eval }} )
|
||||
expected = %( 1+1 + 1 )
|
||||
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: SecurityFilter)
|
||||
assert_equal(expected, Template.parse(text).render!(@assigns, filters: SecurityFilter))
|
||||
end
|
||||
|
||||
def test_does_not_add_filters_to_symbol_table
|
||||
def test_does_not_permanently_add_filters_to_symbol_table
|
||||
current_symbols = Symbol.all_symbols
|
||||
|
||||
test = %( {{ "some_string" | a_bad_filter }} )
|
||||
# MRI imprecisely marks objects found on the C stack, which can result
|
||||
# in uninitialized memory being marked. This can even result in the test failing
|
||||
# deterministically for a given compilation of ruby. Using a separate thread will
|
||||
# keep these writes of the symbol pointer on a separate stack that will be garbage
|
||||
# collected after Thread#join.
|
||||
Thread.new do
|
||||
test = %( {{ "some_string" | a_bad_filter }} )
|
||||
Template.parse(test).render!
|
||||
nil
|
||||
end.join
|
||||
|
||||
template = Template.parse(test)
|
||||
assert_equal [], (Symbol.all_symbols - current_symbols)
|
||||
GC.start
|
||||
|
||||
template.render!
|
||||
assert_equal [], (Symbol.all_symbols - current_symbols)
|
||||
assert_equal([], (Symbol.all_symbols - current_symbols))
|
||||
end
|
||||
|
||||
def test_does_not_add_drop_methods_to_symbol_table
|
||||
current_symbols = Symbol.all_symbols
|
||||
|
||||
assigns = { 'drop' => Drop.new }
|
||||
assert_equal "", Template.parse("{{ drop.custom_method_1 }}", assigns).render!
|
||||
assert_equal "", Template.parse("{{ drop.custom_method_2 }}", assigns).render!
|
||||
assert_equal "", Template.parse("{{ drop.custom_method_3 }}", assigns).render!
|
||||
assert_equal("", Template.parse("{{ drop.custom_method_1 }}", assigns).render!)
|
||||
assert_equal("", Template.parse("{{ drop.custom_method_2 }}", assigns).render!)
|
||||
assert_equal("", Template.parse("{{ drop.custom_method_3 }}", assigns).render!)
|
||||
|
||||
assert_equal [], (Symbol.all_symbols - current_symbols)
|
||||
assert_equal([], (Symbol.all_symbols - current_symbols))
|
||||
end
|
||||
|
||||
def test_max_depth_nested_blocks_does_not_raise_exception
|
||||
depth = Liquid::Block::MAX_DEPTH
|
||||
code = "{% if true %}" * depth + "rendered" + "{% endif %}" * depth
|
||||
assert_equal "rendered", Template.parse(code).render!
|
||||
code = "{% if true %}" * depth + "rendered" + "{% endif %}" * depth
|
||||
assert_equal("rendered", Template.parse(code).render!)
|
||||
end
|
||||
|
||||
def test_more_than_max_depth_nested_blocks_raises_exception
|
||||
depth = Liquid::Block::MAX_DEPTH + 1
|
||||
code = "{% if true %}" * depth + "rendered" + "{% endif %}" * depth
|
||||
code = "{% if true %}" * depth + "rendered" + "{% endif %}" * depth
|
||||
assert_raises(Liquid::StackLevelError) do
|
||||
Template.parse(code).render!
|
||||
end
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,59 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class TagDisableableTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
module RenderTagName
|
||||
def render(_context)
|
||||
tag_name
|
||||
end
|
||||
end
|
||||
|
||||
class Custom < Tag
|
||||
prepend Liquid::Tag::Disableable
|
||||
include RenderTagName
|
||||
end
|
||||
|
||||
class Custom2 < Tag
|
||||
prepend Liquid::Tag::Disableable
|
||||
include RenderTagName
|
||||
end
|
||||
|
||||
class DisableCustom < Block
|
||||
disable_tags "custom"
|
||||
end
|
||||
|
||||
class DisableBoth < Block
|
||||
disable_tags "custom", "custom2"
|
||||
end
|
||||
|
||||
def test_block_tag_disabling_nested_tag
|
||||
with_disableable_tags do
|
||||
with_custom_tag('disable', DisableCustom) do
|
||||
output = Template.parse('{% disable %}{% custom %};{% custom2 %}{% enddisable %}').render
|
||||
assert_equal('Liquid error: custom usage is not allowed in this context;custom2', output)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_block_tag_disabling_multiple_nested_tags
|
||||
with_disableable_tags do
|
||||
with_custom_tag('disable', DisableBoth) do
|
||||
output = Template.parse('{% disable %}{% custom %};{% custom2 %}{% enddisable %}').render
|
||||
assert_equal('Liquid error: custom usage is not allowed in this context;Liquid error: custom2 usage is not allowed in this context', output)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def with_disableable_tags
|
||||
with_custom_tag('custom', Custom) do
|
||||
with_custom_tag('custom2', Custom2) do
|
||||
yield
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,45 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class TagTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def test_custom_tags_have_a_default_render_to_output_buffer_method_for_backwards_compatibility
|
||||
klass1 = Class.new(Tag) do
|
||||
def render(*)
|
||||
'hello'
|
||||
end
|
||||
end
|
||||
|
||||
with_custom_tag('blabla', klass1) do
|
||||
template = Liquid::Template.parse("{% blabla %}")
|
||||
|
||||
assert_equal('hello', template.render)
|
||||
|
||||
buf = +''
|
||||
output = template.render({}, output: buf)
|
||||
assert_equal('hello', output)
|
||||
assert_equal('hello', buf)
|
||||
assert_equal(buf.object_id, output.object_id)
|
||||
end
|
||||
|
||||
klass2 = Class.new(klass1) do
|
||||
def render(*)
|
||||
'foo' + super + 'bar'
|
||||
end
|
||||
end
|
||||
|
||||
with_custom_tag('blabla', klass2) do
|
||||
template = Liquid::Template.parse("{% blabla %}")
|
||||
|
||||
assert_equal('foohellobar', template.render)
|
||||
|
||||
buf = +''
|
||||
output = template.render({}, output: buf)
|
||||
assert_equal('foohellobar', output)
|
||||
assert_equal('foohellobar', buf)
|
||||
assert_equal(buf.object_id, output.object_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -8,9 +8,9 @@ class BreakTagTest < Minitest::Test
|
||||
# tests that no weird errors are raised if break is called outside of a
|
||||
# block
|
||||
def test_break_with_no_block
|
||||
assigns = { 'i' => 1 }
|
||||
markup = '{% break %}'
|
||||
expected = ''
|
||||
assigns = { 'i' => 1 }
|
||||
markup = 'before{% break %}after'
|
||||
expected = 'before'
|
||||
|
||||
assert_template_result(expected, markup, assigns)
|
||||
end
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user