mirror of
https://github.com/Shopify/liquid.git
synced 2026-09-13 07:50:43 -07:00
Compare commits
231
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba82f8d05e | ||
|
|
7c70b56002 | ||
|
|
8a71d29bfb | ||
|
|
f6d1b56b4e | ||
|
|
45ddc00710 | ||
|
|
c2b7b3df50 | ||
|
|
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 | ||
|
|
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 |
@@ -0,0 +1,39 @@
|
||||
name: Liquid
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
entry:
|
||||
- { ruby: 2.5, 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@v2
|
||||
- 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@v2
|
||||
- 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
+9
-2
@@ -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,10 +10,15 @@ Performance:
|
||||
Enabled: true
|
||||
|
||||
AllCops:
|
||||
TargetRubyVersion: 2.4
|
||||
TargetRubyVersion: 2.5
|
||||
NewCops: disable
|
||||
Exclude:
|
||||
- 'vendor/bundle/**/*'
|
||||
|
||||
Naming/MethodName:
|
||||
Exclude:
|
||||
- 'example/server/liquid_servlet.rb'
|
||||
|
||||
# Backport https://github.com/Shopify/ruby-style-guide/pull/258
|
||||
Layout/BeginEndAlignment:
|
||||
Enabled: true
|
||||
|
||||
+17
-8
@@ -1,6 +1,6 @@
|
||||
# This configuration was generated by
|
||||
# `rubocop --auto-gen-config`
|
||||
# on 2019-09-11 06:34:25 +1000 using RuboCop version 0.74.0.
|
||||
# on 2020-12-11 18:53:41 UTC using RuboCop version 1.6.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
|
||||
@@ -14,14 +14,26 @@ Lint/InheritException:
|
||||
Exclude:
|
||||
- 'lib/liquid/interrupts.rb'
|
||||
|
||||
# Offense count: 98
|
||||
# Offense count: 113
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
|
||||
# URISchemes: http, https
|
||||
Layout/LineLength:
|
||||
Max: 294
|
||||
Max: 260
|
||||
|
||||
# Offense count: 44
|
||||
# Offense count: 8
|
||||
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: 43
|
||||
Naming/ConstantName:
|
||||
Exclude:
|
||||
- 'lib/liquid.rb'
|
||||
@@ -32,7 +44,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'
|
||||
@@ -40,9 +51,7 @@ Naming/ConstantName:
|
||||
- 'performance/shopify/paginate.rb'
|
||||
- 'test/integration/tags/include_tag_test.rb'
|
||||
|
||||
# Offense count: 5
|
||||
# Offense count: 2
|
||||
Style/ClassVars:
|
||||
Exclude:
|
||||
- 'lib/liquid/condition.rb'
|
||||
- 'lib/liquid/strainer.rb'
|
||||
- 'lib/liquid/template.rb'
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
language: ruby
|
||||
cache: bundler
|
||||
|
||||
rvm:
|
||||
- 2.4
|
||||
- 2.5
|
||||
- 2.6
|
||||
- &latest_ruby 2.7
|
||||
- ruby-head
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- rvm: *latest_ruby
|
||||
script: bundle exec rake memory_profile:run
|
||||
name: Profiling Memory Usage
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -11,14 +11,20 @@ group :benchmark, :test do
|
||||
gem 'benchmark-ips'
|
||||
gem 'memory_profiler'
|
||||
gem 'terminal-table'
|
||||
gem 'unicode_plot'
|
||||
|
||||
install_if -> { RUBY_PLATFORM !~ /mingw|mswin|java/ && RUBY_ENGINE != 'truffleruby' } do
|
||||
gem 'stackprof'
|
||||
end
|
||||
end
|
||||
|
||||
group :development do
|
||||
gem 'pry-byebug'
|
||||
end
|
||||
|
||||
group :test do
|
||||
gem 'rubocop', '~> 0.78.0', require: false
|
||||
gem 'rubocop', '~> 1.4', require: false
|
||||
gem 'rubocop-shopify', '~> 1.0.7', require: false
|
||||
gem 'rubocop-performance', require: false
|
||||
|
||||
platform :mri, :truffleruby do
|
||||
|
||||
+70
-2
@@ -1,9 +1,77 @@
|
||||
# Liquid Change Log
|
||||
|
||||
### Unreleased
|
||||
## 5.3.0 2022-03-22
|
||||
|
||||
* Split Strainer class as a factory and a template (#1208) [Thierry Joyal]
|
||||
### 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
|
||||
|
||||
|
||||
@@ -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,7 +56,7 @@ 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:
|
||||
|
||||
@@ -9,11 +9,17 @@ task(default: [:test, :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
|
||||
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')
|
||||
task :warn_test do
|
||||
ENV['LIQUID_PARSER_MODE'] = 'warn'
|
||||
@@ -40,13 +46,28 @@ 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
|
||||
|
||||
if RUBY_ENGINE == 'ruby'
|
||||
Rake::Task['test_compile'].invoke
|
||||
end
|
||||
end
|
||||
|
||||
task :test_compile do
|
||||
ENV['LIQUID_COMPILE'] = '1'
|
||||
ENV['LIQUID_PARSER_MODE'] = 'lax'
|
||||
Rake::Task['integration_test'].reenable
|
||||
Rake::Task['integration_test'].invoke
|
||||
|
||||
ENV['LIQUID_PARSER_MODE'] = 'strict'
|
||||
Rake::Task['integration_test'].reenable
|
||||
Rake::Task['integration_test'].invoke
|
||||
end
|
||||
|
||||
task(gem: :build)
|
||||
@@ -71,6 +92,22 @@ namespace :benchmark do
|
||||
ruby "./performance/benchmark.rb lax"
|
||||
end
|
||||
|
||||
desc "Compare the render performance of compiled Liquid to standard Liquid and Liquid-C"
|
||||
task :compare_render do
|
||||
ENV.delete("LIQUID_C")
|
||||
ENV.delete("RENDER_ONLY")
|
||||
|
||||
ENV["LIQUID_COMPILE"] = "1"
|
||||
ruby "./performance/benchmark.rb strict"
|
||||
|
||||
ENV.delete("LIQUID_COMPILE")
|
||||
ruby "./performance/benchmark.rb strict"
|
||||
|
||||
ENV["LIQUID_C"] = "1"
|
||||
ruby "./performance/benchmark.rb strict"
|
||||
|
||||
end
|
||||
|
||||
desc "Run the liquid benchmark with strict parsing"
|
||||
task :strict do
|
||||
ruby "./performance/benchmark.rb strict"
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# https://github.com/evanphx/benchmark-ips
|
||||
require 'liquid'
|
||||
#require 'liquid/c'
|
||||
require 'benchmark/ips'
|
||||
|
||||
require_relative '../lib/liquid/compile'
|
||||
|
||||
require_relative 'shop_filter'
|
||||
require_relative 'money_filter'
|
||||
|
||||
# Each database table is a hash
|
||||
require_relative 'database'
|
||||
tables = Database.tables
|
||||
|
||||
Liquid::Template.register_filter(MoneyFilter)
|
||||
Liquid::Template.register_filter(ShopFilter)
|
||||
@template = Liquid::Template.parse(File.read("product.liquid"))
|
||||
|
||||
context = Liquid::Context.new([tables, {}], {}, {}, false, Liquid::ResourceLimits.new(Liquid::Template.default_resource_limits))
|
||||
|
||||
Benchmark.ips do |x|
|
||||
x.report("render") { @template.render(context) }
|
||||
|
||||
# Compare the iterations per second of the various reports
|
||||
x.compare!
|
||||
end
|
||||
@@ -0,0 +1,57 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'yaml'
|
||||
require 'stackprof'
|
||||
|
||||
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 =
|
||||
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, symbolize_names: true)
|
||||
else
|
||||
YAML.load_file(DATABASE_FILE_PATH, symbolize_names: true)
|
||||
end
|
||||
|
||||
# From vision source
|
||||
db[:products].each do |product|
|
||||
collections = db[:collections].find_all do |collection|
|
||||
collection[:products].any? { |p| p[:id].to_i == product[:id].to_i }
|
||||
end
|
||||
product[:collections] = collections
|
||||
end
|
||||
|
||||
# key the tables by handles, as this is how liquid expects it.
|
||||
db = db.each_with_object({}) do |(key, values), assigns|
|
||||
assigns[key] = values.each_with_object({}) do |v, h|
|
||||
h[v[:handle]] = v
|
||||
end
|
||||
end
|
||||
|
||||
# Some standard direct accessors so that the specialized templates
|
||||
# render correctly
|
||||
db[:collection] = db[:collections].values.first
|
||||
db[:product] = db[:products].values.first
|
||||
db[:blog] = db[:blogs].values.first
|
||||
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] },
|
||||
:items => db[:line_items].values,
|
||||
}
|
||||
|
||||
db
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if __FILE__ == $PROGRAM_NAME
|
||||
p(Database.tables[:collections][:frontpage].keys)
|
||||
# p Database.tables[:blog][:articles]
|
||||
end
|
||||
@@ -0,0 +1,19 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module MoneyFilter
|
||||
def money_with_currency(money)
|
||||
return '' if money.nil?
|
||||
format("$ %.2f USD", money / 100.0)
|
||||
end
|
||||
|
||||
def money(money)
|
||||
return '' if money.nil?
|
||||
format("$ %.2f", money / 100.0)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def currency
|
||||
ShopDrop.new.currency
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,36 @@
|
||||
<div id="product-left">
|
||||
{% for image in product.images %}{% if forloop.first %}<div id="product-image">
|
||||
<a href="{{ image | product_img_url: 'large' }}" rel="lightbox[images]" title="{{ product.title | escape }}"><img src="{{ image | product_img_url: 'medium' }}" alt="{{ product.title | escape }}" /></a>
|
||||
</div>{% else %}
|
||||
<div class="product-images">
|
||||
<a href="{{ image | product_img_url: 'large' }}" rel="lightbox[images]" title="{{ product.title | escape }}"><img src="{{ image | product_img_url: 'small' }}" alt="{{ product.title | escape }}" /></a>
|
||||
</div>{% endif %}{% endfor %}
|
||||
</div>
|
||||
<div id="product-right">
|
||||
<h1>{{ product.title }}</h1>
|
||||
{{ product.description }}
|
||||
|
||||
{% if product.available %}
|
||||
<form action="/cart/add" method="post">
|
||||
|
||||
<div id="product-variants">
|
||||
<div id="price-field"></div>
|
||||
|
||||
<select id="product-select" name='id'>
|
||||
{% for variant in product.variants %}
|
||||
<option value="{{ variant.id }}">{{ variant.title }} - {{ variant.price | money }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<input type="image" src="{{ 'purchase.png' | asset_url }}" name="add" value="Purchase" id="purchase" />
|
||||
</form>
|
||||
{% else %}
|
||||
<p class="bold-red">This product is temporarily unavailable</p>
|
||||
{% endif %}
|
||||
|
||||
<div id="product-details">
|
||||
<strong>Continue Shopping</strong><br />
|
||||
Browse more {{ product.type | link_to_type }} or additional {{ product.vendor | link_to_vendor }} products.
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,106 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ShopFilter
|
||||
def asset_url(input)
|
||||
"/files/1/[shop_id]/[shop_id]/assets/#{input}"
|
||||
end
|
||||
|
||||
def global_asset_url(input)
|
||||
"/global/#{input}"
|
||||
end
|
||||
|
||||
def shopify_asset_url(input)
|
||||
"/shopify/#{input}"
|
||||
end
|
||||
|
||||
def script_tag(url)
|
||||
%(<script src="#{url}" type="text/javascript"></script>)
|
||||
end
|
||||
|
||||
def stylesheet_tag(url, media = "all")
|
||||
%(<link href="#{url}" rel="stylesheet" type="text/css" media="#{media}" />)
|
||||
end
|
||||
|
||||
def link_to(link, url, title = "")
|
||||
%(<a href="#{url}" title="#{title}">#{link}</a>)
|
||||
end
|
||||
|
||||
def img_tag(url, alt = "")
|
||||
%(<img src="#{url}" alt="#{alt}" />)
|
||||
end
|
||||
|
||||
def link_to_vendor(vendor)
|
||||
if vendor
|
||||
link_to(vendor, url_for_vendor(vendor), vendor)
|
||||
else
|
||||
'Unknown Vendor'
|
||||
end
|
||||
end
|
||||
|
||||
def link_to_type(type)
|
||||
if type
|
||||
link_to(type, url_for_type(type), type)
|
||||
else
|
||||
'Unknown Vendor'
|
||||
end
|
||||
end
|
||||
|
||||
def url_for_vendor(vendor_title)
|
||||
"/collections/#{to_handle(vendor_title)}"
|
||||
end
|
||||
|
||||
def url_for_type(type_title)
|
||||
"/collections/#{to_handle(type_title)}"
|
||||
end
|
||||
|
||||
def product_img_url(url, style = 'small')
|
||||
unless url =~ %r{\Aproducts/([\w\-\_]+)\.(\w{2,4})}
|
||||
raise ArgumentError, 'filter "size" can only be called on product images'
|
||||
end
|
||||
|
||||
case style
|
||||
when 'original'
|
||||
'/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
|
||||
raise ArgumentError, 'valid parameters for filter "size" are: original, grande, large, medium, compact, small, thumb and icon '
|
||||
end
|
||||
end
|
||||
|
||||
def default_pagination(paginate)
|
||||
html = []
|
||||
html << %(<span class="prev">#{link_to(paginate['previous']['title'], paginate['previous']['url'])}</span>) if paginate['previous']
|
||||
|
||||
paginate['parts'].each do |part|
|
||||
html << if part['is_link']
|
||||
%(<span class="page">#{link_to(part['title'], part['url'])}</span>)
|
||||
elsif part['title'].to_i == paginate['current_page'].to_i
|
||||
%(<span class="page current">#{part['title']}</span>)
|
||||
else
|
||||
%(<span class="deco">#{part['title']}</span>)
|
||||
end
|
||||
end
|
||||
|
||||
html << %(<span class="next">#{link_to(paginate['next']['title'], paginate['next']['url'])}</span>) if paginate['next']
|
||||
html.join(' ')
|
||||
end
|
||||
|
||||
# Accepts a number, and two words - one for singular, one for plural
|
||||
# Returns the singular word if input equals 1, otherwise plural
|
||||
def pluralize(input, singular, plural)
|
||||
input == 1 ? singular : plural
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def to_handle(str)
|
||||
result = str.dup
|
||||
result.downcase!
|
||||
result.delete!("'\"()[]")
|
||||
result.gsub!(/\W+/, '-')
|
||||
result.gsub!(/-+\z/, '') if result[-1] == '-'
|
||||
result.gsub!(/\A-+/, '') if result[0] == '-'
|
||||
result
|
||||
end
|
||||
end
|
||||
Binary file not shown.
@@ -0,0 +1,945 @@
|
||||
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
|
||||
# Variants
|
||||
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
|
||||
|
||||
product_variants:
|
||||
- &product-1-var-1
|
||||
id: 1
|
||||
title: 151cm / Normal
|
||||
price: 19900
|
||||
weight: 1000
|
||||
compare_at_price: 49900
|
||||
available: true
|
||||
inventory_quantity: 5
|
||||
option1: 151cm
|
||||
option2: Normal
|
||||
option3:
|
||||
- &product-1-var-2
|
||||
id: 2
|
||||
title: 155cm / Normal
|
||||
price: 31900
|
||||
weight: 1000
|
||||
compare_at_price: 50900
|
||||
available: true
|
||||
inventory_quantity: 2
|
||||
option1: 155cm
|
||||
option2: Normal
|
||||
option3:
|
||||
- &product-2-var-1
|
||||
id: 3
|
||||
title: 162cm
|
||||
price: 29900
|
||||
weight: 1000
|
||||
compare_at_price: 52900
|
||||
available: true
|
||||
inventory_quantity: 3
|
||||
option1: 162cm
|
||||
option2:
|
||||
option3:
|
||||
- &product-3-var-1
|
||||
id: 4
|
||||
title: 159cm
|
||||
price: 19900
|
||||
weight: 1000
|
||||
compare_at_price:
|
||||
available: true
|
||||
inventory_quantity: 4
|
||||
option1: 159cm
|
||||
option2:
|
||||
option3:
|
||||
- &product-4-var-1
|
||||
id: 5
|
||||
title: 159cm
|
||||
price: 19900
|
||||
weight: 1000
|
||||
compare_at_price: 32900
|
||||
available: true
|
||||
inventory_quantity: 6
|
||||
option1: 159cm
|
||||
option2:
|
||||
option3:
|
||||
- &product-1-var-3
|
||||
id: 6
|
||||
title: 158cm / Wide
|
||||
price: 23900
|
||||
weight: 1000
|
||||
compare_at_price: 99900
|
||||
available: false
|
||||
inventory_quantity: 0
|
||||
option1: 158cm
|
||||
option2: Wide
|
||||
option3:
|
||||
- &product-3-var-2
|
||||
id: 7
|
||||
title: 162cm
|
||||
price: 19900
|
||||
weight: 1000
|
||||
compare_at_price:
|
||||
available: false
|
||||
inventory_quantity: 0
|
||||
option1: 162cm
|
||||
option2:
|
||||
option3:
|
||||
- &product-3-var-3
|
||||
id: 8
|
||||
title: 165cm
|
||||
price: 22900
|
||||
weight: 1000
|
||||
compare_at_price:
|
||||
available: true
|
||||
inventory_quantity: 4
|
||||
option1: 165cm
|
||||
option2:
|
||||
option3:
|
||||
- &product-5-var-1
|
||||
id: 9
|
||||
title: black / 42
|
||||
price: 11900
|
||||
weight: 500
|
||||
compare_at_price: 22900
|
||||
available: true
|
||||
inventory_quantity: 1
|
||||
option1: black
|
||||
option2: 42
|
||||
option3:
|
||||
- &product-5-var-2
|
||||
id: 10
|
||||
title: beige / 42
|
||||
price: 11900
|
||||
weight: 500
|
||||
compare_at_price: 22900
|
||||
available: true
|
||||
inventory_quantity: 3
|
||||
option1: beige
|
||||
option2: 42
|
||||
option3:
|
||||
- &product-5-var-3
|
||||
id: 11
|
||||
title: white / 42
|
||||
price: 13900
|
||||
weight: 500
|
||||
compare_at_price: 24900
|
||||
available: true
|
||||
inventory_quantity: 1
|
||||
option1: white
|
||||
option2: 42
|
||||
option3:
|
||||
- &product-5-var-4
|
||||
id: 12
|
||||
title: black / 44
|
||||
price: 11900
|
||||
weight: 500
|
||||
compare_at_price: 22900
|
||||
available: true
|
||||
inventory_quantity: 2
|
||||
option1: black
|
||||
option2: 44
|
||||
option3:
|
||||
- &product-5-var-5
|
||||
id: 13
|
||||
title: beige / 44
|
||||
price: 11900
|
||||
weight: 500
|
||||
compare_at_price: 22900
|
||||
available: false
|
||||
inventory_quantity: 0
|
||||
option1: beige
|
||||
option2: 44
|
||||
option3:
|
||||
- &product-5-var-6
|
||||
id: 14
|
||||
title: white / 44
|
||||
price: 13900
|
||||
weight: 500
|
||||
compare_at_price: 24900
|
||||
available: false
|
||||
inventory_quantity: 0
|
||||
option1: white
|
||||
option2: 44
|
||||
option3:
|
||||
- &product-6-var-1
|
||||
id: 15
|
||||
title: red
|
||||
price: 2179500
|
||||
weight: 200000
|
||||
compare_at_price:
|
||||
available: true
|
||||
inventory_quantity: 0
|
||||
option1: red
|
||||
option2:
|
||||
option3:
|
||||
- &product-7-var-1
|
||||
id: 16
|
||||
title: black / small
|
||||
price: 1900
|
||||
weight: 200
|
||||
compare_at_price:
|
||||
available: true
|
||||
inventory_quantity: 20
|
||||
option1: black
|
||||
option2: small
|
||||
option3:
|
||||
- &product-7-var-2
|
||||
id: 17
|
||||
title: black / medium
|
||||
price: 1900
|
||||
weight: 200
|
||||
compare_at_price:
|
||||
available: false
|
||||
inventory_quantity: 0
|
||||
option1: black
|
||||
option2: medium
|
||||
option3:
|
||||
- &product-7-var-3
|
||||
id: 18
|
||||
title: black / large
|
||||
price: 1900
|
||||
weight: 200
|
||||
compare_at_price:
|
||||
available: true
|
||||
inventory_quantity: 10
|
||||
option1: black
|
||||
option2: large
|
||||
option3:
|
||||
- &product-7-var-4
|
||||
id: 19
|
||||
title: black / extra large
|
||||
price: 1900
|
||||
weight: 200
|
||||
compare_at_price:
|
||||
available: false
|
||||
inventory_quantity: 0
|
||||
option1: black
|
||||
option2: extra large
|
||||
option3:
|
||||
- &product-8-var-1
|
||||
id: 20
|
||||
title: brown / small
|
||||
price: 5900
|
||||
weight: 400
|
||||
compare_at_price: 6900
|
||||
available: true
|
||||
inventory_quantity: 5
|
||||
option1: brown
|
||||
option2: small
|
||||
option3:
|
||||
- &product-8-var-2
|
||||
id: 21
|
||||
title: brown / medium
|
||||
price: 5900
|
||||
weight: 400
|
||||
compare_at_price: 6900
|
||||
available: false
|
||||
inventory_quantity: 0
|
||||
option1: brown
|
||||
option2: medium
|
||||
option3:
|
||||
- &product-8-var-3
|
||||
id: 22
|
||||
title: brown / large
|
||||
price: 5900
|
||||
weight: 400
|
||||
compare_at_price: 6900
|
||||
available: true
|
||||
inventory_quantity: 10
|
||||
option1: brown
|
||||
option2: large
|
||||
option3:
|
||||
- &product-8-var-4
|
||||
id: 23
|
||||
title: black / small
|
||||
price: 5900
|
||||
weight: 400
|
||||
compare_at_price: 6900
|
||||
available: true
|
||||
inventory_quantity: 10
|
||||
option1: black
|
||||
option2: small
|
||||
option3:
|
||||
- &product-8-var-5
|
||||
id: 24
|
||||
title: black / medium
|
||||
price: 5900
|
||||
weight: 400
|
||||
compare_at_price: 6900
|
||||
available: true
|
||||
inventory_quantity: 10
|
||||
option1: black
|
||||
option2: medium
|
||||
option3:
|
||||
- &product-8-var-6
|
||||
id: 25
|
||||
title: black / large
|
||||
price: 5900
|
||||
weight: 400
|
||||
compare_at_price: 6900
|
||||
available: false
|
||||
inventory_quantity: 0
|
||||
option1: black
|
||||
option2: large
|
||||
option3:
|
||||
- &product-9-var-1
|
||||
id: 26
|
||||
title: Body Only
|
||||
price: 499995
|
||||
weight: 2000
|
||||
compare_at_price:
|
||||
available: true
|
||||
inventory_quantity: 3
|
||||
option1: Body Only
|
||||
option2:
|
||||
option3:
|
||||
- &product-9-var-2
|
||||
id: 27
|
||||
title: Kit with 18-55mm VR lens
|
||||
price: 523995
|
||||
weight: 2000
|
||||
compare_at_price:
|
||||
available: true
|
||||
inventory_quantity: 2
|
||||
option1: Kit with 18-55mm VR lens
|
||||
option2:
|
||||
option3:
|
||||
- &product-9-var-3
|
||||
id: 28
|
||||
title: Kit with 18-200 VR lens
|
||||
price: 552500
|
||||
weight: 2000
|
||||
compare_at_price:
|
||||
available: true
|
||||
inventory_quantity: 3
|
||||
option1: Kit with 18-200 VR lens
|
||||
option2:
|
||||
option3:
|
||||
|
||||
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
|
||||
# Products
|
||||
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
|
||||
|
||||
products:
|
||||
- &product-1
|
||||
id: 1
|
||||
title: Arbor Draft
|
||||
handle: arbor-draft
|
||||
type: Snowboards
|
||||
vendor: Arbor
|
||||
price: 23900
|
||||
price_max: 31900
|
||||
price_min: 23900
|
||||
price_varies: true
|
||||
available: true
|
||||
tags:
|
||||
- season2005
|
||||
- pro
|
||||
- intermediate
|
||||
- wooden
|
||||
- freestyle
|
||||
options:
|
||||
- Length
|
||||
- Style
|
||||
compare_at_price: 49900
|
||||
compare_at_price_max: 50900
|
||||
compare_at_price_min: 49900
|
||||
compare_at_price_varies: true
|
||||
url: /products/arbor-draft
|
||||
featured_image: products/arbor_draft.jpg
|
||||
images:
|
||||
- products/arbor_draft.jpg
|
||||
description:
|
||||
The Arbor Draft snowboard wouldn't exist if Polynesians hadn't figured out how to surf hundreds of years ago. But the Draft does exist, and it's here to bring your urban and park riding to a new level. The board's freaky Tiki design pays homage to culture that inspired snowboarding. It's designed to spin with ease, land smoothly, lock hook-free onto rails, and take the abuse of a pavement pounding or twelve. The Draft will pop off kickers with authority and carve solidly across the pipe. The Draft features targeted Koa wood die cuts inlayed into the deck that enhance the flex pattern. Now bow down to riding's ancestors.
|
||||
variants:
|
||||
- *product-1-var-1
|
||||
- *product-1-var-2
|
||||
- *product-1-var-3
|
||||
- &product-2
|
||||
id: 2
|
||||
title: Arbor Element
|
||||
handle: arbor-element
|
||||
type: Snowboards
|
||||
vendor: Arbor
|
||||
price: 29900
|
||||
price_max: 29900
|
||||
price_min: 29900
|
||||
price_varies: false
|
||||
available: true
|
||||
tags:
|
||||
- season2005
|
||||
- pro
|
||||
- wooden
|
||||
- freestyle
|
||||
options:
|
||||
- Length
|
||||
compare_at_price: 52900
|
||||
compare_at_price_max: 52900
|
||||
compare_at_price_min: 52900
|
||||
compare_at_price_varies: false
|
||||
url: /products/arbor-element
|
||||
featured_image: products/element58.jpg
|
||||
images:
|
||||
- products/element58.jpg
|
||||
description:
|
||||
The Element is a technically advanced all-mountain board for riders who readily transition from one terrain, snow condition, or riding style to another. Its balanced design provides the versatility needed for the true ride-it-all experience. The Element is exceedingly lively, freely initiates, and holds a tight edge at speed. Its structural real-wood topsheet is made with book-matched Koa.
|
||||
variants:
|
||||
- *product-2-var-1
|
||||
|
||||
- &product-3
|
||||
id: 3
|
||||
title: Comic ~ Pastel
|
||||
handle: comic-pastel
|
||||
type: Snowboards
|
||||
vendor: Technine
|
||||
price: 19900
|
||||
price_max: 22900
|
||||
price_min: 19900
|
||||
tags:
|
||||
- season2006
|
||||
- beginner
|
||||
- intermediate
|
||||
- freestyle
|
||||
- purple
|
||||
options:
|
||||
- Length
|
||||
price_varies: true
|
||||
available: true
|
||||
compare_at_price:
|
||||
compare_at_price_max: 0
|
||||
compare_at_price_min: 0
|
||||
compare_at_price_varies: false
|
||||
url: /products/comic-pastel
|
||||
featured_image: products/technine1.jpg
|
||||
images:
|
||||
- products/technine1.jpg
|
||||
- products/technine2.jpg
|
||||
- products/technine_detail.jpg
|
||||
description:
|
||||
2005 Technine Comic Series Description The Comic series was developed to be the ultimate progressive freestyle board in the Technine line. Dependable edge control and a perfect flex pattern for jumping in the park or out of bounds. Landins and progression will come easy with this board and it will help your riding progress to the next level. Street rails, park jibs, backcountry booters and park jumps, this board will do it all.
|
||||
variants:
|
||||
- *product-3-var-1
|
||||
- *product-3-var-2
|
||||
- *product-3-var-3
|
||||
|
||||
- &product-4
|
||||
id: 4
|
||||
title: Comic ~ Orange
|
||||
handle: comic-orange
|
||||
type: Snowboards
|
||||
vendor: Technine
|
||||
price: 19900
|
||||
price_max: 19900
|
||||
price_min: 19900
|
||||
price_varies: false
|
||||
available: true
|
||||
tags:
|
||||
- season2006
|
||||
- beginner
|
||||
- intermediate
|
||||
- freestyle
|
||||
- orange
|
||||
options:
|
||||
- Length
|
||||
compare_at_price: 32900
|
||||
compare_at_price_max: 32900
|
||||
compare_at_price_min: 32900
|
||||
compare_at_price_varies: false
|
||||
url: /products/comic-orange
|
||||
featured_image: products/technine3.jpg
|
||||
images:
|
||||
- products/technine3.jpg
|
||||
- products/technine4.jpg
|
||||
description:
|
||||
2005 Technine Comic Series Description The Comic series was developed to be the ultimate progressive freestyle board in the Technine line. Dependable edge control and a perfect flex pattern for jumping in the park or out of bounds. Landins and progression will come easy with this board and it will help your riding progress to the next level. Street rails, park jibs, backcountry booters and park jumps, this board will do it all.
|
||||
variants:
|
||||
- *product-4-var-1
|
||||
|
||||
- &product-5
|
||||
id: 5
|
||||
title: Burton Boots
|
||||
handle: burton-boots
|
||||
type: Boots
|
||||
vendor: Burton
|
||||
price: 11900
|
||||
price_max: 11900
|
||||
price_min: 11900
|
||||
price_varies: false
|
||||
available: true
|
||||
tags:
|
||||
- season2006
|
||||
- beginner
|
||||
- intermediate
|
||||
- boots
|
||||
options:
|
||||
- Color
|
||||
- Shoe Size
|
||||
compare_at_price: 22900
|
||||
compare_at_price_max: 22900
|
||||
compare_at_price_min: 22900
|
||||
compare_at_price_varies: false
|
||||
url: /products/burton-boots
|
||||
featured_image: products/burton.jpg
|
||||
images:
|
||||
- products/burton.jpg
|
||||
description:
|
||||
The Burton boots are particularly well on snowboards. The very best thing about them is that the according picture is cubic. This makes testing in a Vision testing environment very easy.
|
||||
variants:
|
||||
- *product-5-var-1
|
||||
- *product-5-var-2
|
||||
- *product-5-var-3
|
||||
- *product-5-var-4
|
||||
- *product-5-var-5
|
||||
- *product-5-var-6
|
||||
|
||||
- &product-6
|
||||
id: 6
|
||||
title: Superbike 1198 S
|
||||
handle: superbike
|
||||
type: Superbike
|
||||
vendor: Ducati
|
||||
price: 2179500
|
||||
price_max: 2179500
|
||||
price_min: 2179500
|
||||
price_varies: false
|
||||
available: true
|
||||
tags:
|
||||
- ducati
|
||||
- superbike
|
||||
- bike
|
||||
- street
|
||||
- racing
|
||||
- performance
|
||||
options:
|
||||
- Color
|
||||
compare_at_price:
|
||||
compare_at_price_max: 0
|
||||
compare_at_price_min: 0
|
||||
compare_at_price_varies: false
|
||||
url: /products/superbike
|
||||
featured_image: products/ducati.jpg
|
||||
images:
|
||||
- products/ducati.jpg
|
||||
description:
|
||||
<h3>‘S’ PERFORMANCE</h3>
|
||||
<p>Producing 170hp (125kW) and with a dry weight of just 169kg (372.6lb), the new 1198 S now incorporates more World Superbike technology than ever before by taking the 1198 motor and adding top-of-the-range suspension, lightweight chassis components and a true racing-style traction control system designed for road use.</p>
|
||||
<p>The high performance, fully adjustable 43mm Öhlins forks, which sport low friction titanium nitride-treated fork sliders, respond effortlessly to every imperfection in the tarmac. Beyond their advanced engineering solutions, one of the most important characteristics of Öhlins forks is their ability to communicate the condition and quality of the tyre-to-road contact patch, a feature that puts every rider in superior control. The suspension set-up at the rear is complemented with a fully adjustable Öhlins rear shock equipped with a ride enhancing top-out spring and mounted to a single-sided swingarm for outstanding drive and traction. The front-to-rear Öhlins package is completed with a control-enhancing adjustable steering damper.</p>
|
||||
variants:
|
||||
- *product-6-var-1
|
||||
|
||||
- &product-7
|
||||
id: 7
|
||||
title: Shopify Shirt
|
||||
handle: shopify-shirt
|
||||
type: Shirt
|
||||
vendor: Shopify
|
||||
price: 1900
|
||||
price_max: 1900
|
||||
price_min: 1900
|
||||
price_varies: false
|
||||
available: true
|
||||
tags:
|
||||
- shopify
|
||||
- shirt
|
||||
- apparel
|
||||
- tshirt
|
||||
- clothing
|
||||
options:
|
||||
- Color
|
||||
- Size
|
||||
compare_at_price:
|
||||
compare_at_price_max: 0
|
||||
compare_at_price_min: 0
|
||||
compare_at_price_varies: false
|
||||
url: /products/shopify-shirt
|
||||
featured_image: products/shopify_shirt.png
|
||||
images:
|
||||
- products/shopify_shirt.png
|
||||
description:
|
||||
<p>High Quality Shopify Shirt. Wear your e-commerce solution with pride and attract attention anywhere you go.</p>
|
||||
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
|
||||
variants:
|
||||
- *product-7-var-1
|
||||
- *product-7-var-2
|
||||
- *product-7-var-3
|
||||
- *product-7-var-4
|
||||
|
||||
- &product-8
|
||||
id: 8
|
||||
title: Hooded Sweater
|
||||
handle: hooded-sweater
|
||||
type: Sweater
|
||||
vendor: Stormtech
|
||||
price: 5900
|
||||
price_max: 5900
|
||||
price_min: 5900
|
||||
price_varies: false
|
||||
available: true
|
||||
tags:
|
||||
- sweater
|
||||
- hooded
|
||||
- apparel
|
||||
- clothing
|
||||
options:
|
||||
- Color
|
||||
- Size
|
||||
compare_at_price: 6900
|
||||
compare_at_price_max: 6900
|
||||
compare_at_price_min: 6900
|
||||
compare_at_price_varies: false
|
||||
url: /products/hooded-sweater
|
||||
featured_image: products/hooded-sweater.jpg
|
||||
images:
|
||||
- products/hooded-sweater.jpg
|
||||
- products/hooded-sweater-b.jpg
|
||||
description:
|
||||
<p>Extra comfortable zip up sweater. Durable quality, ideal for any outdoor activities.</p>
|
||||
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
|
||||
variants:
|
||||
- *product-8-var-1
|
||||
- *product-8-var-2
|
||||
- *product-8-var-3
|
||||
- *product-8-var-4
|
||||
- *product-8-var-5
|
||||
- *product-8-var-6
|
||||
|
||||
- &product-9
|
||||
id: 9
|
||||
title: D3 Digital SLR Camera
|
||||
handle: d3
|
||||
type: SLR
|
||||
vendor: Nikon
|
||||
price: 499995
|
||||
price_max: 552500
|
||||
price_min: 499995
|
||||
price_varies: true
|
||||
available: true
|
||||
tags:
|
||||
- camera
|
||||
- slr
|
||||
- nikon
|
||||
- professional
|
||||
options:
|
||||
- Bundle
|
||||
compare_at_price:
|
||||
compare_at_price_max: 0
|
||||
compare_at_price_min: 0
|
||||
compare_at_price_varies: false
|
||||
url: /products/d3
|
||||
featured_image: products/d3.jpg
|
||||
images:
|
||||
- products/d3.jpg
|
||||
- products/d3_2.jpg
|
||||
- products/d3_3.jpg
|
||||
description:
|
||||
<p>Flagship pro D-SLR with a 12.1-MP FX-format CMOS sensor, blazing 9 fps shooting at full FX resolution and low-noise performance up to 6400 ISO.</p>
|
||||
<p><strong>Nikon's original 12.1-megapixel FX-format (23.9 x 36mm) CMOS sensor:</strong> Couple Nikon's exclusive digital image processing system with the 12.1-megapixel FX-format and you'll get breathtakingly rich images while also reducing noise to unprecedented levels with even higher ISOs.</p>
|
||||
<p><strong>Continuous shooting at up to 9 frames per second:</strong> At full FX resolution and up to 11fps in the DX crop mode, the D3 offers uncompromised shooting speeds for fast-action and sports photography.</p>
|
||||
variants:
|
||||
- *product-9-var-1
|
||||
- *product-9-var-2
|
||||
- *product-9-var-3
|
||||
|
||||
|
||||
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
|
||||
# Line Items
|
||||
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
|
||||
|
||||
line_items:
|
||||
- &line_item-1
|
||||
id: 1
|
||||
title: 'Arbor Draft'
|
||||
subtitle: '151cm'
|
||||
price: 29900
|
||||
line_price: 29900
|
||||
quantity: 1
|
||||
variant: *product-1-var-1
|
||||
product: *product-1
|
||||
|
||||
- &line_item-2
|
||||
id: 2
|
||||
title: 'Comic ~ Orange'
|
||||
subtitle: '159cm'
|
||||
price: 19900
|
||||
line_price: 39800
|
||||
quantity: 2
|
||||
variant: *product-4-var-1
|
||||
product: *product-4
|
||||
|
||||
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
|
||||
# Link Lists
|
||||
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
|
||||
links:
|
||||
- &link-1
|
||||
id: 1
|
||||
title: Our Sale
|
||||
url: /collections/sale
|
||||
- &link-2
|
||||
id: 2
|
||||
title: Arbor Stuff
|
||||
url: /collections/arbor
|
||||
- &link-3
|
||||
id: 3
|
||||
title: All our Snowboards
|
||||
url: /collections/snowboards
|
||||
- &link-4
|
||||
id: 4
|
||||
title: Powered by Shopify
|
||||
url: 'http://shopify.com'
|
||||
- &link-5
|
||||
id: 5
|
||||
title: About Us
|
||||
url: /pages/about-us
|
||||
- &link-6
|
||||
id: 6
|
||||
title: Policies
|
||||
url: /pages/shipping
|
||||
- &link-7
|
||||
id: 7
|
||||
title: Contact Us
|
||||
url: /pages/contact
|
||||
- &link-8
|
||||
id: 8
|
||||
title: Our blog
|
||||
url: /blogs/bigcheese-blog
|
||||
- &link-9
|
||||
id: 9
|
||||
title: New Boots
|
||||
url: /products/burton-boots
|
||||
- &link-10
|
||||
id: 10
|
||||
title: Paginated Sale
|
||||
url: /collections/paginated-sale
|
||||
- &link-11
|
||||
id: 11
|
||||
title: Our Paginated blog
|
||||
url: /blogs/paginated-blog
|
||||
- &link-12
|
||||
id: 12
|
||||
title: Catalog
|
||||
url: /collections/all
|
||||
|
||||
|
||||
|
||||
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
|
||||
# Link Lists
|
||||
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
|
||||
|
||||
link_lists:
|
||||
- &link-list-1
|
||||
id: 1
|
||||
title: 'Main Menu'
|
||||
handle: 'main-menu'
|
||||
links:
|
||||
- *link-12
|
||||
- *link-5
|
||||
- *link-7
|
||||
- *link-8
|
||||
- &link-list-2
|
||||
id: 1
|
||||
title: 'Footer Menu'
|
||||
handle: 'footer'
|
||||
links:
|
||||
- *link-5
|
||||
- *link-6
|
||||
|
||||
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
|
||||
# Collections
|
||||
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
|
||||
|
||||
collections:
|
||||
- &collection-1
|
||||
id: 1
|
||||
title: Frontpage
|
||||
handle: frontpage
|
||||
url: /collections/frontpage
|
||||
products:
|
||||
- *product-7
|
||||
- *product-8
|
||||
- *product-9
|
||||
|
||||
- &collection-2
|
||||
id: 2
|
||||
title: Arbor
|
||||
handle: arbor
|
||||
url: /collections/arbor
|
||||
products:
|
||||
- *product-1
|
||||
- *product-2
|
||||
|
||||
- &collection-3
|
||||
id: 3
|
||||
title: Snowboards
|
||||
handle: snowboards
|
||||
url: /collections/snowboards
|
||||
description:
|
||||
<p>This is a description for my <strong>Snowboards</strong> collection.</p>
|
||||
products:
|
||||
- *product-1
|
||||
- *product-2
|
||||
- *product-3
|
||||
- *product-4
|
||||
|
||||
- &collection-4
|
||||
id: 4
|
||||
title: Items On Sale
|
||||
handle: sale
|
||||
url: /collections/sale
|
||||
products:
|
||||
- *product-1
|
||||
|
||||
- &collection-5
|
||||
id: 5
|
||||
title: Paginated Sale
|
||||
handle: 'paginated-sale'
|
||||
url: '/collections/paginated-sale'
|
||||
products:
|
||||
- *product-1
|
||||
- *product-2
|
||||
- *product-3
|
||||
- *product-4
|
||||
products_count: 210
|
||||
|
||||
- &collection-6
|
||||
id: 6
|
||||
title: All products
|
||||
handle: 'all'
|
||||
url: '/collections/all'
|
||||
products:
|
||||
- *product-7
|
||||
- *product-8
|
||||
- *product-9
|
||||
- *product-6
|
||||
- *product-1
|
||||
- *product-2
|
||||
- *product-3
|
||||
- *product-4
|
||||
- *product-5
|
||||
|
||||
|
||||
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
|
||||
# Pages and Blogs
|
||||
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
|
||||
pages:
|
||||
- &page-2
|
||||
id: 1
|
||||
title: Contact Us
|
||||
handle: contact
|
||||
url: /pages/contact
|
||||
author: Tobi
|
||||
content:
|
||||
"<p>You can contact us via phone under (555) 567-2222.</p>
|
||||
<p>Our retail store is located at <em>Rue d'Avignon 32, Avignon (Provence)</em>.</p>
|
||||
<p><strong>Opening Hours:</strong><br />Monday through Friday: 9am - 6pm<br />Saturday: 10am - 3pm<br />Sunday: closed</p>"
|
||||
created_at: 2005-04-04 12:00
|
||||
|
||||
- &page-3
|
||||
id: 2
|
||||
title: About Us
|
||||
handle: about-us
|
||||
url: /pages/about-us
|
||||
author: Tobi
|
||||
content:
|
||||
"<p>Our company was founded in 1894 and we are since operating out of Avignon from the beautiful Provence.</p>
|
||||
<p>We offer the highest quality products and are proud to serve our customers to their heart's content.</p>"
|
||||
created_at: 2005-04-04 12:00
|
||||
|
||||
- &page-4
|
||||
id: 3
|
||||
title: Shopping Cart
|
||||
handle: shopping-cart
|
||||
url: /pages/shopping-cart
|
||||
author: Tobi
|
||||
content: "<ul><li>Your order is safe with us. Our checkout uses industry standard security to protect your information.</li><li>Your order will be billed immediately upon checkout.</li><li><b>ALL SALES ARE FINAL:</b> Defective or damaged product will be exchanged</li><li>All orders are processed expediently: usually in under 24 hours.</li></ul>"
|
||||
created_at: 2005-04-04 12:00
|
||||
|
||||
- &page-5
|
||||
id: 4
|
||||
title: Shipping and Handling
|
||||
handle: shipping
|
||||
url: /pages/shipping
|
||||
author: Tobi
|
||||
content: <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
|
||||
created_at: 2005-04-04 12:00
|
||||
|
||||
- &page-6
|
||||
id: 5
|
||||
title: Frontpage
|
||||
handle: frontpage
|
||||
url: /pages/frontpage
|
||||
author: Tobi
|
||||
content: <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
|
||||
created_at: 2005-04-04 12:00
|
||||
|
||||
blogs:
|
||||
- id: 1
|
||||
handle: news
|
||||
title: News
|
||||
url: /blogs/news
|
||||
articles:
|
||||
- id: 3
|
||||
title: 'Welcome to the new Foo Shop'
|
||||
author: Daniel
|
||||
content: <p><strong>Welcome to your Shopify store! The jaded Pixel crew is really glad you decided to take Shopify for a spin.</strong></p><p>To help you get you started with Shopify, here are a couple of tips regarding what you see on this page.</p><p>The text you see here is an article. To edit this article, create new articles or create new pages you can go to the <a href="/admin/pages">Blogs & Pages</a> tab of the administration menu.</p><p>The Shopify t-shirt above is a product and selling products is what Shopify is all about. To edit this product, or create new products you can go to the <a href="/admin/products">Products Tab</a> in of the administration menu.</p><p>While you're looking around be sure to check out the <a href="/admin/collections">Collections</a> and <a href="/admin/links">Navigations</a> tabs and soon you will be well on your way to populating your site.</p><p>And of course don't forget to browse the <a href="admin/design/appearance/themes">theme gallery</a> to pick a new look for your shop!</p><p><strong>Shopify is in beta</strong><br />If you would like to make comments or suggestions please visit us in the <a href="http://forums.shopify.com/community">Shopify Forums</a> or drop us an <a href="mailto:[email protected]">email</a>.</p>
|
||||
created_at: 2005-04-04 16:00
|
||||
- id: 4
|
||||
title: 'Breaking News: Restock on all sales products'
|
||||
author: Tobi
|
||||
content: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||
created_at: 2005-04-04 12:00
|
||||
articles_count: 2
|
||||
|
||||
- id: 2
|
||||
handle: bigcheese-blog
|
||||
title: Bigcheese blog
|
||||
url: /blogs/bigcheese-blog
|
||||
articles:
|
||||
- id: 1
|
||||
title: 'One thing you probably did not know yet...'
|
||||
author: Justin
|
||||
content: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||
created_at: 2005-04-04 16:00
|
||||
comments:
|
||||
-
|
||||
id: 1
|
||||
author: John Smith
|
||||
email: [email protected]
|
||||
content: Wow...great article man.
|
||||
status: published
|
||||
created_at: 2009-01-01 12:00
|
||||
updated_at: 2009-02-01 12:00
|
||||
url: ""
|
||||
-
|
||||
id: 2
|
||||
author: John Jones
|
||||
email: [email protected]
|
||||
content: I really enjoyed this article. And I love your shop! It's awesome. Shopify rocks!
|
||||
status: published
|
||||
created_at: 2009-03-01 12:00
|
||||
updated_at: 2009-02-01 12:00
|
||||
url: "http://somesite.com/"
|
||||
- id: 2
|
||||
title: Fascinating
|
||||
author: Tobi
|
||||
content: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||
created_at: 2005-04-06 12:00
|
||||
comments:
|
||||
articles_count: 2
|
||||
comments_enabled?: true
|
||||
comment_post_url: ""
|
||||
comments_count: 2
|
||||
moderated?: true
|
||||
|
||||
- id: 3
|
||||
handle: paginated-blog
|
||||
title: Paginated blog
|
||||
url: /blogs/paginated-blog
|
||||
articles:
|
||||
- id: 6
|
||||
title: 'One thing you probably did not know yet...'
|
||||
author: Justin
|
||||
content: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||
created_at: 2005-04-04 16:00
|
||||
|
||||
- id: 7
|
||||
title: Fascinating
|
||||
author: Tobi
|
||||
content: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||
created_at: 2005-04-06 12:00
|
||||
articles_count: 200
|
||||
Vendored
BIN
Binary file not shown.
+6
-3
@@ -36,12 +36,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,12 +59,14 @@ require 'liquid/forloop_drop'
|
||||
require 'liquid/extensions'
|
||||
require 'liquid/errors'
|
||||
require 'liquid/interrupts'
|
||||
require 'liquid/strainer_factory'
|
||||
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/block'
|
||||
require 'liquid/block_body'
|
||||
require 'liquid/document'
|
||||
@@ -86,4 +90,3 @@ require 'liquid/template_factory'
|
||||
# Load all the tags of the standard library
|
||||
#
|
||||
Dir["#{__dir__}/liquid/tags/*.rb"].each { |f| require f }
|
||||
Dir["#{__dir__}/liquid/registers/*.rb"].each { |f| require f }
|
||||
|
||||
+20
-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,14 @@ module Liquid
|
||||
@block_delimiter ||= "end#{block_name}"
|
||||
end
|
||||
|
||||
protected
|
||||
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 +78,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
|
||||
|
||||
+95
-59
@@ -19,6 +19,8 @@ module Liquid
|
||||
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
|
||||
@@ -28,9 +30,14 @@ module Liquid
|
||||
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
|
||||
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
|
||||
@@ -54,28 +61,62 @@ module Liquid
|
||||
end
|
||||
|
||||
# @api private
|
||||
def self.unknown_tag_in_liquid_tag(end_tag_name, end_tag_markup)
|
||||
yield end_tag_name, end_tag_markup
|
||||
ensure
|
||||
Usage.increment("liquid_tag_contains_outer_tag") unless $ERROR_INFO.is_a?(SyntaxError)
|
||||
def self.unknown_tag_in_liquid_tag(tag, parse_context)
|
||||
Block.raise_unknown_tag(tag, 'liquid', '%}', parse_context)
|
||||
end
|
||||
|
||||
private def parse_liquid_tag(markup, parse_context, &block)
|
||||
liquid_tag_tokenizer = Tokenizer.new(markup, 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|
|
||||
next unless end_tag_name
|
||||
self.class.unknown_tag_in_liquid_tag(end_tag_name, end_tag_markup, &block)
|
||||
# @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_for_document(tokenizer, parse_context, &block)
|
||||
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 def parse_for_document(tokenizer, parse_context)
|
||||
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)
|
||||
BlockBody.raise_missing_tag_terminator(token, parse_context)
|
||||
end
|
||||
tag_name = Regexp.last_match(2)
|
||||
markup = Regexp.last_match(4)
|
||||
@@ -87,7 +128,7 @@ module Liquid
|
||||
end
|
||||
|
||||
if tag_name == 'liquid'
|
||||
parse_liquid_tag(markup, parse_context, &block)
|
||||
parse_liquid_tag(markup, parse_context)
|
||||
next
|
||||
end
|
||||
|
||||
@@ -109,7 +150,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
|
||||
@@ -121,7 +162,11 @@ module Liquid
|
||||
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)
|
||||
@@ -131,38 +176,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
|
||||
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
|
||||
@@ -171,45 +227,25 @@ module Liquid
|
||||
private
|
||||
|
||||
def render_node(context, output, node)
|
||||
if node.disabled?(context)
|
||||
output << node.disabled_error_message
|
||||
return
|
||||
end
|
||||
disable_tags(context, node.disabled_tags) do
|
||||
node.render_to_output_buffer(context, output)
|
||||
end
|
||||
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 disable_tags(context, tags, &block)
|
||||
return yield if tags.empty?
|
||||
context.registers[:disabled_tags].disable(tags, &block)
|
||||
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
|
||||
|
||||
# @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
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'pry-byebug'
|
||||
require 'set'
|
||||
|
||||
module Liquid
|
||||
class Compiler
|
||||
def initialize
|
||||
@ruby = +""
|
||||
@nodes = {}
|
||||
@declared = Set.new(['__l_product'])
|
||||
end
|
||||
|
||||
def <<(line)
|
||||
@ruby << line
|
||||
end
|
||||
|
||||
def var_name(name)
|
||||
"__l_#{name}"
|
||||
end
|
||||
|
||||
def declare(name)
|
||||
@declared << name
|
||||
end
|
||||
|
||||
def declared?(name)
|
||||
@declared.member?(name)
|
||||
end
|
||||
|
||||
def to_proc
|
||||
show_ruby = ENV["SHOW_RUBY"] && ENV["SHOW_RUBY"].to_i || 0
|
||||
STDERR.puts @ruby if show_ruby >= 1
|
||||
res = RubyVM::InstructionSequence.compile(<<~RUBY).eval.call(@nodes)
|
||||
# frozen_string_literal: true
|
||||
->(__nodes) {
|
||||
->(__context, __output, __l_product) {
|
||||
__assigns = __context.scopes.last
|
||||
#{@ruby}
|
||||
__output
|
||||
}
|
||||
}
|
||||
RUBY
|
||||
|
||||
STDERR.puts RubyVM::InstructionSequence.disasm(res) if show_ruby >= 2
|
||||
res
|
||||
end
|
||||
|
||||
def compile_expr(node)
|
||||
if node.respond_to?(:compile_expr)
|
||||
node.compile_expr(self)
|
||||
else
|
||||
case node
|
||||
when Range # returned by RangeLookup when range contains only literals
|
||||
node.inspect
|
||||
when Integer, Float, nil, true, false, String
|
||||
node.inspect
|
||||
else
|
||||
raise ArgumentError, "cannot compile node #{node.inspect}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def output(node)
|
||||
compiled = compile_expr(node)
|
||||
self << if compiled.is_a?(String)
|
||||
"__output << #{compiled}\n"
|
||||
else
|
||||
"__output << #{compiled}.to_s\n"
|
||||
end
|
||||
end
|
||||
|
||||
def fallback_evaluate_expr(node)
|
||||
"__nodes[#{node.object_id}].evaluate(__context)"
|
||||
end
|
||||
|
||||
def to_integer_expr(var_name)
|
||||
<<~RUBY.strip
|
||||
(begin
|
||||
if #{var_name}.is_a?(Integer)
|
||||
#{var_name}
|
||||
else
|
||||
begin
|
||||
Integer(#{var_name}.to_s)
|
||||
rescue ::ArgumentError
|
||||
raise Liquid::ArgumentError, "invalid integer"
|
||||
end
|
||||
end
|
||||
end)
|
||||
RUBY
|
||||
end
|
||||
|
||||
def compile(node)
|
||||
if node.instance_of?(String)
|
||||
self << "__output << #{node.inspect}\n"
|
||||
else
|
||||
@nodes[node.object_id] = node
|
||||
if node.respond_to?(:compile)
|
||||
node.compile(self)
|
||||
else
|
||||
line_number = if node.respond_to?(:line_number) && node.line_number.is_a?(Integer)
|
||||
node.line_number
|
||||
else
|
||||
nil
|
||||
end
|
||||
catch_errors(line_number, show_message: !node.blank?) do
|
||||
self << "__nodes[#{node.object_id}].render_to_output_buffer(__context, __output) # #{node.inspect} \n"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def catch_errors(line_number, show_message: true)
|
||||
self << "begin\n"
|
||||
yield
|
||||
self << <<~RUBY
|
||||
rescue => __exc
|
||||
case __exc
|
||||
when Liquid::MemoryError
|
||||
raise
|
||||
when Liquid::UndefinedVariable, Liquid::UndefinedDropMethod, Liquid::UndefinedFilter
|
||||
__context.handle_error(__exc, #{line_number.inspect})
|
||||
else
|
||||
__error_message = __context.handle_error(__exc, #{line_number.inspect})
|
||||
RUBY
|
||||
self << "__output << __error_message\n" if show_message
|
||||
self << "end\nend\n"
|
||||
end
|
||||
end
|
||||
|
||||
class BlockBody
|
||||
def render_to_output_buffer(context, output)
|
||||
raise "Tried to render uncompiled block" unless @compiled
|
||||
@compiled.call(context, output, context.environments[0][:product])
|
||||
end
|
||||
|
||||
def compile_top_level
|
||||
compiler = Compiler.new
|
||||
compile(compiler)
|
||||
@compiled = compiler.to_proc
|
||||
end
|
||||
|
||||
def compile(compiler)
|
||||
nodelist.each { |node| compiler.compile(node) }
|
||||
end
|
||||
end
|
||||
|
||||
class Document
|
||||
def parse(tokenizer, parse_context)
|
||||
while parse_body(tokenizer)
|
||||
end
|
||||
@body.compile_top_level
|
||||
@body.freeze
|
||||
rescue SyntaxError => e
|
||||
e.line_number ||= parse_context.line_number
|
||||
raise
|
||||
end
|
||||
end
|
||||
|
||||
class VariableLookup
|
||||
def compile_expr(compiler)
|
||||
# HACK
|
||||
if @name == "forloop" && @lookups == ["first"]
|
||||
return "forloop_first"
|
||||
end
|
||||
|
||||
var_name = compiler.var_name(@name)
|
||||
root = if compiler.declared?(var_name)
|
||||
var_name
|
||||
else
|
||||
"__scope[#{@name.inspect}]"
|
||||
end
|
||||
|
||||
@lookups.reduce(root) do |prev, lookup|
|
||||
"#{prev}[:#{lookup}]"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Variable
|
||||
FILTERS = {
|
||||
"modulo" => ->(compiler, expr, args, kwargs) {
|
||||
"(#{expr} % #{args[0]})"
|
||||
},
|
||||
"product_img_url" => ->(compiler, expr, args, kwargs) {
|
||||
style = args.fetch(0, 'small')
|
||||
|
||||
rest = case style
|
||||
when 'original'
|
||||
"\"/files/shops/random_number/\#{url}\""
|
||||
when 'grande', 'large', 'medium', 'compact', 'small', 'thumb', 'icon'
|
||||
"\"/files/shops/random_number/products/\#{$1}_#{style}.\#{$2}\""
|
||||
else
|
||||
"raise ArgumentError, 'valid parameters for filter \"size\" are: original, grande, large, medium, compact, small, thumb and icon '"
|
||||
end
|
||||
|
||||
<<~RUBY.strip
|
||||
(begin
|
||||
if #{expr} =~ %r{\\Aproducts/([\\w\\-\\_]+)\\.(\\w{2,4})}
|
||||
#{rest}
|
||||
else
|
||||
raise ArgumentError, 'filter \"size\" can only be called on product images'
|
||||
end
|
||||
end)
|
||||
RUBY
|
||||
},
|
||||
"escape" => ->(compiler, expr, args, kwargs) {
|
||||
"(_t = #{expr}; CGI.escapeHTML(_t) if _t)"
|
||||
},
|
||||
"money" => ->(compiler, expr, args, kwargs) {
|
||||
"(_m = #{expr}; _m.nil? ? '' : \"$ \#{(_m / 100.0).round(2)}\")"
|
||||
},
|
||||
}
|
||||
|
||||
def compile(compiler)
|
||||
if const?
|
||||
compiler.output(name)
|
||||
else
|
||||
compiler.catch_errors(@line_number, show_message: true) do
|
||||
compiler.output(self)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def const?
|
||||
@filters.empty? && !name.respond_to?(:compile_expr)
|
||||
end
|
||||
|
||||
def compile_expr(compiler)
|
||||
@filters.reduce(compiler.compile_expr(name)) do |expr, (name, args, kwargs)|
|
||||
if FILTERS.key?(name)
|
||||
FILTERS[name].call(compiler, expr, args, kwargs)
|
||||
else
|
||||
expr
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Echo
|
||||
def compile(compiler)
|
||||
compiler.compile(variable)
|
||||
end
|
||||
end
|
||||
|
||||
class For
|
||||
def compile(compiler)
|
||||
compiler << "collection = #{compiler.compile_expr(@collection_name)}\n"
|
||||
|
||||
unless @from.nil?
|
||||
compiler << <<~RUBY
|
||||
from_value = #{compiler.compile_expr(@from)}
|
||||
from = if from_value.nil?
|
||||
0
|
||||
else
|
||||
#{compiler.to_integer_expr(:from_value)}
|
||||
end
|
||||
|
||||
limit_value = #{compiler.compile_expr(@limit)}
|
||||
to = if limit_value.nil?
|
||||
nil
|
||||
else
|
||||
#{compiler.to_integer_expr(:limit_value)} + from
|
||||
end
|
||||
|
||||
collection = #{compiler.slice_collection_expr(:collection, :from, :to)}
|
||||
#{@reversed ? "segment.reverse!" : "" }
|
||||
RUBY
|
||||
end
|
||||
|
||||
item_var = compiler.var_name(variable_name)
|
||||
compiler << <<~RUBY
|
||||
forloop_first = true
|
||||
for #{item_var} in collection
|
||||
RUBY
|
||||
compiler.declare(item_var)
|
||||
compiler.compile(@for_block)
|
||||
compiler << <<~RUBY
|
||||
forloop_first = false
|
||||
end
|
||||
RUBY
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
class If
|
||||
def compile(compiler)
|
||||
compiler << "if #{compiler.compile_expr(blocks[0])}\n"
|
||||
compiler.compile(blocks[0].attachment)
|
||||
blocks.drop(1).each do |block|
|
||||
if block.else?
|
||||
compiler << "else\n"
|
||||
else
|
||||
compiler << "elsif #{block.compile_expr(compiler)}\n"
|
||||
end
|
||||
compiler.compile(block.attachment)
|
||||
end
|
||||
compiler << "end\n"
|
||||
end
|
||||
end
|
||||
|
||||
class Comment
|
||||
def compile(_compiler); end
|
||||
end
|
||||
|
||||
class Condition
|
||||
def compile_expr(compiler)
|
||||
if operator
|
||||
left_expr = compiler.compile_expr(left)
|
||||
right_expr = compiler.compile_expr(right)
|
||||
if child_relation
|
||||
child_expr = compiler.compile_expr(child_condition)
|
||||
expr = "((#{left_expr} #{operator} #{right_expr}) #{child_relation} #{child_expr})"
|
||||
else
|
||||
expr = "(#{left_expr} #{operator} #{right_expr})"
|
||||
end
|
||||
expr
|
||||
else
|
||||
left_expr = compiler.compile_expr(left)
|
||||
expr = "#{left_expr}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Assign
|
||||
def compile(compiler)
|
||||
compiler.declare(compiler.var_name(@to))
|
||||
compiler << "#{compiler.var_name(@to)} = #{compiler.compile_expr(@from)}\n"
|
||||
end
|
||||
end
|
||||
end
|
||||
+30
-6
@@ -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,10 +27,28 @@ 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
|
||||
|
||||
@@ -43,7 +61,7 @@ module Liquid
|
||||
@child_condition = nil
|
||||
end
|
||||
|
||||
def evaluate(context = Context.new)
|
||||
def evaluate(context = deprecated_default_context)
|
||||
condition = self
|
||||
result = nil
|
||||
loop do
|
||||
@@ -91,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
|
||||
@@ -99,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
|
||||
@@ -116,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}")
|
||||
|
||||
@@ -132,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
|
||||
[
|
||||
|
||||
+30
-10
@@ -18,8 +18,8 @@ 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 = {})
|
||||
@@ -34,16 +34,20 @@ module Liquid
|
||||
@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 = {}
|
||||
|
||||
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
|
||||
|
||||
@@ -120,7 +124,7 @@ module Liquid
|
||||
# context['var'] = 'hi'
|
||||
# end
|
||||
#
|
||||
# context['var] #=> nil
|
||||
# context['var'] #=> nil
|
||||
def stack(new_scope = {})
|
||||
push(new_scope)
|
||||
yield
|
||||
@@ -133,7 +137,7 @@ 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)
|
||||
@@ -144,6 +148,7 @@ module Liquid
|
||||
subcontext.strainer = nil
|
||||
subcontext.errors = errors
|
||||
subcontext.warnings = warnings
|
||||
subcontext.disabled_tags = @disabled_tags
|
||||
end
|
||||
end
|
||||
|
||||
@@ -208,9 +213,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
|
||||
|
||||
|
||||
+42
-6
@@ -1,23 +1,34 @@
|
||||
# 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 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 +36,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
|
||||
|
||||
@@ -46,7 +46,6 @@ module Liquid
|
||||
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)
|
||||
@@ -54,5 +53,6 @@ module Liquid
|
||||
UndefinedDropMethod = Class.new(Error)
|
||||
UndefinedFilter = Class.new(Error)
|
||||
MethodOverrideError = Class.new(Error)
|
||||
DisabledError = Class.new(Error)
|
||||
InternalError = Class.new(Error)
|
||||
end
|
||||
|
||||
+23
-29
@@ -2,46 +2,40 @@
|
||||
|
||||
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
|
||||
|
||||
@@ -9,7 +9,12 @@ module Liquid
|
||||
@index = 0
|
||||
end
|
||||
|
||||
attr_reader :name, :length, :parentloop
|
||||
attr_reader :length, :parentloop
|
||||
|
||||
def name
|
||||
Usage.increment('forloop_drop_name')
|
||||
@name
|
||||
end
|
||||
|
||||
def index
|
||||
@index + 1
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"
|
||||
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:
|
||||
|
||||
@@ -19,6 +19,18 @@ 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
|
||||
|
||||
+24
-17
@@ -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,6 +2,18 @@
|
||||
|
||||
module Liquid
|
||||
module ParserSwitching
|
||||
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)
|
||||
|
||||
+61
-93
@@ -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,126 +46,94 @@ module Liquid
|
||||
include Enumerable
|
||||
|
||||
class Timing
|
||||
attr_reader :code, :partial, :line_number, :children, :total_time, :self_time
|
||||
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
|
||||
@total_time = @end_time - @start_time
|
||||
|
||||
if @children.empty?
|
||||
@self_time = @total_time
|
||||
else
|
||||
total_children_time = 0
|
||||
def self_time
|
||||
@self_time ||= begin
|
||||
total_children_time = 0.0
|
||||
@children.each do |child|
|
||||
total_children_time += child.total_time
|
||||
end
|
||||
@self_time = @total_time - total_children_time
|
||||
@total_time - total_children_time
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def render_time
|
||||
@end_time - @start_time
|
||||
attr_reader :total_time
|
||||
alias_method :total_render_time, :total_time
|
||||
|
||||
def initialize
|
||||
@root_children = []
|
||||
@current_children = nil
|
||||
@total_time = 0.0
|
||||
end
|
||||
|
||||
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 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
|
||||
def children
|
||||
children = @root_children
|
||||
if children.length == 1
|
||||
children.first.children
|
||||
else
|
||||
yield
|
||||
children
|
||||
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
|
||||
|
||||
def initialize(partial_name = "<root>")
|
||||
@partial_stack = [partial_name]
|
||||
|
||||
@root_timing = Timing.new("", current_partial)
|
||||
@timing_stack = [@root_timing]
|
||||
|
||||
@render_start_at = Time.now
|
||||
@render_end_at = @render_start_at
|
||||
end
|
||||
|
||||
def start
|
||||
Thread.current[:liquid_profiler] = self
|
||||
@render_start_at = Time.now
|
||||
end
|
||||
|
||||
def stop
|
||||
Thread.current[:liquid_profiler] = nil
|
||||
@render_end_at = Time.now
|
||||
end
|
||||
|
||||
def total_render_time
|
||||
@render_end_at - @render_start_at
|
||||
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
|
||||
|
||||
@@ -12,6 +12,8 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
attr_reader :start_obj, :end_obj
|
||||
|
||||
def initialize(start_obj, end_obj)
|
||||
@start_obj = start_obj
|
||||
@end_obj = end_obj
|
||||
@@ -23,6 +25,10 @@ module Liquid
|
||||
start_int..end_int
|
||||
end
|
||||
|
||||
def compile_expr(compiler)
|
||||
compiler.fallback_evaluate_expr(self)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def to_integer(input)
|
||||
@@ -35,5 +41,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
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
module Liquid
|
||||
class DisabledTags < Register
|
||||
def initialize
|
||||
@disabled_tags = {}
|
||||
end
|
||||
|
||||
def disabled?(tag)
|
||||
@disabled_tags.key?(tag) && @disabled_tags[tag] > 0
|
||||
end
|
||||
|
||||
def disable(tags)
|
||||
tags.each(&method(:increment))
|
||||
yield
|
||||
ensure
|
||||
tags.each(&method(:decrement))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def increment(tag)
|
||||
@disabled_tags[tag] ||= 0
|
||||
@disabled_tags[tag] += 1
|
||||
end
|
||||
|
||||
def decrement(tag)
|
||||
@disabled_tags[tag] -= 1
|
||||
end
|
||||
end
|
||||
|
||||
Template.add_register(:disabled_tags, DisabledTags.new)
|
||||
end
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
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]
|
||||
@@ -12,14 +12,51 @@ module Liquid
|
||||
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
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'cgi'
|
||||
require 'base64'
|
||||
require 'bigdecimal'
|
||||
|
||||
module Liquid
|
||||
module StandardFilters
|
||||
MAX_INT = (1 << 31) - 1
|
||||
HTML_ESCAPE = {
|
||||
'&' => '&',
|
||||
'>' => '>',
|
||||
@@ -41,7 +43,7 @@ module Liquid
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
@@ -62,6 +64,26 @@ module Liquid
|
||||
result
|
||||
end
|
||||
|
||||
def base64_encode(input)
|
||||
Base64.strict_encode64(input.to_s)
|
||||
end
|
||||
|
||||
def base64_decode(input)
|
||||
Base64.strict_decode64(input.to_s)
|
||||
rescue ::ArgumentError
|
||||
raise Liquid::ArgumentError, "invalid base64 provided to base64_decode"
|
||||
end
|
||||
|
||||
def base64_url_safe_encode(input)
|
||||
Base64.urlsafe_encode64(input.to_s)
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def slice(input, offset, length = nil)
|
||||
offset = Utils.to_integer(offset)
|
||||
length = length ? Utils.to_integer(length) : 1
|
||||
@@ -89,13 +111,21 @@ module Liquid
|
||||
|
||||
def truncatewords(input, words = 15, truncate_string = "...")
|
||||
return if input.nil?
|
||||
wordlist = input.to_s.split
|
||||
words = Utils.to_integer(words)
|
||||
input = input.to_s
|
||||
words = Utils.to_integer(words)
|
||||
words = 1 if words <= 0
|
||||
|
||||
l = words - 1
|
||||
l = 0 if l < 0
|
||||
wordlist = begin
|
||||
input.split(" ", words + 1)
|
||||
rescue RangeError
|
||||
raise if words + 1 < MAX_INT
|
||||
# e.g. integer #{words} too big to convert to `int'
|
||||
raise Liquid::ArgumentError, "integer #{words} too big for truncatewords"
|
||||
end
|
||||
return input if wordlist.length <= words
|
||||
|
||||
wordlist.length > l ? wordlist[0..l].join(" ").concat(truncate_string.to_s) : input
|
||||
wordlist.pop
|
||||
wordlist.join(" ").concat(truncate_string.to_s)
|
||||
end
|
||||
|
||||
# Split input string into an array of substrings separated by given pattern.
|
||||
@@ -183,17 +213,23 @@ module Liquid
|
||||
|
||||
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
|
||||
@@ -207,11 +243,14 @@ module Liquid
|
||||
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
|
||||
@@ -247,11 +286,14 @@ module Liquid
|
||||
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
|
||||
@@ -266,14 +308,34 @@ module Liquid
|
||||
input.to_s.sub(string.to_s, replacement.to_s)
|
||||
end
|
||||
|
||||
# Replace the last occurrences of a string with another
|
||||
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
|
||||
|
||||
# remove a substring
|
||||
def remove(input, string)
|
||||
input.to_s.gsub(string.to_s, '')
|
||||
replace(input, string, '')
|
||||
end
|
||||
|
||||
# remove the first occurrences of a substring
|
||||
def remove_first(input, string)
|
||||
input.to_s.sub(string.to_s, '')
|
||||
replace_first(input, string, '')
|
||||
end
|
||||
|
||||
# remove the last occurences of a substring
|
||||
def remove_last(input, string)
|
||||
replace_last(input, string, '')
|
||||
end
|
||||
|
||||
# add one string to another
|
||||
@@ -295,7 +357,7 @@ module Liquid
|
||||
|
||||
# Add <br /> tags in front of all newlines in input 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
|
||||
@@ -438,7 +500,7 @@ module Liquid
|
||||
#
|
||||
def default(input, default_value = '', options = {})
|
||||
options = {} unless options.is_a?(Hash)
|
||||
false_check = options['allow_false'] ? input.nil? : !input
|
||||
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
|
||||
|
||||
@@ -456,10 +518,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
|
||||
|
||||
@@ -514,8 +582,9 @@ module Liquid
|
||||
|
||||
def each
|
||||
@input.each do |e|
|
||||
e = e.respond_to?(:to_liquid) ? e.to_liquid : e
|
||||
e.context = @context if e.respond_to?(:context=)
|
||||
yield(e.respond_to?(:to_liquid) ? e.to_liquid : e)
|
||||
yield(e)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
module Liquid
|
||||
class StaticRegisters
|
||||
attr_reader :static, :registers
|
||||
attr_reader :static
|
||||
|
||||
def initialize(registers = {})
|
||||
@static = registers.is_a?(StaticRegisters) ? registers.static : registers
|
||||
@@ -25,8 +25,20 @@ module Liquid
|
||||
@registers.delete(key)
|
||||
end
|
||||
|
||||
def fetch(key, default = nil)
|
||||
key?(key) ? self[key] : default
|
||||
UNDEFINED = Object.new
|
||||
|
||||
def fetch(key, default = UNDEFINED, &block)
|
||||
if @registers.key?(key)
|
||||
@registers.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)
|
||||
|
||||
@@ -7,25 +7,26 @@ module Liquid
|
||||
|
||||
def add_global_filter(filter)
|
||||
strainer_class_cache.clear
|
||||
global_filters << filter
|
||||
GlobalCache.add_filter(filter)
|
||||
end
|
||||
|
||||
def create(context, filters = [])
|
||||
strainer_from_cache(filters).new(context)
|
||||
end
|
||||
|
||||
GlobalCache = Class.new(StrainerTemplate)
|
||||
|
||||
private
|
||||
|
||||
def global_filters
|
||||
@global_filters ||= []
|
||||
end
|
||||
|
||||
def strainer_from_cache(filters)
|
||||
strainer_class_cache[filters] ||= begin
|
||||
klass = Class.new(StrainerTemplate)
|
||||
global_filters.each { |f| klass.add_filter(f) }
|
||||
filters.each { |f| klass.add_filter(f) }
|
||||
klass
|
||||
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
|
||||
|
||||
|
||||
@@ -31,6 +31,11 @@ module Liquid
|
||||
filter_methods.include?(method.to_s)
|
||||
end
|
||||
|
||||
def inherited(subclass)
|
||||
super
|
||||
subclass.instance_variable_set(:@filter_methods, @filter_methods.dup)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def filter_methods
|
||||
|
||||
+8
-16
@@ -13,15 +13,13 @@ module Liquid
|
||||
tag
|
||||
end
|
||||
|
||||
def disable_tags(*tags)
|
||||
disabled_tags.push(*tags)
|
||||
def disable_tags(*tag_names)
|
||||
@disabled_tags ||= []
|
||||
@disabled_tags.concat(tag_names)
|
||||
prepend(Disabler)
|
||||
end
|
||||
|
||||
private :new
|
||||
|
||||
def disabled_tags
|
||||
@disabled_tags ||= []
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(tag_name, markup, parse_context)
|
||||
@@ -46,14 +44,6 @@ module Liquid
|
||||
''
|
||||
end
|
||||
|
||||
def disabled?(context)
|
||||
context.registers[:disabled_tags].disabled?(tag_name)
|
||||
end
|
||||
|
||||
def disabled_error_message
|
||||
"#{tag_name} #{options[:locale].t('errors.disabled.tag')}"
|
||||
end
|
||||
|
||||
# For backwards compatibility with custom tags. In a future release, the semantics
|
||||
# of the `render_to_output_buffer` method will become the default and the `render`
|
||||
# method will be removed.
|
||||
@@ -66,8 +56,10 @@ module Liquid
|
||||
false
|
||||
end
|
||||
|
||||
def disabled_tags
|
||||
self.class.disabled_tags
|
||||
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,21 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Tag
|
||||
module Disabler
|
||||
module ClassMethods
|
||||
attr_reader :disabled_tags
|
||||
end
|
||||
|
||||
def self.prepended(base)
|
||||
base.extend(ClassMethods)
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
context.with_disabled_tags(self.class.disabled_tags) do
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -12,26 +12,27 @@ module Liquid
|
||||
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
|
||||
|
||||
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)
|
||||
@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 +45,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
|
||||
|
||||
@@ -11,8 +11,11 @@ module Liquid
|
||||
# {% endfor %}
|
||||
#
|
||||
class Break < Tag
|
||||
def interrupt
|
||||
BreakInterrupt.new
|
||||
INTERRUPT = BreakInterrupt.new.freeze
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
context.push_interrupt(INTERRUPT)
|
||||
output
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -25,10 +25,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
|
||||
|
||||
|
||||
+21
-6
@@ -12,15 +12,23 @@ module Liquid
|
||||
@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 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 +52,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
|
||||
@@ -56,7 +71,7 @@ module Liquid
|
||||
private
|
||||
|
||||
def record_when_condition(markup)
|
||||
body = BlockBody.new
|
||||
body = new_body
|
||||
|
||||
while markup
|
||||
unless markup =~ WhenSyntax
|
||||
@@ -65,7 +80,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 +92,7 @@ module Liquid
|
||||
end
|
||||
|
||||
block = ElseCondition.new
|
||||
block.attach(BlockBody.new)
|
||||
block.attach(new_body)
|
||||
@blocks << block
|
||||
end
|
||||
|
||||
|
||||
@@ -11,8 +11,11 @@ module Liquid
|
||||
# {% endfor %}
|
||||
#
|
||||
class Continue < Tag
|
||||
def interrupt
|
||||
ContinueInterrupt.new
|
||||
INTERRUPT = ContinueInterrupt.new.freeze
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
context.push_interrupt(INTERRUPT)
|
||||
output
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ module Liquid
|
||||
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
|
||||
@@ -61,7 +61,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
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ module Liquid
|
||||
# {% echo user | link %}
|
||||
#
|
||||
class Echo < Tag
|
||||
attr_reader :variable
|
||||
|
||||
def initialize(tag_name, markup, parse_context)
|
||||
super
|
||||
@variable = Variable.new(markup, parse_context)
|
||||
@@ -20,6 +22,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)
|
||||
|
||||
+16
-8
@@ -54,13 +54,20 @@ module Liquid
|
||||
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
|
||||
@@ -69,7 +76,7 @@ module Liquid
|
||||
|
||||
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)
|
||||
@@ -92,7 +99,7 @@ module Liquid
|
||||
collection_name = Regexp.last_match(2)
|
||||
@reversed = !!Regexp.last_match(3)
|
||||
@name = "#{@variable_name}-#{collection_name}"
|
||||
@collection_name = Expression.parse(collection_name)
|
||||
@collection_name = parse_expression(collection_name)
|
||||
markup.scan(TagAttributes) do |key, value|
|
||||
set_attribute(key, value)
|
||||
end
|
||||
@@ -107,7 +114,7 @@ module Liquid
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in") unless p.id?('in')
|
||||
|
||||
collection_name = p.expression
|
||||
@collection_name = Expression.parse(collection_name)
|
||||
@collection_name = parse_expression(collection_name)
|
||||
|
||||
@name = "#{@variable_name}-#{collection_name}"
|
||||
@reversed = p.id?('reversed')
|
||||
@@ -191,12 +198,13 @@ module Liquid
|
||||
case key
|
||||
when 'offset'
|
||||
@from = if expr == 'continue'
|
||||
Usage.increment('for_offset_continue')
|
||||
:continue
|
||||
else
|
||||
Expression.parse(expr)
|
||||
parse_expression(expr)
|
||||
end
|
||||
when 'limit'
|
||||
@limit = Expression.parse(expr)
|
||||
@limit = parse_expression(expr)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
+22
-7
@@ -31,10 +31,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 +50,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 +72,25 @@ 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
|
||||
|
||||
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
|
||||
@@ -103,9 +118,9 @@ module Liquid
|
||||
end
|
||||
|
||||
def parse_comparison(p)
|
||||
a = Expression.parse(p.expression)
|
||||
a = parse_expression(p.expression)
|
||||
if (op = p.consume?(:comparison))
|
||||
b = Expression.parse(p.expression)
|
||||
b = parse_expression(p.expression)
|
||||
Condition.new(a, op, b)
|
||||
else
|
||||
Condition.new(a)
|
||||
|
||||
@@ -16,6 +16,8 @@ module Liquid
|
||||
# {% include 'product' for products %}
|
||||
#
|
||||
class Include < Tag
|
||||
prepend Tag::Disableable
|
||||
|
||||
SYNTAX = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
|
||||
Syntax = SYNTAX
|
||||
|
||||
@@ -30,12 +32,12 @@ module Liquid
|
||||
variable_name = Regexp.last_match(3)
|
||||
|
||||
@alias_name = Regexp.last_match(5)
|
||||
@variable_name_expr = variable_name ? Expression.parse(variable_name) : nil
|
||||
@template_name_expr = Expression.parse(template_name)
|
||||
@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
|
||||
|
||||
@@ -14,14 +14,14 @@ module Liquid
|
||||
def parse(tokens)
|
||||
@body = +''
|
||||
while (token = tokens.shift)
|
||||
if token =~ FullTokenPossiblyInvalid
|
||||
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)
|
||||
|
||||
@@ -19,13 +19,13 @@ module Liquid
|
||||
variable_name = Regexp.last_match(4)
|
||||
|
||||
@alias_name = Regexp.last_match(6)
|
||||
@variable_name_expr = variable_name ? Expression.parse(variable_name) : nil
|
||||
@template_name_expr = Expression.parse(template_name)
|
||||
@variable_name_expr = variable_name ? parse_expression(variable_name) : nil
|
||||
@template_name_expr = parse_expression(template_name)
|
||||
@for = (with_or_for == FOR)
|
||||
|
||||
@attributes = {}
|
||||
markup.scan(TagAttributes) do |key, value|
|
||||
@attributes[key] = Expression.parse(value)
|
||||
@attributes[key] = parse_expression(value)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -10,10 +10,10 @@ module Liquid
|
||||
super
|
||||
if markup =~ Syntax
|
||||
@variable_name = Regexp.last_match(1)
|
||||
@collection_name = Expression.parse(Regexp.last_match(2))
|
||||
@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")
|
||||
|
||||
@@ -11,13 +11,21 @@ module Liquid
|
||||
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
|
||||
|
||||
+20
-60
@@ -64,23 +64,6 @@ module Liquid
|
||||
attr_accessor :error_mode
|
||||
Template.error_mode = :lax
|
||||
|
||||
attr_reader :taint_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
|
||||
# @deprecated Since it is being deprecated in ruby itself.
|
||||
def taint_mode=(mode)
|
||||
taint_supported = Object.new.taint.tainted?
|
||||
if mode != :lax && !taint_supported
|
||||
raise NotImplementedError, "#{RUBY_ENGINE} #{RUBY_VERSION} doesn't support taint checking"
|
||||
end
|
||||
@taint_mode = mode
|
||||
end
|
||||
|
||||
Template.taint_mode = :lax
|
||||
|
||||
attr_accessor :default_exception_renderer
|
||||
Template.default_exception_renderer = lambda do |exception|
|
||||
exception
|
||||
@@ -97,14 +80,6 @@ module Liquid
|
||||
tags[name.to_s] = klass
|
||||
end
|
||||
|
||||
attr_accessor :registers
|
||||
Template.registers = {}
|
||||
private :registers=
|
||||
|
||||
def add_register(name, klass)
|
||||
registers[name.to_sym] = klass
|
||||
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)
|
||||
@@ -119,25 +94,21 @@ module Liquid
|
||||
# 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
|
||||
end
|
||||
|
||||
def initialize
|
||||
@rethrow_errors = false
|
||||
@resource_limits = ResourceLimits.new(self.class.default_resource_limits)
|
||||
@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
|
||||
|
||||
@@ -179,7 +150,7 @@ module Liquid
|
||||
c = args.shift
|
||||
|
||||
if @rethrow_errors
|
||||
c.exception_renderer = ->(_e) { raise }
|
||||
c.exception_renderer = Liquid::RAISE_EXCEPTION_LAMBDA
|
||||
end
|
||||
|
||||
c
|
||||
@@ -212,19 +183,16 @@ module Liquid
|
||||
context.add_filters(args.pop)
|
||||
end
|
||||
|
||||
Template.registers.each do |key, register|
|
||||
context_register[key] = register
|
||||
end
|
||||
|
||||
# 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
|
||||
@@ -243,25 +211,17 @@ 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(context.template_name)
|
||||
@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)
|
||||
|
||||
@@ -5,7 +5,7 @@ module Liquid
|
||||
attr_reader :line_number, :for_liquid_tag
|
||||
|
||||
def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false)
|
||||
@source = source
|
||||
@source = source.to_s.to_str
|
||||
@line_number = line_number || (line_numbers ? 1 : nil)
|
||||
@for_liquid_tag = for_liquid_tag
|
||||
@tokens = tokenize
|
||||
@@ -24,7 +24,7 @@ module Liquid
|
||||
private
|
||||
|
||||
def tokenize
|
||||
return [] if @source.to_s.empty?
|
||||
return [] if @source.empty?
|
||||
|
||||
return @source.split("\n") if @for_liquid_tag
|
||||
|
||||
|
||||
@@ -81,5 +81,13 @@ 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
|
||||
end
|
||||
end
|
||||
|
||||
+12
-25
@@ -30,7 +30,7 @@ module Liquid
|
||||
@parse_context = parse_context
|
||||
@line_number = parse_context.line_number
|
||||
|
||||
parse_with_selected_parser(markup)
|
||||
strict_parse_with_error_mode_fallback(markup)
|
||||
end
|
||||
|
||||
def raw
|
||||
@@ -63,6 +63,8 @@ module Liquid
|
||||
@filters = []
|
||||
p = Parser.new(markup)
|
||||
|
||||
return if p.look(:end_of_string)
|
||||
|
||||
@name = Expression.parse(p.expression)
|
||||
while p.consume?(:pipe)
|
||||
filtername = p.consume(:id)
|
||||
@@ -81,14 +83,18 @@ 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 compile
|
||||
"__output << #{@name}\n"
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
@@ -142,25 +148,6 @@ module Liquid
|
||||
parsed_args
|
||||
end
|
||||
|
||||
def taint_check(context, obj)
|
||||
return if Template.taint_mode == :lax
|
||||
return unless obj.tainted?
|
||||
|
||||
@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,8 +2,7 @@
|
||||
|
||||
module Liquid
|
||||
class VariableLookup
|
||||
SQUARE_BRACKETED = /\A\[(.*)\]\z/m
|
||||
COMMAND_METHODS = ['size', 'first', 'last'].freeze
|
||||
COMMAND_METHODS = ['size', 'first', 'last'].freeze
|
||||
|
||||
attr_reader :name, :lookups
|
||||
|
||||
@@ -15,8 +14,8 @@ module Liquid
|
||||
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
|
||||
|
||||
@@ -25,8 +24,8 @@ module Liquid
|
||||
|
||||
@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
|
||||
@@ -40,6 +39,9 @@ module Liquid
|
||||
@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?(:[]) &&
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
VERSION = "4.0.3"
|
||||
VERSION = "5.3.0"
|
||||
end
|
||||
|
||||
+4
-2
@@ -17,9 +17,11 @@ 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.5.0"
|
||||
s.required_rubygems_version = ">= 1.3.7"
|
||||
|
||||
s.metadata['allowed_push_host'] = 'https://rubygems.org'
|
||||
|
||||
s.test_files = Dir.glob("{test}/**/*")
|
||||
s.files = Dir.glob("{lib}/**/*") + %w(LICENSE README.md)
|
||||
|
||||
@@ -27,6 +29,6 @@ Gem::Specification.new do |s|
|
||||
|
||||
s.require_path = "lib"
|
||||
|
||||
s.add_development_dependency('rake', '~> 11.3')
|
||||
s.add_development_dependency('rake', '~> 13.0')
|
||||
s.add_development_dependency('minitest')
|
||||
end
|
||||
|
||||
Executable
+224
@@ -0,0 +1,224 @@
|
||||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
unless ENV.key?("BUNDLE_BIN_PATH")
|
||||
exec("bundle", "exec", "ruby", __FILE__, *ARGV)
|
||||
end
|
||||
|
||||
require "pry"
|
||||
require "liquid"
|
||||
require "unicode_plot"
|
||||
require "optparse"
|
||||
require "open3"
|
||||
require "csv"
|
||||
require "io/console"
|
||||
|
||||
TERM_ROWS, TERM_COLS = IO.console.winsize
|
||||
|
||||
def record
|
||||
if ARGV.count != 2
|
||||
STDERR.puts "Usage: benchmark.rb record [output_path]"
|
||||
exit(1)
|
||||
end
|
||||
output_path = ARGV[1]
|
||||
out, status = Open3.capture2("ruby", "#{__dir__}/benchmark_child.rb")
|
||||
File.write(output_path, out)
|
||||
end
|
||||
|
||||
def calc_stats(nums)
|
||||
mean = nums.reduce(:+) / nums.length
|
||||
variance = nums.map { |n| (n - mean).pow(2) }.reduce(:+) / nums.length
|
||||
stddev = Math.sqrt(variance)
|
||||
{
|
||||
mean: mean,
|
||||
variance: variance,
|
||||
stddev: stddev,
|
||||
normalized: normalize_outliers(mean, stddev, nums),
|
||||
raw: nums
|
||||
}
|
||||
end
|
||||
|
||||
def normalize_outliers(mean, stddev, nums)
|
||||
cutoff = stddev * 3
|
||||
nums.map do |n|
|
||||
if (n - mean).abs < cutoff
|
||||
n
|
||||
else
|
||||
mean
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def show
|
||||
if ARGV.count < 2
|
||||
STDERR.puts "Usage: benchmark.rb show [path1] [path2]? ..."
|
||||
exit(1)
|
||||
end
|
||||
|
||||
recordings = ARGV.drop(1).to_h do |path|
|
||||
[File.basename(path), CSV.parse(
|
||||
File.open(path),
|
||||
col_sep: "\t",
|
||||
headers: true,
|
||||
converters: :integer
|
||||
)]
|
||||
end
|
||||
|
||||
runs = (1..1000).to_a
|
||||
recordings.values.first.headers.each do |benchmark|
|
||||
colors = [:green, :blue, :red]
|
||||
|
||||
10.times { puts }
|
||||
title = "Benchmark: #{benchmark} (times in µs)"
|
||||
print " " * (TERM_COLS / 2 - title.length)
|
||||
puts title
|
||||
puts
|
||||
|
||||
all_stats = recordings.transform_values do |csv|
|
||||
stats = calc_stats(csv.map { |row| row[benchmark] })
|
||||
stats[:color] = colors.shift
|
||||
stats
|
||||
end
|
||||
|
||||
line_plots = []
|
||||
distributions = []
|
||||
shared_line_plot = nil
|
||||
|
||||
max_mean_stats = all_stats.values.max_by { |stats| stats[:mean] }
|
||||
max_y = (max_mean_stats[:mean] + max_mean_stats[:stddev] * 3).to_i
|
||||
|
||||
all_stats.each do |name, stats|
|
||||
if shared_line_plot == nil
|
||||
shared_line_plot = UnicodePlot.lineplot(
|
||||
runs,
|
||||
stats[:normalized],
|
||||
name: name,
|
||||
width: TERM_COLS - 25,
|
||||
ylim: [0, max_y],
|
||||
color: stats[:color]
|
||||
)
|
||||
else
|
||||
UnicodePlot.lineplot!(shared_line_plot, stats[:normalized], name: name, color: stats[:color])
|
||||
end
|
||||
|
||||
line_plots << render_to_s(UnicodePlot.lineplot(
|
||||
runs,
|
||||
stats[:normalized],
|
||||
name: name,
|
||||
color: stats[:color],
|
||||
width: 40
|
||||
))
|
||||
|
||||
distributions << render_to_s(UnicodePlot.histogram(
|
||||
stats[:normalized],
|
||||
title: name,
|
||||
color: stats[:color]
|
||||
))
|
||||
end
|
||||
|
||||
shared_line_plot.render
|
||||
print_columns(line_plots)
|
||||
print_columns(distributions)
|
||||
|
||||
all_times = all_stats.transform_values { |stats| stats[:normalized] }
|
||||
UnicodePlot.boxplot(data: all_times, title: "Comparison", width: TERM_COLS - 25).render
|
||||
end
|
||||
end
|
||||
|
||||
def render_to_s(plot)
|
||||
io = StringIO.new
|
||||
plot.render(io, color: true)
|
||||
io.string
|
||||
end
|
||||
|
||||
def visual_length(line)
|
||||
line.gsub(/\e\[(\d+)m/, '').length
|
||||
end
|
||||
|
||||
def print_columns(cols)
|
||||
col_lines = cols.map { |col| col.split("\n") }
|
||||
col_width = col_lines.map do |lines|
|
||||
lines.map { |line| visual_length(line) }.max
|
||||
end.max
|
||||
col_height = col_lines.map { |lines| lines.length }.max
|
||||
|
||||
(0...col_height).each do |i|
|
||||
col_lines.each do |lines|
|
||||
line = lines[i] || ""
|
||||
vis_length = visual_length(line)
|
||||
print line
|
||||
print(" " * (col_width - vis_length))
|
||||
end
|
||||
puts
|
||||
end
|
||||
end
|
||||
|
||||
ITERS = 1
|
||||
Benchmarks = Class.new do
|
||||
def define(_name, benchmark)
|
||||
@benchmark = benchmark
|
||||
end
|
||||
|
||||
def run
|
||||
start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
||||
@benchmark.compile
|
||||
parsed = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
||||
|
||||
# warmup
|
||||
ITERS.times { @benchmark.render}
|
||||
warm = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
||||
|
||||
res = nil
|
||||
ITERS.times { res = @benchmark.render }
|
||||
ran = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
||||
|
||||
if res.is_a?(String)
|
||||
puts res
|
||||
puts "Output digest: #{Digest::SHA2.hexdigest(res)}"
|
||||
end
|
||||
|
||||
if ENV['SHOW_RUBY'] == '1'
|
||||
STDERR.puts "note: SHOW_RUBY prevents gathering parse metrics"
|
||||
STDERR.puts "render: #{(ran - warm) / ITERS}µs"
|
||||
else
|
||||
STDERR.puts "parse: #{parsed - start}µs"
|
||||
STDERR.puts "render: #{(ran - warm) / ITERS}µs"
|
||||
STDERR.puts "total: #{(ran - warm) / ITERS + (parsed - start)}µs"
|
||||
end
|
||||
end
|
||||
end.new
|
||||
|
||||
def execute
|
||||
require 'digest'
|
||||
|
||||
if ARGV.count != 2
|
||||
STDERR.puts "Usage: benchmark.rb record [output_path]"
|
||||
exit(1)
|
||||
end
|
||||
|
||||
case ENV['ENGINE']
|
||||
when 'LIQUID_COMPILE'
|
||||
require_relative '../lib/liquid/compile'
|
||||
when 'LIQUID_C'
|
||||
require 'liquid/c'
|
||||
when 'LIQUID_RUBY'
|
||||
else
|
||||
raise "Invalid ENGINE: #{ENV['ENGINE'].inspect}, expected ENGINE=(LIQUID_RUBY|LIQUID_COMPILE)"
|
||||
end
|
||||
|
||||
require_relative "./benchmarks/#{ARGV[1]}.rb"
|
||||
Benchmarks.run
|
||||
end
|
||||
|
||||
case ARGV.first
|
||||
when "record", "r"
|
||||
record
|
||||
when "show", "s"
|
||||
show
|
||||
when "execute", "x"
|
||||
execute
|
||||
else
|
||||
puts "Invalid command. Expected benchmark [record|show|execute] ..."
|
||||
exit 1
|
||||
end
|
||||
@@ -1,20 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'benchmark/ips'
|
||||
require_relative 'theme_runner'
|
||||
|
||||
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
|
||||
profiler = ThemeRunner.new
|
||||
|
||||
Benchmark.ips do |x|
|
||||
x.time = 10
|
||||
x.warmup = 5
|
||||
|
||||
puts
|
||||
puts "Running benchmark for #{x.time} seconds (with #{x.warmup} seconds warmup)."
|
||||
puts
|
||||
|
||||
x.report("parse:") { profiler.compile }
|
||||
x.report("render:") { profiler.render }
|
||||
x.report("parse & render:") { profiler.run }
|
||||
end
|
||||
@@ -0,0 +1,62 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'liquid'
|
||||
|
||||
Liquid::Template.error_mode = :strict
|
||||
|
||||
case ENV['ENGINE']
|
||||
when 'LIQUID_COMPILE'
|
||||
require_relative '../lib/liquid/compile'
|
||||
when 'LIQUID_C'
|
||||
require 'liquid/c'
|
||||
when 'LIQUID_RUBY'
|
||||
else
|
||||
raise "Invalid engine: #{ENV['ENGINE'].inspect}"
|
||||
end
|
||||
|
||||
OPTIONS = {
|
||||
render_iters: 1000
|
||||
}
|
||||
|
||||
def get_time_us
|
||||
Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
||||
end
|
||||
|
||||
Benchmarks = Class.new do
|
||||
def initialize
|
||||
@by_name = {}
|
||||
end
|
||||
|
||||
def define(name, benchmark)
|
||||
@by_name[name] = benchmark
|
||||
end
|
||||
|
||||
def run
|
||||
times = {}
|
||||
@by_name.each do |name, benchmark|
|
||||
benchmark.compile
|
||||
|
||||
times[name] = OPTIONS[:render_iters].times.map do
|
||||
before = get_time_us
|
||||
benchmark.render
|
||||
get_time_us - before
|
||||
end
|
||||
end
|
||||
|
||||
puts times.keys.join("\t")
|
||||
cols = times.values
|
||||
OPTIONS[:render_iters].times do |i|
|
||||
cols.each do |values|
|
||||
print values[i]
|
||||
print "\t" unless values == cols.last
|
||||
end
|
||||
puts
|
||||
end
|
||||
end
|
||||
end.new
|
||||
|
||||
Dir[__dir__ + "/benchmarks/*.rb", base: __dir__].each do |path|
|
||||
require_relative path
|
||||
end
|
||||
|
||||
Benchmarks.run
|
||||
@@ -0,0 +1,28 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
Benchmarks.define('fizzbuzz_10000', Class.new do
|
||||
TEMPLATE = <<~LIQUID
|
||||
{% for i in (1..#{ENV['COUNT'].to_i}) %}
|
||||
{% liquid
|
||||
assign rem_3 = i | modulo: 3
|
||||
assign rem_5 = i | modulo: 5
|
||||
if rem_3 == 0 and rem_5 == 0
|
||||
echo "Fizzbuzz"
|
||||
elsif rem_3 == 0
|
||||
echo "Fizz"
|
||||
elsif rem_5 == 0
|
||||
echo "Buzz"
|
||||
else
|
||||
echo i
|
||||
endif
|
||||
%}{% endfor %}
|
||||
LIQUID
|
||||
|
||||
def compile
|
||||
@parsed = Liquid::Template.parse(TEMPLATE)
|
||||
end
|
||||
|
||||
def render
|
||||
@parsed.render
|
||||
end
|
||||
end.new)
|
||||
@@ -0,0 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
Benchmarks.define('simple_loop', Class.new do
|
||||
TEMPLATE = "{% for i in (1..1000) %}{{ i }}{% endfor %}"
|
||||
|
||||
def compile
|
||||
@parsed = Liquid::Template.parse(TEMPLATE)
|
||||
end
|
||||
|
||||
def render
|
||||
@parsed.render
|
||||
end
|
||||
end.new)
|
||||
@@ -0,0 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../theme_runner'
|
||||
|
||||
#Benchmarks.define('theme_runner', ThemeRunner.new)
|
||||
@@ -3,6 +3,16 @@
|
||||
require 'stackprof'
|
||||
require_relative 'theme_runner'
|
||||
|
||||
if ENV['LIQUID_C'] == '1'
|
||||
puts "-- LIQUID C"
|
||||
require 'liquid/c'
|
||||
end
|
||||
|
||||
if ENV['LIQUID_COMPILE'] == '1'
|
||||
puts "-- COMPILED"
|
||||
require 'liquid/compile'
|
||||
end
|
||||
|
||||
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
|
||||
profiler = ThemeRunner.new
|
||||
profiler.run
|
||||
|
||||
@@ -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|
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
$LOAD_PATH.unshift(__dir__ + '/../../lib')
|
||||
require_relative '../../lib/liquid'
|
||||
|
||||
require_relative 'comment_form'
|
||||
require_relative 'paginate'
|
||||
require_relative 'json_filter'
|
||||
|
||||
@@ -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
|
||||
compiled_test = compile_test(template, layout, assigns, page_template, template_file)
|
||||
render_layout(compiled_test[:tmpl], compiled_test[:layout], compiled_test[:assigns])
|
||||
end
|
||||
|
||||
def compile_all_tests
|
||||
|
||||
@@ -44,7 +44,74 @@ class AssignTest < Minitest::Test
|
||||
end
|
||||
end
|
||||
with_error_mode(:lax) do
|
||||
assert Template.parse("{% assign foo = ('X' | downcase) %}")
|
||||
assert(Template.parse("{% assign foo = ('X' | downcase) %}"))
|
||||
end
|
||||
end
|
||||
end # AssignTest
|
||||
|
||||
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
|
||||
|
||||
def value
|
||||
@obj
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
@@ -11,4 +11,48 @@ class BlockTest < Minitest::Test
|
||||
end
|
||||
assert_equal(exc.message, "Liquid syntax error: 'endunless' is not a valid delimiter for if tags. use endif")
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
@@ -49,4 +49,10 @@ class CaptureTest < Minitest::Test
|
||||
rendered = template.render!
|
||||
assert_equal("3-3", rendered.gsub(/\s/, ''))
|
||||
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,595 @@
|
||||
|
||||
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_nil(@context['does_not_exist'])
|
||||
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
|
||||
@context['numbers'] = [1, 2, 3, 4]
|
||||
|
||||
assert_equal(4, @context['numbers.size'])
|
||||
|
||||
@context['numbers'] = { 1 => 1, 2 => 2, 3 => 3, 4 => 4 }
|
||||
|
||||
assert_equal(4, @context['numbers.size'])
|
||||
|
||||
@context['numbers'] = { 1 => 1, 2 => 2, 3 => 3, 4 => 4, 'size' => 1000 }
|
||||
|
||||
assert_equal(1000, @context['numbers.size'])
|
||||
end
|
||||
|
||||
def test_hyphenated_variable
|
||||
@context['oh-my'] = 'godz'
|
||||
assert_equal('godz', @context['oh-my'])
|
||||
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
|
||||
@context['hash'] = { "name" => 'tobi' }
|
||||
assert_equal('tobi', @context['hash.name'])
|
||||
assert_equal('tobi', @context['hash["name"]'])
|
||||
end
|
||||
|
||||
def test_keywords
|
||||
assert_equal(true, @context['true'])
|
||||
assert_equal(false, @context['false'])
|
||||
end
|
||||
|
||||
def test_digits
|
||||
assert_equal(100, @context['100'])
|
||||
assert_equal(100.00, @context['100.00'])
|
||||
end
|
||||
|
||||
def test_strings
|
||||
assert_equal("hello!", @context['"hello!"'])
|
||||
assert_equal("hello!", @context["'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
|
||||
@context['test'] = [1, 2, 3, 4, 5]
|
||||
|
||||
assert_equal(1, @context['test[0]'])
|
||||
assert_equal(2, @context['test[1]'])
|
||||
assert_equal(3, @context['test[2]'])
|
||||
assert_equal(4, @context['test[3]'])
|
||||
assert_equal(5, @context['test[4]'])
|
||||
end
|
||||
|
||||
def test_recoursive_array_notation
|
||||
@context['test'] = { 'test' => [1, 2, 3, 4, 5] }
|
||||
|
||||
assert_equal(1, @context['test.test[0]'])
|
||||
|
||||
@context['test'] = [{ 'test' => 'worked' }]
|
||||
|
||||
assert_equal('worked', @context['test[0].test'])
|
||||
end
|
||||
|
||||
def test_hash_to_array_transition
|
||||
@context['colors'] = {
|
||||
'Blue' => ['003366', '336699', '6699CC', '99CCFF'],
|
||||
'Green' => ['003300', '336633', '669966', '99CC99'],
|
||||
'Yellow' => ['CC9900', 'FFCC00', 'FFFF99', 'FFFFCC'],
|
||||
'Red' => ['660000', '993333', 'CC6666', 'FF9999'],
|
||||
}
|
||||
|
||||
assert_equal('003366', @context['colors.Blue[0]'])
|
||||
assert_equal('FF9999', @context['colors.Red[3]'])
|
||||
end
|
||||
|
||||
def test_try_first
|
||||
@context['test'] = [1, 2, 3, 4, 5]
|
||||
|
||||
assert_equal(1, @context['test.first'])
|
||||
assert_equal(5, @context['test.last'])
|
||||
|
||||
@context['test'] = { 'test' => [1, 2, 3, 4, 5] }
|
||||
|
||||
assert_equal(1, @context['test.test.first'])
|
||||
assert_equal(5, @context['test.test.last'])
|
||||
|
||||
@context['test'] = [1]
|
||||
assert_equal(1, @context['test.first'])
|
||||
assert_equal(1, @context['test.last'])
|
||||
end
|
||||
|
||||
def test_access_hashes_with_hash_notation
|
||||
@context['products'] = { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] }
|
||||
@context['product'] = { 'variants' => [{ 'title' => 'draft151cm' }, { 'title' => 'element151cm' }] }
|
||||
|
||||
assert_equal(5, @context['products["count"]'])
|
||||
assert_equal('deepsnow', @context['products["tags"][0]'])
|
||||
assert_equal('deepsnow', @context['products["tags"].first'])
|
||||
assert_equal('draft151cm', @context['product["variants"][0]["title"]'])
|
||||
assert_equal('element151cm', @context['product["variants"][1]["title"]'])
|
||||
assert_equal('draft151cm', @context['product["variants"][0]["title"]'])
|
||||
assert_equal('element151cm', @context['product["variants"].last["title"]'])
|
||||
end
|
||||
|
||||
def test_access_variable_with_hash_notation
|
||||
@context['foo'] = 'baz'
|
||||
@context['bar'] = 'foo'
|
||||
|
||||
assert_equal('baz', @context['["foo"]'])
|
||||
assert_equal('baz', @context['[bar]'])
|
||||
end
|
||||
|
||||
def test_access_hashes_with_hash_access_variables
|
||||
@context['var'] = 'tags'
|
||||
@context['nested'] = { 'var' => 'tags' }
|
||||
@context['products'] = { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] }
|
||||
|
||||
assert_equal('deepsnow', @context['products[var].first'])
|
||||
assert_equal('freestyle', @context['products[nested.var].last'])
|
||||
end
|
||||
|
||||
def test_hash_notation_only_for_hash_access
|
||||
@context['array'] = [1, 2, 3, 4, 5]
|
||||
@context['hash'] = { 'first' => 'Hello' }
|
||||
|
||||
assert_equal(1, @context['array.first'])
|
||||
assert_nil(@context['array["first"]'])
|
||||
assert_equal('Hello', @context['hash["first"]'])
|
||||
end
|
||||
|
||||
def test_first_can_appear_in_middle_of_callchain
|
||||
@context['product'] = { 'variants' => [{ 'title' => 'draft151cm' }, { 'title' => 'element151cm' }] }
|
||||
|
||||
assert_equal('draft151cm', @context['product.variants[0].title'])
|
||||
assert_equal('element151cm', @context['product.variants[1].title'])
|
||||
assert_equal('draft151cm', @context['product.variants.first.title'])
|
||||
assert_equal('element151cm', @context['product.variants.last.title'])
|
||||
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
|
||||
@context.merge("test" => '5')
|
||||
assert_equal((1..5), @context['(1..5)'])
|
||||
assert_equal((1..5), @context['(1..test)'])
|
||||
assert_equal((5..5), @context['(test..test)'])
|
||||
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({}, {}, StaticRegisters.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 +605,30 @@ 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
|
||||
|
||||
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
|
||||
|
||||
@@ -49,10 +49,6 @@ class ProductDrop < Liquid::Drop
|
||||
ContextDrop.new
|
||||
end
|
||||
|
||||
def user_input
|
||||
(+"foo").taint
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def callmenot
|
||||
@@ -114,34 +110,6 @@ class DropsTest < Minitest::Test
|
||||
assert_equal(' ', tpl.render!('product' => ProductDrop.new))
|
||||
end
|
||||
|
||||
if taint_supported?
|
||||
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
|
||||
end
|
||||
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))
|
||||
@@ -199,7 +167,7 @@ class DropsTest < Minitest::Test
|
||||
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
|
||||
|
||||
@@ -242,28 +210,28 @@ class DropsTest < Minitest::Test
|
||||
|
||||
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))
|
||||
|
||||
[: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
|
||||
|
||||
@@ -281,7 +249,7 @@ class DropsTest < Minitest::Test
|
||||
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 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)
|
||||
|
||||
@@ -193,7 +193,7 @@ class ErrorHandlingTest < Minitest::Test
|
||||
|
||||
# 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
|
||||
@@ -261,4 +261,12 @@ class ErrorHandlingTest < Minitest::Test
|
||||
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,46 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class ExpressionTest < Minitest::Test
|
||||
def test_keyword_literals
|
||||
assert_equal(true, parse_and_eval("true"))
|
||||
assert_equal(true, parse_and_eval(" true "))
|
||||
end
|
||||
|
||||
def test_string
|
||||
assert_equal("single quoted", parse_and_eval("'single quoted'"))
|
||||
assert_equal("double quoted", parse_and_eval('"double quoted"'))
|
||||
assert_equal("spaced", parse_and_eval(" 'spaced' "))
|
||||
assert_equal("spaced2", parse_and_eval(' "spaced2" '))
|
||||
end
|
||||
|
||||
def test_int
|
||||
assert_equal(123, parse_and_eval("123"))
|
||||
assert_equal(456, parse_and_eval(" 456 "))
|
||||
assert_equal(12, parse_and_eval("012"))
|
||||
end
|
||||
|
||||
def test_float
|
||||
assert_equal(1.5, parse_and_eval("1.5"))
|
||||
assert_equal(2.5, parse_and_eval(" 2.5 "))
|
||||
end
|
||||
|
||||
def test_range
|
||||
assert_equal(1..2, parse_and_eval("(1..2)"))
|
||||
assert_equal(3..4, parse_and_eval(" ( 3 .. 4 ) "))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse_and_eval(markup, **assigns)
|
||||
if Liquid::Template.error_mode == :strict
|
||||
p = Liquid::Parser.new(markup)
|
||||
markup = p.expression
|
||||
p.consume(:end_of_string)
|
||||
end
|
||||
expression = Liquid::Expression.parse(markup)
|
||||
context = Liquid::Context.new(assigns)
|
||||
context.evaluate(expression)
|
||||
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
|
||||
@@ -153,6 +153,15 @@ class FiltersTest < Minitest::Test
|
||||
# tap still treated as a non-existent filter
|
||||
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
|
||||
|
||||
class FiltersInTemplate < Minitest::Test
|
||||
@@ -160,9 +169,9 @@ 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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -32,7 +32,7 @@ class ParsingQuirksTest < Minitest::Test
|
||||
assert(Template.parse("{{test}}"))
|
||||
|
||||
with_error_mode(:lax) do
|
||||
assert Template.parse("{{|test}}")
|
||||
assert(Template.parse("{{|test}}"))
|
||||
end
|
||||
|
||||
with_error_mode(:strict) do
|
||||
@@ -118,6 +118,16 @@ 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)
|
||||
end
|
||||
|
||||
@@ -2,7 +2,28 @@
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class RenderProfilingTest < Minitest::Test
|
||||
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
|
||||
@@ -62,6 +83,17 @@ class RenderProfilingTest < Minitest::Test
|
||||
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!
|
||||
@@ -77,6 +109,37 @@ class RenderProfilingTest < Minitest::Test
|
||||
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!
|
||||
@@ -91,7 +154,7 @@ class RenderProfilingTest < Minitest::Test
|
||||
|
||||
include_node = t.profiler[1]
|
||||
include_node.children.each do |child|
|
||||
assert_equal "a_template", child.partial
|
||||
assert_equal("a_template", child.partial)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -101,12 +164,12 @@ class RenderProfilingTest < Minitest::Test
|
||||
|
||||
a_template = t.profiler[1]
|
||||
a_template.children.each do |child|
|
||||
assert_equal "a_template", child.partial
|
||||
assert_equal("a_template", child.partial)
|
||||
end
|
||||
|
||||
b_template = t.profiler[2]
|
||||
b_template.children.each do |child|
|
||||
assert_equal "b_template", child.partial
|
||||
assert_equal("b_template", child.partial)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -116,12 +179,12 @@ class RenderProfilingTest < Minitest::Test
|
||||
|
||||
a_template1 = t.profiler[1]
|
||||
a_template1.children.each do |child|
|
||||
assert_equal "a_template", child.partial
|
||||
assert_equal("a_template", child.partial)
|
||||
end
|
||||
|
||||
a_template2 = t.profiler[2]
|
||||
a_template2.children.each do |child|
|
||||
assert_equal "a_template", child.partial
|
||||
assert_equal("a_template", child.partial)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -156,16 +219,22 @@ class RenderProfilingTest < Minitest::Test
|
||||
|
||||
def test_profiling_supports_self_time
|
||||
t = Template.parse("{% for item in collection %} {{ item }} {% endfor %}", profile: true)
|
||||
t.render!("collection" => ["one", "two"])
|
||||
leaf = t.profiler[0].children[0]
|
||||
collection = [
|
||||
TestDrop.new("one"),
|
||||
TestDrop.new("two"),
|
||||
]
|
||||
output = t.render!("collection" => collection)
|
||||
assert_equal(" one two ", output)
|
||||
|
||||
assert_operator(leaf.self_time, :>, 0)
|
||||
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 %} {% increment test %} {{ test }} {% endif %}", profile: true)
|
||||
t.render!
|
||||
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)
|
||||
assert_operator(t.profiler[0].total_time, :>, 0.0)
|
||||
end
|
||||
end
|
||||
@@ -1,27 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class DisabledTagsTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
class DisableRaw < Block
|
||||
disable_tags "raw"
|
||||
end
|
||||
|
||||
class DisableRawEcho < Block
|
||||
disable_tags "raw", "echo"
|
||||
end
|
||||
|
||||
def test_disables_raw
|
||||
with_custom_tag('disable', DisableRaw) do
|
||||
assert_template_result 'raw usage is not allowed in this contextfoo', '{% disable %}{% raw %}Foobar{% endraw %}{% echo "foo" %}{% enddisable %}'
|
||||
end
|
||||
end
|
||||
|
||||
def test_disables_echo_and_raw
|
||||
with_custom_tag('disable', DisableRawEcho) do
|
||||
assert_template_result 'raw usage is not allowed in this contextecho usage is not allowed in this context', '{% disable %}{% raw %}Foobar{% endraw %}{% echo "foo" %}{% enddisable %}'
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -43,15 +43,22 @@ class SecurityTest < Minitest::Test
|
||||
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))
|
||||
end
|
||||
|
||||
|
||||
@@ -3,10 +3,6 @@
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class Filters
|
||||
include Liquid::StandardFilters
|
||||
end
|
||||
|
||||
class TestThing
|
||||
attr_reader :foo
|
||||
|
||||
@@ -29,8 +25,24 @@ class TestThing
|
||||
end
|
||||
|
||||
class TestDrop < Liquid::Drop
|
||||
def test
|
||||
"testfoo"
|
||||
def initialize(value:)
|
||||
@value = value
|
||||
end
|
||||
|
||||
attr_reader :value
|
||||
|
||||
def registers
|
||||
@context.registers
|
||||
end
|
||||
end
|
||||
|
||||
class TestModel
|
||||
def initialize(value:)
|
||||
@value = value
|
||||
end
|
||||
|
||||
def to_liquid
|
||||
TestDrop.new(value: @value)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -53,10 +65,13 @@ class NumberLikeThing < Liquid::Drop
|
||||
end
|
||||
|
||||
class StandardFiltersTest < Minitest::Test
|
||||
Filters = Class.new(Liquid::StrainerTemplate)
|
||||
Filters.add_filter(Liquid::StandardFilters)
|
||||
|
||||
include Liquid
|
||||
|
||||
def setup
|
||||
@filters = Filters.new
|
||||
@filters = Filters.new(Context.new)
|
||||
end
|
||||
|
||||
def test_size
|
||||
@@ -145,6 +160,40 @@ class StandardFiltersTest < Minitest::Test
|
||||
assert_equal('<strong>Hulk</strong>', @filters.escape_once('<strong>Hulk</strong>'))
|
||||
end
|
||||
|
||||
def test_base64_encode
|
||||
assert_equal('b25lIHR3byB0aHJlZQ==', @filters.base64_encode('one two three'))
|
||||
assert_equal('', @filters.base64_encode(nil))
|
||||
end
|
||||
|
||||
def test_base64_decode
|
||||
assert_equal('one two three', @filters.base64_decode('b25lIHR3byB0aHJlZQ=='))
|
||||
|
||||
exception = assert_raises(Liquid::ArgumentError) do
|
||||
@filters.base64_decode("invalidbase64")
|
||||
end
|
||||
|
||||
assert_equal('Liquid error: invalid base64 provided to base64_decode', exception.message)
|
||||
end
|
||||
|
||||
def test_base64_url_safe_encode
|
||||
assert_equal(
|
||||
'YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXogQUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVogMTIzNDU2Nzg5MCAhQCMkJV4mKigpLT1fKy8_Ljo7W117fVx8',
|
||||
@filters.base64_url_safe_encode('abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890 !@#$%^&*()-=_+/?.:;[]{}\|')
|
||||
)
|
||||
assert_equal('', @filters.base64_url_safe_encode(nil))
|
||||
end
|
||||
|
||||
def test_base64_url_safe_decode
|
||||
assert_equal(
|
||||
'abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890 !@#$%^&*()-=_+/?.:;[]{}\|',
|
||||
@filters.base64_url_safe_decode('YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXogQUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVogMTIzNDU2Nzg5MCAhQCMkJV4mKigpLT1fKy8_Ljo7W117fVx8')
|
||||
)
|
||||
exception = assert_raises(Liquid::ArgumentError) do
|
||||
@filters.base64_url_safe_decode("invalidbase64")
|
||||
end
|
||||
assert_equal('Liquid error: invalid base64 provided to base64_url_safe_decode', exception.message)
|
||||
end
|
||||
|
||||
def test_url_encode
|
||||
assert_equal('foo%2B1%40example.com', @filters.url_encode('[email protected]'))
|
||||
assert_equal('1', @filters.url_encode(1))
|
||||
@@ -159,7 +208,7 @@ class StandardFiltersTest < Minitest::Test
|
||||
assert_equal('1', @filters.url_decode(1))
|
||||
assert_equal('2001-02-03', @filters.url_decode(Date.new(2001, 2, 3)))
|
||||
assert_nil(@filters.url_decode(nil))
|
||||
exception = assert_raises Liquid::ArgumentError do
|
||||
exception = assert_raises(Liquid::ArgumentError) do
|
||||
@filters.url_decode('%ff')
|
||||
end
|
||||
assert_equal('Liquid error: invalid byte sequence in UTF-8', exception.message)
|
||||
@@ -171,10 +220,17 @@ class StandardFiltersTest < Minitest::Test
|
||||
assert_equal('one two three', @filters.truncatewords('one two three'))
|
||||
assert_equal(
|
||||
'Two small (13” x 5.5” x 10” high) baskets fit inside one large basket (13”...',
|
||||
@filters.truncatewords('Two small (13” x 5.5” x 10” high) baskets fit inside one large basket (13” x 16” x 10.5” high) with cover.', 15)
|
||||
@filters.truncatewords('Two small (13” x 5.5” x 10” high) baskets fit inside one large basket (13” x 16” x 10.5” high) with cover.', 15)
|
||||
)
|
||||
assert_equal("测试测试测试测试", @filters.truncatewords('测试测试测试测试', 5))
|
||||
assert_equal('one two1', @filters.truncatewords("one two three", 2, 1))
|
||||
assert_equal('one two three...', @filters.truncatewords("one two\tthree\nfour", 3))
|
||||
assert_equal('one two...', @filters.truncatewords("one two three four", 2))
|
||||
assert_equal('one...', @filters.truncatewords("one two three four", 0))
|
||||
exception = assert_raises(Liquid::ArgumentError) do
|
||||
@filters.truncatewords("one two three four", 1 << 31)
|
||||
end
|
||||
assert_equal("Liquid error: integer #{1 << 31} too big for truncatewords", exception.message)
|
||||
end
|
||||
|
||||
def test_strip_html
|
||||
@@ -218,8 +274,8 @@ class StandardFiltersTest < Minitest::Test
|
||||
{ "price" => 1, "handle" => "gamma" },
|
||||
{ "price" => 2, "handle" => "epsilon" },
|
||||
{ "price" => 4, "handle" => "alpha" },
|
||||
{ "handle" => "delta" },
|
||||
{ "handle" => "beta" },
|
||||
{ "handle" => "delta" },
|
||||
]
|
||||
assert_equal(expectation, @filters.sort(input, "price"))
|
||||
end
|
||||
@@ -286,7 +342,7 @@ class StandardFiltersTest < Minitest::Test
|
||||
[3],
|
||||
]
|
||||
|
||||
assert_raises Liquid::ArgumentError do
|
||||
assert_raises(Liquid::ArgumentError) do
|
||||
@filters.sort(foo, "bar")
|
||||
end
|
||||
end
|
||||
@@ -302,7 +358,7 @@ class StandardFiltersTest < Minitest::Test
|
||||
[3],
|
||||
]
|
||||
|
||||
assert_raises Liquid::ArgumentError do
|
||||
assert_raises(Liquid::ArgumentError) do
|
||||
@filters.sort_natural(foo, "bar")
|
||||
end
|
||||
end
|
||||
@@ -322,8 +378,9 @@ class StandardFiltersTest < Minitest::Test
|
||||
assert_equal(["foo"], @filters.uniq("foo"))
|
||||
assert_equal([1, 3, 2, 4], @filters.uniq([1, 1, 3, 2, 3, 1, 4, 3, 2, 1]))
|
||||
assert_equal([{ "a" => 1 }, { "a" => 3 }, { "a" => 2 }], @filters.uniq([{ "a" => 1 }, { "a" => 3 }, { "a" => 1 }, { "a" => 2 }], "a"))
|
||||
testdrop = TestDrop.new
|
||||
assert_equal([testdrop], @filters.uniq([testdrop, TestDrop.new], 'test'))
|
||||
test_drop = TestDrop.new(value: "test")
|
||||
test_drop_alternate = TestDrop.new(value: "test")
|
||||
assert_equal([test_drop], @filters.uniq([test_drop, test_drop_alternate], 'value'))
|
||||
end
|
||||
|
||||
def test_uniq_empty_array
|
||||
@@ -337,7 +394,7 @@ class StandardFiltersTest < Minitest::Test
|
||||
[3],
|
||||
]
|
||||
|
||||
assert_raises Liquid::ArgumentError do
|
||||
assert_raises(Liquid::ArgumentError) do
|
||||
@filters.uniq(foo, "bar")
|
||||
end
|
||||
end
|
||||
@@ -353,7 +410,7 @@ class StandardFiltersTest < Minitest::Test
|
||||
[3],
|
||||
]
|
||||
|
||||
assert_raises Liquid::ArgumentError do
|
||||
assert_raises(Liquid::ArgumentError) do
|
||||
@filters.compact(foo, "bar")
|
||||
end
|
||||
end
|
||||
@@ -382,6 +439,16 @@ class StandardFiltersTest < Minitest::Test
|
||||
assert_template_result("woot: 1", '{{ foo | map: "whatever" }}', "foo" => [t])
|
||||
end
|
||||
|
||||
def test_map_calls_context=
|
||||
model = TestModel.new(value: "test")
|
||||
|
||||
template = Template.parse('{{ foo | map: "registers" }}')
|
||||
template.registers[:test] = 1234
|
||||
template.assigns['foo'] = [model]
|
||||
|
||||
assert_template_result("{:test=>1234}", template.render!)
|
||||
end
|
||||
|
||||
def test_map_on_hashes
|
||||
assert_template_result("4217", '{{ thing | map: "foo" | map: "bar" }}',
|
||||
"thing" => { "foo" => [{ "bar" => 42 }, { "bar" => 17 }] })
|
||||
@@ -400,9 +467,9 @@ class StandardFiltersTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_map_over_proc
|
||||
drop = TestDrop.new
|
||||
drop = TestDrop.new(value: "testfoo")
|
||||
p = proc { drop }
|
||||
templ = '{{ procs | map: "test" }}'
|
||||
templ = '{{ procs | map: "value" }}'
|
||||
assert_template_result("testfoo", templ, "procs" => [p])
|
||||
end
|
||||
|
||||
@@ -430,7 +497,7 @@ class StandardFiltersTest < Minitest::Test
|
||||
[3],
|
||||
]
|
||||
|
||||
assert_raises Liquid::ArgumentError do
|
||||
assert_raises(Liquid::ArgumentError) do
|
||||
@filters.map(foo, "bar")
|
||||
end
|
||||
end
|
||||
@@ -441,7 +508,7 @@ class StandardFiltersTest < Minitest::Test
|
||||
[2],
|
||||
[3],
|
||||
]
|
||||
assert_raises Liquid::ArgumentError do
|
||||
assert_raises(Liquid::ArgumentError) do
|
||||
@filters.map(foo, nil)
|
||||
end
|
||||
end
|
||||
@@ -485,8 +552,8 @@ class StandardFiltersTest < Minitest::Test
|
||||
assert_equal('', @filters.date('', "%B"))
|
||||
|
||||
with_timezone("UTC") do
|
||||
assert_equal "07/05/2006", @filters.date(1152098955, "%m/%d/%Y")
|
||||
assert_equal "07/05/2006", @filters.date("1152098955", "%m/%d/%Y")
|
||||
assert_equal("07/05/2006", @filters.date(1152098955, "%m/%d/%Y"))
|
||||
assert_equal("07/05/2006", @filters.date("1152098955", "%m/%d/%Y"))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -498,19 +565,31 @@ class StandardFiltersTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_replace
|
||||
assert_equal('2 2 2 2', @filters.replace('1 1 1 1', '1', 2))
|
||||
assert_equal('b b b b', @filters.replace('a a a a', 'a', 'b'))
|
||||
assert_equal('2 2 2 2', @filters.replace('1 1 1 1', 1, 2))
|
||||
assert_equal('2 1 1 1', @filters.replace_first('1 1 1 1', '1', 2))
|
||||
assert_equal('1 1 1 1', @filters.replace('1 1 1 1', 2, 3))
|
||||
assert_template_result('2 2 2 2', "{{ '1 1 1 1' | replace: '1', 2 }}")
|
||||
|
||||
assert_equal('b a a a', @filters.replace_first('a a a a', 'a', 'b'))
|
||||
assert_equal('2 1 1 1', @filters.replace_first('1 1 1 1', 1, 2))
|
||||
assert_equal('1 1 1 1', @filters.replace_first('1 1 1 1', 2, 3))
|
||||
assert_template_result('2 1 1 1', "{{ '1 1 1 1' | replace_first: '1', 2 }}")
|
||||
|
||||
assert_equal('a a a b', @filters.replace_last('a a a a', 'a', 'b'))
|
||||
assert_equal('1 1 1 2', @filters.replace_last('1 1 1 1', 1, 2))
|
||||
assert_equal('1 1 1 1', @filters.replace_last('1 1 1 1', 2, 3))
|
||||
assert_template_result('1 1 1 2', "{{ '1 1 1 1' | replace_last: '1', 2 }}")
|
||||
end
|
||||
|
||||
def test_remove
|
||||
assert_equal(' ', @filters.remove("a a a a", 'a'))
|
||||
assert_equal(' ', @filters.remove("1 1 1 1", 1))
|
||||
assert_equal('a a a', @filters.remove_first("a a a a", 'a '))
|
||||
assert_equal(' 1 1 1', @filters.remove_first("1 1 1 1", 1))
|
||||
assert_template_result('a a a', "{{ 'a a a a' | remove_first: 'a ' }}")
|
||||
assert_template_result(' ', "{{ '1 1 1 1' | remove: 1 }}")
|
||||
|
||||
assert_equal('b a a', @filters.remove_first("a b a a", 'a '))
|
||||
assert_template_result(' 1 1 1', "{{ '1 1 1 1' | remove_first: 1 }}")
|
||||
|
||||
assert_equal('a a b', @filters.remove_last("a a b a", ' a'))
|
||||
assert_template_result('1 1 1 ', "{{ '1 1 1 1' | remove_last: 1 }}")
|
||||
end
|
||||
|
||||
def test_pipes_in_string_arguments
|
||||
@@ -539,6 +618,7 @@ class StandardFiltersTest < Minitest::Test
|
||||
|
||||
def test_newlines_to_br
|
||||
assert_template_result("a<br />\nb<br />\nc", "{{ source | newline_to_br }}", 'source' => "a\nb\nc")
|
||||
assert_template_result("a<br />\nb<br />\nc", "{{ source | newline_to_br }}", 'source' => "a\r\nb\nc")
|
||||
end
|
||||
|
||||
def test_plus
|
||||
@@ -587,7 +667,7 @@ class StandardFiltersTest < Minitest::Test
|
||||
|
||||
assert_template_result("0.5", "{{ 2.0 | divided_by:4 }}")
|
||||
assert_raises(Liquid::ZeroDivisionError) do
|
||||
assert_template_result "4", "{{ 1 | modulo: 0 }}"
|
||||
assert_template_result("4", "{{ 1 | modulo: 0 }}")
|
||||
end
|
||||
|
||||
assert_template_result("5", "{{ price | divided_by:2 }}", 'price' => NumberLikeThing.new(10))
|
||||
@@ -596,7 +676,7 @@ class StandardFiltersTest < Minitest::Test
|
||||
def test_modulo
|
||||
assert_template_result("1", "{{ 3 | modulo:2 }}")
|
||||
assert_raises(Liquid::ZeroDivisionError) do
|
||||
assert_template_result "4", "{{ 1 | modulo: 0 }}"
|
||||
assert_template_result("4", "{{ 1 | modulo: 0 }}")
|
||||
end
|
||||
|
||||
assert_template_result("1", "{{ price | modulo:2 }}", 'price' => NumberLikeThing.new(3))
|
||||
@@ -607,7 +687,7 @@ class StandardFiltersTest < Minitest::Test
|
||||
assert_template_result("4", "{{ '4.3' | round }}")
|
||||
assert_template_result("4.56", "{{ input | round: 2 }}", 'input' => 4.5612)
|
||||
assert_raises(Liquid::FloatDomainError) do
|
||||
assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | round }}"
|
||||
assert_template_result("4", "{{ 1.0 | divided_by: 0.0 | round }}")
|
||||
end
|
||||
|
||||
assert_template_result("5", "{{ price | round }}", 'price' => NumberLikeThing.new(4.6))
|
||||
@@ -618,7 +698,7 @@ class StandardFiltersTest < Minitest::Test
|
||||
assert_template_result("5", "{{ input | ceil }}", 'input' => 4.6)
|
||||
assert_template_result("5", "{{ '4.3' | ceil }}")
|
||||
assert_raises(Liquid::FloatDomainError) do
|
||||
assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | ceil }}"
|
||||
assert_template_result("4", "{{ 1.0 | divided_by: 0.0 | ceil }}")
|
||||
end
|
||||
|
||||
assert_template_result("5", "{{ price | ceil }}", 'price' => NumberLikeThing.new(4.6))
|
||||
@@ -628,7 +708,7 @@ class StandardFiltersTest < Minitest::Test
|
||||
assert_template_result("4", "{{ input | floor }}", 'input' => 4.6)
|
||||
assert_template_result("4", "{{ '4.3' | floor }}")
|
||||
assert_raises(Liquid::FloatDomainError) do
|
||||
assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | floor }}"
|
||||
assert_template_result("4", "{{ 1.0 | divided_by: 0.0 | floor }}")
|
||||
end
|
||||
|
||||
assert_template_result("5", "{{ price | floor }}", 'price' => NumberLikeThing.new(5.4))
|
||||
@@ -686,6 +766,8 @@ class StandardFiltersTest < Minitest::Test
|
||||
assert_equal("bar", @filters.default([], "bar"))
|
||||
assert_equal("bar", @filters.default({}, "bar"))
|
||||
assert_template_result('bar', "{{ false | default: 'bar' }}")
|
||||
assert_template_result('bar', "{{ drop | default: 'bar' }}", 'drop' => BooleanDrop.new(false))
|
||||
assert_template_result('Yay', "{{ drop | default: 'bar' }}", 'drop' => BooleanDrop.new(true))
|
||||
end
|
||||
|
||||
def test_default_handle_false
|
||||
@@ -696,6 +778,8 @@ class StandardFiltersTest < Minitest::Test
|
||||
assert_equal("bar", @filters.default([], "bar", "allow_false" => true))
|
||||
assert_equal("bar", @filters.default({}, "bar", "allow_false" => true))
|
||||
assert_template_result('false', "{{ false | default: 'bar', allow_false: true }}")
|
||||
assert_template_result('Nay', "{{ drop | default: 'bar', allow_false: true }}", 'drop' => BooleanDrop.new(false))
|
||||
assert_template_result('Yay', "{{ drop | default: 'bar', allow_false: true }}", 'drop' => BooleanDrop.new(true))
|
||||
end
|
||||
|
||||
def test_cannot_access_private_methods
|
||||
@@ -724,6 +808,18 @@ class StandardFiltersTest < Minitest::Test
|
||||
assert_equal(expectation, @filters.where(input, "ok"))
|
||||
end
|
||||
|
||||
def test_where_string_keys
|
||||
input = [
|
||||
"alpha", "beta", "gamma", "delta"
|
||||
]
|
||||
|
||||
expectation = [
|
||||
"beta",
|
||||
]
|
||||
|
||||
assert_equal(expectation, @filters.where(input, "be"))
|
||||
end
|
||||
|
||||
def test_where_no_key_set
|
||||
input = [
|
||||
{ "handle" => "alpha", "ok" => true },
|
||||
@@ -769,7 +865,7 @@ class StandardFiltersTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_all_filters_never_raise_non_liquid_exception
|
||||
test_drop = TestDrop.new
|
||||
test_drop = TestDrop.new(value: "test")
|
||||
test_drop.context = Context.new
|
||||
test_enum = TestEnumerable.new
|
||||
test_enum.context = Context.new
|
||||
@@ -794,19 +890,14 @@ class StandardFiltersTest < Minitest::Test
|
||||
{ 1 => "bar" },
|
||||
["foo", 123, nil, true, false, Drop, ["foo"], { foo: "bar" }],
|
||||
]
|
||||
test_types.each do |first|
|
||||
test_types.each do |other|
|
||||
(@filters.methods - Object.methods).each do |method|
|
||||
arg_count = @filters.method(method).arity
|
||||
arg_count *= -1 if arg_count < 0
|
||||
inputs = [first]
|
||||
inputs << ([other] * (arg_count - 1)) if arg_count > 1
|
||||
begin
|
||||
@filters.send(method, *inputs)
|
||||
rescue Liquid::ArgumentError, Liquid::ZeroDivisionError
|
||||
nil
|
||||
end
|
||||
end
|
||||
StandardFilters.public_instance_methods(false).each do |method|
|
||||
arg_count = @filters.method(method).arity
|
||||
arg_count *= -1 if arg_count < 0
|
||||
|
||||
test_types.repeated_permutation(arg_count) do |args|
|
||||
@filters.send(method, *args)
|
||||
rescue Liquid::Error
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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
|
||||
@@ -437,4 +437,30 @@ HERE
|
||||
|
||||
assert(context.registers[:for_stack].empty?)
|
||||
end
|
||||
|
||||
def test_instrument_for_offset_continue
|
||||
assert_usage_increment('for_offset_continue') do
|
||||
Template.parse('{% for item in items offset:continue %}{{item}}{% endfor %}')
|
||||
end
|
||||
|
||||
assert_usage_increment('for_offset_continue', times: 0) do
|
||||
Template.parse('{% for item in items offset:2 %}{{item}}{% endfor %}')
|
||||
end
|
||||
end
|
||||
|
||||
def test_instrument_forloop_drop_name
|
||||
assigns = { 'items' => [1, 2, 3, 4, 5] }
|
||||
|
||||
assert_usage_increment('forloop_drop_name', times: 5) do
|
||||
Template.parse('{% for item in items %}{{forloop.name}}{% endfor %}').render!(assigns)
|
||||
end
|
||||
|
||||
assert_usage_increment('forloop_drop_name', times: 0) do
|
||||
Template.parse('{% for item in items %}{{forloop.index}}{% endfor %}').render!(assigns)
|
||||
end
|
||||
|
||||
assert_usage_increment('forloop_drop_name', times: 0) do
|
||||
Template.parse('{% for item in items %}{{item}}{% endfor %}').render!(assigns)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -184,7 +184,7 @@ class IfElseTagTest < Minitest::Test
|
||||
tests.each do |vals, expected|
|
||||
a, b, c = vals
|
||||
assigns = { 'a' => a, 'b' => b, 'c' => c }
|
||||
assert_template_result expected.to_s, tpl, assigns, assigns.to_s
|
||||
assert_template_result(expected.to_s, tpl, assigns, assigns.to_s)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -96,12 +96,12 @@ class IncludeTagTest < Minitest::Test
|
||||
|
||||
def test_include_tag_with_alias
|
||||
assert_template_result("Product: Draft 151cm ",
|
||||
"{% include 'product_alias' with products[0] as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
|
||||
"{% include 'product_alias' with products[0] as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
|
||||
end
|
||||
|
||||
def test_include_tag_for_alias
|
||||
assert_template_result("Product: Draft 151cm Product: Element 155cm ",
|
||||
"{% include 'product_alias' for products as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
|
||||
"{% include 'product_alias' for products as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
|
||||
end
|
||||
|
||||
def test_include_tag_with_default_name
|
||||
@@ -226,13 +226,13 @@ class IncludeTagTest < Minitest::Test
|
||||
Template.parse("{% include template %}", error_mode: :strict).render!("template" => '{{ "X" || downcase }}')
|
||||
end
|
||||
with_error_mode(:lax) do
|
||||
assert_equal 'x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: true).render!("template" => '{{ "X" || downcase }}')
|
||||
assert_equal('x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: true).render!("template" => '{{ "X" || downcase }}'))
|
||||
end
|
||||
assert_raises(Liquid::SyntaxError) do
|
||||
Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:locale]).render!("template" => '{{ "X" || downcase }}')
|
||||
end
|
||||
with_error_mode(:lax) do
|
||||
assert_equal 'x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:error_mode]).render!("template" => '{{ "X" || downcase }}')
|
||||
assert_equal('x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:error_mode]).render!("template" => '{{ "X" || downcase }}'))
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -82,15 +82,13 @@ class LiquidTagTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_nested_liquid_tag
|
||||
assert_usage_increment("liquid_tag_contains_outer_tag", times: 0) do
|
||||
assert_template_result('good', <<~LIQUID)
|
||||
{%- if true %}
|
||||
{%- liquid
|
||||
echo "good"
|
||||
%}
|
||||
{%- endif -%}
|
||||
LIQUID
|
||||
end
|
||||
assert_template_result('good', <<~LIQUID)
|
||||
{%- if true %}
|
||||
{%- liquid
|
||||
echo "good"
|
||||
%}
|
||||
{%- endif -%}
|
||||
LIQUID
|
||||
end
|
||||
|
||||
def test_cannot_open_blocks_living_past_a_liquid_tag
|
||||
@@ -102,14 +100,12 @@ class LiquidTagTest < Minitest::Test
|
||||
LIQUID
|
||||
end
|
||||
|
||||
def test_quirk_can_close_blocks_created_before_a_liquid_tag
|
||||
assert_usage_increment("liquid_tag_contains_outer_tag") do
|
||||
assert_template_result("42", <<~LIQUID)
|
||||
{%- if true -%}
|
||||
42
|
||||
{%- liquid endif -%}
|
||||
LIQUID
|
||||
end
|
||||
def test_cannot_close_blocks_created_before_a_liquid_tag
|
||||
assert_match_syntax_error("syntax error (line 3): 'endif' is not a valid delimiter for liquid tags. use %}", <<~LIQUID)
|
||||
{%- if true -%}
|
||||
42
|
||||
{%- liquid endif -%}
|
||||
LIQUID
|
||||
end
|
||||
|
||||
def test_liquid_tag_in_raw
|
||||
|
||||
@@ -23,6 +23,7 @@ class RawTagTest < Minitest::Test
|
||||
assert_template_result(' Foobar {% {% {% ', '{% raw %} Foobar {% {% {% {% endraw %}')
|
||||
assert_template_result(' test {% raw %} {% endraw %}', '{% raw %} test {% raw %} {% {% endraw %}endraw %}')
|
||||
assert_template_result(' Foobar {{ invalid 1', '{% raw %} Foobar {{ invalid {% endraw %}{{ 1 }}')
|
||||
assert_template_result(' Foobar {% foo {% bar %}', '{% raw %} Foobar {% foo {% bar %}{% endraw %}')
|
||||
end
|
||||
|
||||
def test_invalid_raw
|
||||
|
||||
@@ -42,34 +42,6 @@ class RenderTagTest < Minitest::Test
|
||||
assert_template_result('', "{% assign snippet = 'should not be visible' %}{% render 'snippet' %}")
|
||||
end
|
||||
|
||||
if taint_supported?
|
||||
def test_render_sets_the_correct_template_name_for_errors
|
||||
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ unsafe }}')
|
||||
|
||||
with_taint_mode :error do
|
||||
template = Liquid::Template.parse('{% render "snippet", unsafe: unsafe %}')
|
||||
context = Context.new('unsafe' => (+'unsafe').tap(&:taint))
|
||||
template.render(context)
|
||||
|
||||
assert_equal [Liquid::TaintedError], template.errors.map(&:class)
|
||||
assert_equal 'snippet', template.errors.first.template_name
|
||||
end
|
||||
end
|
||||
|
||||
def test_render_sets_the_correct_template_name_for_warnings
|
||||
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ unsafe }}')
|
||||
|
||||
with_taint_mode :warn do
|
||||
template = Liquid::Template.parse('{% render "snippet", unsafe: unsafe %}')
|
||||
context = Context.new('unsafe' => (+'unsafe').tap(&:taint))
|
||||
template.render(context)
|
||||
|
||||
assert_equal [Liquid::TaintedError], context.warnings.map(&:class)
|
||||
assert_equal 'snippet', context.warnings.first.template_name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_render_does_not_mutate_parent_scope
|
||||
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{% assign inner = 1 %}')
|
||||
assert_template_result('', "{% render 'snippet' %}{{ inner }}")
|
||||
@@ -86,7 +58,7 @@ class RenderTagTest < Minitest::Test
|
||||
def test_recursively_rendered_template_does_not_produce_endless_loop
|
||||
Liquid::Template.file_system = StubFileSystem.new('loop' => '{% render "loop" %}')
|
||||
|
||||
assert_raises Liquid::StackLevelError do
|
||||
assert_raises(Liquid::StackLevelError) do
|
||||
Template.parse('{% render "loop" %}').render!
|
||||
end
|
||||
end
|
||||
@@ -95,7 +67,7 @@ class RenderTagTest < Minitest::Test
|
||||
Liquid::Template.file_system = StubFileSystem.new(
|
||||
'loop_render' => '{% render "loop_render" %}',
|
||||
)
|
||||
assert_raises Liquid::StackLevelError do
|
||||
assert_raises(Liquid::StackLevelError) do
|
||||
Template.parse('{% render "loop_render" %}').render!
|
||||
end
|
||||
end
|
||||
@@ -103,7 +75,7 @@ class RenderTagTest < Minitest::Test
|
||||
def test_dynamically_choosen_templates_are_not_allowed
|
||||
Liquid::Template.file_system = StubFileSystem.new('snippet' => 'should not be rendered')
|
||||
|
||||
assert_raises Liquid::SyntaxError do
|
||||
assert_raises(Liquid::SyntaxError) do
|
||||
Liquid::Template.parse("{% assign name = 'snippet' %}{% render name %}")
|
||||
end
|
||||
end
|
||||
@@ -155,7 +127,10 @@ class RenderTagTest < Minitest::Test
|
||||
'test_include' => '{% include "foo" %}'
|
||||
)
|
||||
|
||||
assert_template_result('include usage is not allowed in this context', '{% render "test_include" %}')
|
||||
exc = assert_raises(Liquid::DisabledError) do
|
||||
Liquid::Template.parse('{% render "test_include" %}').render!
|
||||
end
|
||||
assert_equal('Liquid error: include usage is not allowed in this context', exc.message)
|
||||
end
|
||||
|
||||
def test_includes_will_not_render_inside_nested_sibling_tags
|
||||
@@ -165,7 +140,8 @@ class RenderTagTest < Minitest::Test
|
||||
'test_include' => '{% include "foo" %}'
|
||||
)
|
||||
|
||||
assert_template_result('include usage is not allowed in this contextinclude usage is not allowed in this context', '{% render "nested_render_with_sibling_include" %}')
|
||||
output = Liquid::Template.parse('{% render "nested_render_with_sibling_include" %}').render
|
||||
assert_equal('Liquid error: include usage is not allowed in this contextLiquid error: include usage is not allowed in this context', output)
|
||||
end
|
||||
|
||||
def test_render_tag_with
|
||||
@@ -175,7 +151,7 @@ class RenderTagTest < Minitest::Test
|
||||
)
|
||||
|
||||
assert_template_result("Product: Draft 151cm ",
|
||||
"{% render 'product' with products[0] %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
|
||||
"{% render 'product' with products[0] %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
|
||||
end
|
||||
|
||||
def test_render_tag_with_alias
|
||||
@@ -185,7 +161,7 @@ class RenderTagTest < Minitest::Test
|
||||
)
|
||||
|
||||
assert_template_result("Product: Draft 151cm ",
|
||||
"{% render 'product_alias' with products[0] as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
|
||||
"{% render 'product_alias' with products[0] as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
|
||||
end
|
||||
|
||||
def test_render_tag_for_alias
|
||||
@@ -195,7 +171,7 @@ class RenderTagTest < Minitest::Test
|
||||
)
|
||||
|
||||
assert_template_result("Product: Draft 151cm Product: Element 155cm ",
|
||||
"{% render 'product_alias' for products as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
|
||||
"{% render 'product_alias' for products as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
|
||||
end
|
||||
|
||||
def test_render_tag_for
|
||||
@@ -205,7 +181,7 @@ class RenderTagTest < Minitest::Test
|
||||
)
|
||||
|
||||
assert_template_result("Product: Draft 151cm Product: Element 155cm ",
|
||||
"{% render 'product' for products %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
|
||||
"{% render 'product' for products %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
|
||||
end
|
||||
|
||||
def test_render_tag_forloop
|
||||
@@ -214,7 +190,7 @@ class RenderTagTest < Minitest::Test
|
||||
)
|
||||
|
||||
assert_template_result("Product: Draft 151cm first index:1 Product: Element 155cm last index:2 ",
|
||||
"{% render 'product' for products %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
|
||||
"{% render 'product' for products %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
|
||||
end
|
||||
|
||||
def test_render_tag_for_drop
|
||||
@@ -223,7 +199,7 @@ class RenderTagTest < Minitest::Test
|
||||
)
|
||||
|
||||
assert_template_result("123",
|
||||
"{% render 'loop' for loop as value %}", "loop" => TestEnumerable.new)
|
||||
"{% render 'loop' for loop as value %}", "loop" => TestEnumerable.new)
|
||||
end
|
||||
|
||||
def test_render_tag_with_drop
|
||||
@@ -232,6 +208,6 @@ class RenderTagTest < Minitest::Test
|
||||
)
|
||||
|
||||
assert_template_result("TestEnumerable",
|
||||
"{% render 'loop' with loop as value %}", "loop" => TestEnumerable.new)
|
||||
"{% render 'loop' with loop as value %}", "loop" => TestEnumerable.new)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -213,6 +213,11 @@ class StandardTagTest < Minitest::Test
|
||||
assert_template_result('', code, 'condition' => 'something else')
|
||||
end
|
||||
|
||||
def test_case_when_comma_and_blank_body
|
||||
code = '{% case condition %}{% when 1, 2 %} {% assign r = "result" %} {% endcase %}{{ r }}'
|
||||
assert_template_result('result', code, 'condition' => 2)
|
||||
end
|
||||
|
||||
def test_assign
|
||||
assert_template_result('variable', '{% assign a = "variable"%}{{a}}')
|
||||
end
|
||||
|
||||
@@ -111,13 +111,12 @@ class TemplateTest < Minitest::Test
|
||||
|
||||
def test_resource_limits_render_length
|
||||
t = Template.parse("0123456789")
|
||||
t.resource_limits.render_length_limit = 5
|
||||
t.resource_limits.render_length_limit = 9
|
||||
assert_equal("Liquid error: Memory limits exceeded", t.render)
|
||||
assert(t.resource_limits.reached?)
|
||||
|
||||
t.resource_limits.render_length_limit = 10
|
||||
assert_equal("0123456789", t.render!)
|
||||
refute_nil(t.resource_limits.render_length)
|
||||
end
|
||||
|
||||
def test_resource_limits_render_score
|
||||
@@ -136,38 +135,6 @@ class TemplateTest < Minitest::Test
|
||||
refute_nil(t.resource_limits.render_score)
|
||||
end
|
||||
|
||||
def test_resource_limits_assign_score
|
||||
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_resource_limits_assign_score_counts_bytes_not_characters
|
||||
t = Template.parse("{% assign foo = 'すごい' %}")
|
||||
t.render
|
||||
assert_equal(9, t.resource_limits.assign_score)
|
||||
|
||||
t = Template.parse("{% capture foo %}すごい{% endcapture %}")
|
||||
t.render
|
||||
assert_equal(9, t.resource_limits.assign_score)
|
||||
end
|
||||
|
||||
def test_resource_limits_assign_score_nested
|
||||
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_resource_limits_aborts_rendering_after_first_error
|
||||
t = Template.parse("{% for a in (1..100) %} foo1 {% endfor %} bar {% for a in (1..100) %} foo2 {% endfor %}")
|
||||
t.resource_limits.render_score_limit = 50
|
||||
@@ -176,50 +143,46 @@ class TemplateTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_resource_limits_hash_in_template_gets_updated_even_if_no_limits_are_set
|
||||
t = Template.parse("{% for a in (1..100) %} {% assign foo = 1 %} {% endfor %}")
|
||||
t = Template.parse("{% for a in (1..100) %}x{% assign foo = 1 %} {% endfor %}")
|
||||
t.render!
|
||||
assert(t.resource_limits.assign_score > 0)
|
||||
assert(t.resource_limits.render_score > 0)
|
||||
assert(t.resource_limits.render_length > 0)
|
||||
end
|
||||
|
||||
def test_render_length_persists_between_blocks
|
||||
t = Template.parse("{% if true %}aaaa{% endif %}")
|
||||
t.resource_limits.render_length_limit = 7
|
||||
t.resource_limits.render_length_limit = 3
|
||||
assert_equal("Liquid error: Memory limits exceeded", t.render)
|
||||
t.resource_limits.render_length_limit = 8
|
||||
t.resource_limits.render_length_limit = 4
|
||||
assert_equal("aaaa", t.render)
|
||||
|
||||
t = Template.parse("{% if true %}aaaa{% endif %}{% if true %}bbb{% endif %}")
|
||||
t.resource_limits.render_length_limit = 13
|
||||
t.resource_limits.render_length_limit = 6
|
||||
assert_equal("Liquid error: Memory limits exceeded", t.render)
|
||||
t.resource_limits.render_length_limit = 14
|
||||
t.resource_limits.render_length_limit = 7
|
||||
assert_equal("aaaabbb", t.render)
|
||||
|
||||
t = Template.parse("{% if true %}a{% endif %}{% if true %}b{% endif %}{% if true %}a{% endif %}{% if true %}b{% endif %}{% if true %}a{% endif %}{% if true %}b{% endif %}")
|
||||
t.resource_limits.render_length_limit = 5
|
||||
assert_equal("Liquid error: Memory limits exceeded", t.render)
|
||||
t.resource_limits.render_length_limit = 11
|
||||
assert_equal("Liquid error: Memory limits exceeded", t.render)
|
||||
t.resource_limits.render_length_limit = 12
|
||||
t.resource_limits.render_length_limit = 6
|
||||
assert_equal("ababab", t.render)
|
||||
end
|
||||
|
||||
def test_render_length_uses_number_of_bytes_not_characters
|
||||
t = Template.parse("{% if true %}すごい{% endif %}")
|
||||
t.resource_limits.render_length_limit = 10
|
||||
t.resource_limits.render_length_limit = 8
|
||||
assert_equal("Liquid error: Memory limits exceeded", t.render)
|
||||
t.resource_limits.render_length_limit = 18
|
||||
t.resource_limits.render_length_limit = 9
|
||||
assert_equal("すごい", t.render)
|
||||
end
|
||||
|
||||
def test_default_resource_limits_unaffected_by_render_with_context
|
||||
context = Context.new
|
||||
t = Template.parse("{% for a in (1..100) %} {% assign foo = 1 %} {% endfor %}")
|
||||
t = Template.parse("{% for a in (1..100) %}x{% assign foo = 1 %} {% endfor %}")
|
||||
t.render!(context)
|
||||
assert(context.resource_limits.assign_score > 0)
|
||||
assert(context.resource_limits.render_score > 0)
|
||||
assert(context.resource_limits.render_length > 0)
|
||||
end
|
||||
|
||||
def test_can_use_drop_as_context
|
||||
@@ -235,7 +198,7 @@ class TemplateTest < Minitest::Test
|
||||
context = Context.new('drop' => ErroneousDrop.new)
|
||||
t = Template.new.parse('{{ drop.bad_method }}')
|
||||
|
||||
e = assert_raises RuntimeError do
|
||||
e = assert_raises(RuntimeError) do
|
||||
t.render!(context)
|
||||
end
|
||||
assert_equal('ruby error in drop', e.message)
|
||||
@@ -294,9 +257,8 @@ class TemplateTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_nil_value_does_not_raise
|
||||
Liquid::Template.error_mode = :strict
|
||||
t = Template.parse("some{{x}}thing")
|
||||
result = t.render!({ 'x' => nil }, strict_variables: true)
|
||||
t = Template.parse("some{{x}}thing", error_mode: :strict)
|
||||
result = t.render!({ 'x' => nil }, strict_variables: true)
|
||||
|
||||
assert_equal(0, t.errors.count)
|
||||
assert_equal('something', result)
|
||||
@@ -305,7 +267,7 @@ class TemplateTest < Minitest::Test
|
||||
def test_undefined_variables_raise
|
||||
t = Template.parse("{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}")
|
||||
|
||||
assert_raises UndefinedVariable do
|
||||
assert_raises(UndefinedVariable) do
|
||||
t.render!({ 'x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } } }, strict_variables: true)
|
||||
end
|
||||
end
|
||||
@@ -324,7 +286,7 @@ class TemplateTest < Minitest::Test
|
||||
d = DropWithUndefinedMethod.new
|
||||
t = Template.new.parse('{{ foo }} {{ woot }}')
|
||||
|
||||
assert_raises UndefinedDropMethod do
|
||||
assert_raises(UndefinedDropMethod) do
|
||||
t.render!(d, strict_variables: true)
|
||||
end
|
||||
end
|
||||
@@ -347,7 +309,7 @@ class TemplateTest < Minitest::Test
|
||||
def test_undefined_filters_raise
|
||||
t = Template.parse("{{x | somefilter1 | upcase | somefilter2}}")
|
||||
|
||||
assert_raises UndefinedFilter do
|
||||
assert_raises(UndefinedFilter) do
|
||||
t.render!({ 'x' => 'foo' }, strict_filters: true)
|
||||
end
|
||||
end
|
||||
@@ -362,11 +324,17 @@ class TemplateTest < Minitest::Test
|
||||
assert_equal('12345', result)
|
||||
end
|
||||
|
||||
unless taint_supported?
|
||||
def test_taint_mode
|
||||
assert_raises(NotImplementedError) do
|
||||
Template.taint_mode = :warn
|
||||
def test_source_string_subclass
|
||||
string_subclass = Class.new(String) do
|
||||
# E.g. ActiveSupport::SafeBuffer does this, so don't just rely on to_s to return a String
|
||||
def to_s
|
||||
self
|
||||
end
|
||||
end
|
||||
source = string_subclass.new("{% assign x = 2 -%} x= {{- x }}")
|
||||
assert_instance_of(string_subclass, source)
|
||||
output = Template.parse(source).render!
|
||||
assert_equal("x=2", output)
|
||||
assert_instance_of(String, output)
|
||||
end
|
||||
end
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user