mirror of
https://github.com/kemko/liquid.git
synced 2026-01-01 15:55:40 +03:00
Compare commits
251 Commits
rubocop-up
...
default-fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac66dbbafe | ||
|
|
250555c9a8 | ||
|
|
e361a4d53c | ||
|
|
b9e0d28729 | ||
|
|
020f6b93c5 | ||
|
|
cfe1637bdd | ||
|
|
eab13a07d9 | ||
|
|
ca96ca0fef | ||
|
|
4e7a953e73 | ||
|
|
6ac2499f7f | ||
|
|
ff70161512 | ||
|
|
026157e128 | ||
|
|
bf64239ea6 | ||
|
|
c270a6f378 | ||
|
|
4fba61a802 | ||
|
|
6b6baece25 | ||
|
|
15b2d193ec | ||
|
|
fd712d134a | ||
|
|
0c2db998cf | ||
|
|
9dac68cce1 | ||
|
|
c50509b741 | ||
|
|
cd66572514 | ||
|
|
dcb5a67089 | ||
|
|
efe44a7e6a | ||
|
|
8625e66453 | ||
|
|
3cae09b968 | ||
|
|
3c499d0241 | ||
|
|
e71e53ffb5 | ||
|
|
260c863e23 | ||
|
|
42b6c07cd0 | ||
|
|
c91a6827f2 | ||
|
|
5dbc3d5701 | ||
|
|
22683cbd2a | ||
|
|
abfab3bef2 | ||
|
|
51e8d6234a | ||
|
|
7ca2846d9c | ||
|
|
7ba0fc7952 | ||
|
|
e2c86d137f | ||
|
|
776a63b61d | ||
|
|
84f9d6957c | ||
|
|
7d32728e16 | ||
|
|
40a9b72b3c | ||
|
|
4ff26cd707 | ||
|
|
462919a28f | ||
|
|
f3e2be9f85 | ||
|
|
4d40f83457 | ||
|
|
00be1e4dd4 | ||
|
|
f7d67b946e | ||
|
|
ae9aee896b | ||
|
|
6dec172743 | ||
|
|
da581d988a | ||
|
|
7960826552 | ||
|
|
84059691b8 | ||
|
|
896288eff1 | ||
|
|
b3f132efd1 | ||
|
|
60214b957c | ||
|
|
7361220af6 | ||
|
|
cb2ad71a31 | ||
|
|
900e3a6491 | ||
|
|
f18084203d | ||
|
|
3358a892f2 | ||
|
|
bbfcaa2cc0 | ||
|
|
ba657871bc | ||
|
|
29d5d9674a | ||
|
|
0a645e72c1 | ||
|
|
1850511334 | ||
|
|
300adfd7ae | ||
|
|
f357662f37 | ||
|
|
ed0aebcbc9 | ||
|
|
ea4f1885f8 | ||
|
|
2f75db604f | ||
|
|
d844a3dd8b | ||
|
|
9fcba1a26c | ||
|
|
0659891e68 | ||
|
|
e7fb3b18f3 | ||
|
|
e6eef4b2c4 | ||
|
|
2ce577e36b | ||
|
|
c7c21e88f0 | ||
|
|
a89371b0b9 | ||
|
|
8f7f8761d1 | ||
|
|
a3ff300419 | ||
|
|
ea6e326b9c | ||
|
|
740f8759cc | ||
|
|
bb9cd4eb6a | ||
|
|
3a591fbf26 | ||
|
|
7754d5aef5 | ||
|
|
1d63d5db5f | ||
|
|
26640368e5 | ||
|
|
f23c2a83f2 | ||
|
|
61d54d1b19 | ||
|
|
10ea6144e0 | ||
|
|
292d971937 | ||
|
|
5c082472a1 | ||
|
|
0bedc71854 | ||
|
|
fe66edb825 | ||
|
|
bfa2df7036 | ||
|
|
0e52706a5b | ||
|
|
4c6166f989 | ||
|
|
8e99b3bd7f | ||
|
|
f6532de1fd | ||
|
|
001fde7694 | ||
|
|
b872eac2b9 | ||
|
|
038d0585cf | ||
|
|
b15428ea83 | ||
|
|
c9ad9d338c | ||
|
|
ae6bd9f6b0 | ||
|
|
866e437c05 | ||
|
|
784db053f2 | ||
|
|
ff1c6bd26e | ||
|
|
46fd63da5f | ||
|
|
420a1c79e1 | ||
|
|
6d39050e1e | ||
|
|
077bf2a409 | ||
|
|
1a3e38c018 | ||
|
|
e495f75cc2 | ||
|
|
e781449c36 | ||
|
|
7eb03ea198 | ||
|
|
bd34cd5613 | ||
|
|
c28d455f7b | ||
|
|
d250a7f502 | ||
|
|
b0f46326ca | ||
|
|
7aed2f122c | ||
|
|
5199a34d9b | ||
|
|
4c2ab6f878 | ||
|
|
a818dd9d19 | ||
|
|
efef03d944 | ||
|
|
33760f083a | ||
|
|
013802c877 | ||
|
|
3dcad3b3cd | ||
|
|
db065315ba | ||
|
|
a03f02789b | ||
|
|
ca4b9b43af | ||
|
|
77084930e9 | ||
|
|
fb77921b15 | ||
|
|
0d02dea20b | ||
|
|
86b47ba28b | ||
|
|
95ff0595c6 | ||
|
|
bbc56f35ec | ||
|
|
dfbbf87ba9 | ||
|
|
037b603603 | ||
|
|
bd33df09de | ||
|
|
6ca5b62112 | ||
|
|
e1a2057a1b | ||
|
|
ae9dbe0ca7 | ||
|
|
3b486425b0 | ||
|
|
b08bcf00ac | ||
|
|
0740e8b431 | ||
|
|
5532df880f | ||
|
|
2b11efc3ae | ||
|
|
a1d982ca76 | ||
|
|
03be7f1ee3 | ||
|
|
1ced4eaf10 | ||
|
|
4970167726 | ||
|
|
065ccbc4aa | ||
|
|
1feaa63813 | ||
|
|
8541c6be35 | ||
|
|
18654526c8 | ||
|
|
bd1f7f9492 | ||
|
|
40d75dd283 | ||
|
|
f5011365f1 | ||
|
|
ebbd046c92 | ||
|
|
b9979088ec | ||
|
|
bd0e53bd2e | ||
|
|
4b586f4105 | ||
|
|
0410119d5f | ||
|
|
c2f67398d0 | ||
|
|
81149344a5 | ||
|
|
e9b649b345 | ||
|
|
9c538f4237 | ||
|
|
c08a358a2b | ||
|
|
dbaef5e79b | ||
|
|
48a155a213 | ||
|
|
c69a9a77c6 | ||
|
|
ef79fa3898 | ||
|
|
f7ad602bfc | ||
|
|
ffd6049ba2 | ||
|
|
b3ad54c0c2 | ||
|
|
67eca3f58d | ||
|
|
0847bf560f | ||
|
|
8074565c3e | ||
|
|
24e81267b9 | ||
|
|
c0ffee3ff9 | ||
|
|
c0ffeeef26 | ||
|
|
22dbf90b7d | ||
|
|
40c68c9c83 | ||
|
|
b7f0f158ab | ||
|
|
d8f31046a9 | ||
|
|
6c6382ed69 | ||
|
|
53ba1372f9 | ||
|
|
57c9cf64eb | ||
|
|
e83b1e4159 | ||
|
|
3784020a8d | ||
|
|
1223444738 | ||
|
|
2bfeed2b00 | ||
|
|
04b800d768 | ||
|
|
f1d62978ef | ||
|
|
ffadc64f28 | ||
|
|
5302f40342 | ||
|
|
b0f8c2c03e | ||
|
|
37e40673ff | ||
|
|
fefee4c675 | ||
|
|
1aa7d3d2ba | ||
|
|
0db9c56f34 | ||
|
|
f4d134cd5c | ||
|
|
b667bcb48b | ||
|
|
2c14e0b2ba | ||
|
|
ca207ed93f | ||
|
|
ef13343591 | ||
|
|
adb40c41b7 | ||
|
|
d8403af515 | ||
|
|
0d26f05bb8 | ||
|
|
1dcad34b06 | ||
|
|
9a42c8c8b2 | ||
|
|
1fcef2133f | ||
|
|
d7514b1305 | ||
|
|
c0ffee5919 | ||
|
|
724d02e9b3 | ||
|
|
a5b387cdd4 | ||
|
|
8318be2edc | ||
|
|
b6547f322e | ||
|
|
b316ff8413 | ||
|
|
806b2622da | ||
|
|
1f90a37b63 | ||
|
|
c34f7c9b2c | ||
|
|
604d899496 | ||
|
|
799da202df | ||
|
|
ddb45cd658 | ||
|
|
dafbb4ae90 | ||
|
|
9876096cf4 | ||
|
|
8750b4b006 | ||
|
|
34083c96d5 | ||
|
|
9672ed5285 | ||
|
|
f3112fc038 | ||
|
|
d338ccb9a6 | ||
|
|
d67de1c9b2 | ||
|
|
2324564743 | ||
|
|
b3097f143c | ||
|
|
7b309dc75d | ||
|
|
8f68cffdf1 | ||
|
|
dd27d0fd1d | ||
|
|
7a26e6b3d8 | ||
|
|
cf4e77ab0c | ||
|
|
7bae55dd39 | ||
|
|
0ce8aef229 | ||
|
|
6eab595fae | ||
|
|
b16b109a80 | ||
|
|
ab698191b9 | ||
|
|
7dc488a73b | ||
|
|
e6ed804ca5 | ||
|
|
951abb67ee | ||
|
|
8d1cd41453 |
39
.github/workflows/liquid.yml
vendored
Normal file
39
.github/workflows/liquid.yml
vendored
Normal file
@@ -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.0, 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
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,7 +4,6 @@
|
||||
pkg
|
||||
*.rbc
|
||||
.rvmrc
|
||||
.idea
|
||||
.ruby-version
|
||||
Gemfile.lock
|
||||
.bundle
|
||||
|
||||
1027
.rubocop.shopify.yml
1027
.rubocop.shopify.yml
File diff suppressed because it is too large
Load Diff
20
.rubocop.yml
20
.rubocop.yml
@@ -1,10 +1,24 @@
|
||||
inherit_gem:
|
||||
rubocop-shopify: rubocop.yml
|
||||
|
||||
inherit_from:
|
||||
- .rubocop.shopify.yml
|
||||
- .rubocop_todo.yml
|
||||
|
||||
require: rubocop-performance
|
||||
|
||||
Performance:
|
||||
Enabled: true
|
||||
|
||||
Metrics/LineLength:
|
||||
Enabled: false
|
||||
AllCops:
|
||||
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
|
||||
|
||||
57
.rubocop_todo.yml
Normal file
57
.rubocop_todo.yml
Normal file
@@ -0,0 +1,57 @@
|
||||
# This configuration was generated by
|
||||
# `rubocop --auto-gen-config`
|
||||
# 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
|
||||
# versions of RuboCop, may require this file to be generated again.
|
||||
|
||||
# Offense count: 2
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle.
|
||||
# SupportedStyles: runtime_error, standard_error
|
||||
Lint/InheritException:
|
||||
Exclude:
|
||||
- 'lib/liquid/interrupts.rb'
|
||||
|
||||
# Offense count: 113
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
|
||||
# URISchemes: http, https
|
||||
Layout/LineLength:
|
||||
Max: 260
|
||||
|
||||
# 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'
|
||||
- 'lib/liquid/block_body.rb'
|
||||
- 'lib/liquid/tags/assign.rb'
|
||||
- 'lib/liquid/tags/capture.rb'
|
||||
- 'lib/liquid/tags/case.rb'
|
||||
- 'lib/liquid/tags/cycle.rb'
|
||||
- 'lib/liquid/tags/for.rb'
|
||||
- 'lib/liquid/tags/if.rb'
|
||||
- 'lib/liquid/tags/raw.rb'
|
||||
- 'lib/liquid/tags/table_row.rb'
|
||||
- 'lib/liquid/variable.rb'
|
||||
- 'performance/shopify/comment_form.rb'
|
||||
- 'performance/shopify/paginate.rb'
|
||||
- 'test/integration/tags/include_tag_test.rb'
|
||||
|
||||
# Offense count: 2
|
||||
Style/ClassVars:
|
||||
Exclude:
|
||||
- 'lib/liquid/condition.rb'
|
||||
30
.travis.yml
30
.travis.yml
@@ -1,30 +0,0 @@
|
||||
language: ruby
|
||||
|
||||
rvm:
|
||||
- 2.4
|
||||
- 2.5
|
||||
- 2.6
|
||||
- 2.7
|
||||
- ruby-head
|
||||
- jruby-head
|
||||
- truffleruby
|
||||
|
||||
sudo: false
|
||||
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- libgmp3-dev
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- rvm: ruby-head
|
||||
- rvm: jruby-head
|
||||
|
||||
install:
|
||||
- bundle install
|
||||
|
||||
script: bundle exec rake
|
||||
|
||||
notifications:
|
||||
disable: true
|
||||
@@ -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
|
||||
|
||||
|
||||
6
Gemfile
6
Gemfile
@@ -10,6 +10,7 @@ gemspec
|
||||
group :benchmark, :test do
|
||||
gem 'benchmark-ips'
|
||||
gem 'memory_profiler'
|
||||
gem 'terminal-table'
|
||||
|
||||
install_if -> { RUBY_PLATFORM !~ /mingw|mswin|java/ && RUBY_ENGINE != 'truffleruby' } do
|
||||
gem 'stackprof'
|
||||
@@ -17,10 +18,11 @@ group :benchmark, :test do
|
||||
end
|
||||
|
||||
group :test do
|
||||
gem 'rubocop', '~> 0.74.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
|
||||
gem 'liquid-c', github: 'Shopify/liquid-c', ref: '7ba926791ef8411984d0f3e41c6353fd716041c6'
|
||||
gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'master'
|
||||
end
|
||||
end
|
||||
|
||||
50
History.md
50
History.md
@@ -1,5 +1,53 @@
|
||||
# Liquid Change Log
|
||||
|
||||
## 5.0.2 (unreleased)
|
||||
|
||||
### Fixes
|
||||
* Fix support for using a String subclass for the liquid source (#1421) [Dylan Thacker-Smith]
|
||||
|
||||
## 5.0.1 / 2021-03-24
|
||||
|
||||
### Fixes
|
||||
* Add ParseTreeVisitor to Echo tag (#1414) [CP Clermont]
|
||||
* Test with ruby 3.0 as the latest ruby version (#1398) [Dylan Thacker-Smith]
|
||||
* Handle carriage return in newlines_to_br (#1391) [Unending]
|
||||
|
||||
### Performance Improvements
|
||||
* Use split limit in truncatewords (#1361) [Dylan Thacker-Smith]
|
||||
|
||||
## 5.0.0 / 2021-01-06
|
||||
|
||||
### Features
|
||||
* Add new `{% render %}` tag (#1122) [Samuel Doiron]
|
||||
* Add support for `as` in `{% render %}` and `{% include %}` (#1181) [Mike Angell]
|
||||
* Add `{% liquid %}` and `{% echo %}` tags (#1086) [Justin Li]
|
||||
* Add [usage tracking](README.md#usage-tracking) [Mike Angell]
|
||||
* Add `Tag.disable_tags` for disabling tags that prepend `Tag::Disableable` at render time (#1162, #1274, #1275) [Mike Angell]
|
||||
* Support using a profiler for multiple renders (#1365, #1366) [Dylan Thacker-Smith]
|
||||
|
||||
### Fixes
|
||||
* Fix catastrophic backtracking in `RANGES_REGEX` regular expression (#1357) [Dylan Thacker-Smith]
|
||||
* Make sure the for tag's limit and offset are integers (#1094) [David Cornu]
|
||||
* Invokable methods for enumerable reject include (#1151) [Thierry Joyal]
|
||||
* Allow `default` filter to handle `false` as value (#1144) [Mike Angell]
|
||||
* Fix render length resource limit so it doesn't multiply nested output (#1285) [Dylan Thacker-Smith]
|
||||
* Fix duplication of text in raw tags (#1304) [Peter Zhu]
|
||||
* Fix strict parsing of find variable with a name expression (#1317) [Dylan Thacker-Smith]
|
||||
* Use monotonic time to measure durations in Liquid::Profiler (#1362) [Dylan Thacker-Smith]
|
||||
|
||||
### Breaking Changes
|
||||
* Require Ruby >= 2.5 (#1131, #1310) [Mike Angell, Dylan Thacker-Smith]
|
||||
* Remove support for taint checking (#1268) [Dylan Thacker-Smith]
|
||||
* Split Strainer class into StrainerFactory and StrainerTemplate (#1208) [Thierry Joyal]
|
||||
* Remove handling of a nil context in the Strainer class (#1218) [Thierry Joyal]
|
||||
* Handle `BlockBody#blank?` at parse time (#1287) [Dylan Thacker-Smith]
|
||||
* Pass the tag markup and tokenizer to `Document#unknown_tag` (#1290) [Dylan Thacker-Smith]
|
||||
* And several internal changes
|
||||
|
||||
### Performance Improvements
|
||||
* Reduce allocations (#1073, #1091, #1115, #1099, #1117, #1141, #1322, #1341) [Richard Monette, Florian Weingarten, Ashwin Maroli]
|
||||
* Improve resources limits performance (#1093, #1323) [Florian Weingarten, Dylan Thacker-Smith]
|
||||
|
||||
## 4.0.3 / 2019-03-12
|
||||
|
||||
### Fixed
|
||||
@@ -274,7 +322,7 @@ Yanked from rubygems, as it contained too many changes that broke compatibility.
|
||||
* Fixed bad error reporting when a filter called a method which doesn't exist. Liquid told you that it couldn't find the filter which was obviously misleading [Tobias Luetke]
|
||||
* Removed count helper from standard lib. use size [Tobias Luetke]
|
||||
* Fixed bug with string filter parameters failing to tolerate commas in strings. [Paul Hammond]
|
||||
* Improved filter parameters. Filter parameters are now context sensitive; Types are resolved according to the rules of the context. Multiple parameters are now separated by the Liquid::ARGUMENT_SEPARATOR: , by default [Paul Hammond]
|
||||
* Improved filter parameters. Filter parameters are now context sensitive; Types are resolved according to the rules of the context. Multiple parameters are now separated by the Liquid::ArgumentSeparator: , by default [Paul Hammond]
|
||||
{{ 'Typo' | link_to: 'http://typo.leetsoft.com', 'Typo - a modern weblog engine' }}
|
||||
* Added Liquid::Drop. A base class which you can use for exporting proxy objects to liquid which can acquire more data when used in liquid. [Tobias Luetke]
|
||||
|
||||
|
||||
@@ -106,3 +106,9 @@ template = Liquid::Template.parse("{{x}} {{y}}")
|
||||
template.render!({ 'x' => 1}, { strict_variables: true })
|
||||
#=> Liquid::UndefinedVariable: Liquid error: undefined variable y
|
||||
```
|
||||
|
||||
### Usage tracking
|
||||
|
||||
To help track usages of a feature or code path in production, we have released opt-in usage tracking. To enable this, we provide an empty `Liquid:: Usage.increment` method which you can customize to your needs. The feature is well suited to https://github.com/Shopify/statsd-instrument. However, the choice of implementation is up to you.
|
||||
|
||||
Once you have enabled usage tracking, we recommend reporting any events through Github Issues that your system may be logging. It is highly likely this event has been added to consider deprecating or improving code specific to this event, so please raise any concerns.
|
||||
53
Rakefile
53
Rakefile
@@ -2,16 +2,22 @@
|
||||
|
||||
require 'rake'
|
||||
require 'rake/testtask'
|
||||
$LOAD_PATH.unshift(File.expand_path('lib', __dir__))
|
||||
require 'liquid/version'
|
||||
$LOAD_PATH.unshift(File.expand_path("../lib", __FILE__))
|
||||
require "liquid/version"
|
||||
|
||||
task(default: %i[test rubocop])
|
||||
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
|
||||
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')
|
||||
@@ -37,22 +43,21 @@ task :test do
|
||||
Rake::Task['base_test'].invoke
|
||||
|
||||
if RUBY_ENGINE == 'ruby' || RUBY_ENGINE == 'truffleruby'
|
||||
|
||||
ENV['LIQUID_C'] = '1'
|
||||
|
||||
ENV['LIQUID_PARSER_MODE'] = 'lax'
|
||||
Rake::Task['base_test'].reenable
|
||||
Rake::Task['base_test'].invoke
|
||||
Rake::Task['integration_test'].reenable
|
||||
Rake::Task['integration_test'].invoke
|
||||
|
||||
ENV['LIQUID_PARSER_MODE'] = 'strict'
|
||||
Rake::Task['base_test'].reenable
|
||||
Rake::Task['base_test'].invoke
|
||||
Rake::Task['integration_test'].reenable
|
||||
Rake::Task['integration_test'].invoke
|
||||
end
|
||||
end
|
||||
|
||||
task(gem: :build)
|
||||
task :build do
|
||||
system 'gem build liquid.gemspec'
|
||||
system "gem build liquid.gemspec"
|
||||
end
|
||||
|
||||
task install: :build do
|
||||
@@ -61,45 +66,45 @@ end
|
||||
|
||||
task release: :build do
|
||||
system "git tag -a v#{Liquid::VERSION} -m 'Tagging #{Liquid::VERSION}'"
|
||||
system 'git push --tags'
|
||||
system "git push --tags"
|
||||
system "gem push liquid-#{Liquid::VERSION}.gem"
|
||||
system "rm liquid-#{Liquid::VERSION}.gem"
|
||||
end
|
||||
|
||||
namespace :benchmark do
|
||||
desc 'Run the liquid benchmark with lax parsing'
|
||||
desc "Run the liquid benchmark with lax parsing"
|
||||
task :run do
|
||||
ruby './performance/benchmark.rb lax'
|
||||
ruby "./performance/benchmark.rb lax"
|
||||
end
|
||||
|
||||
desc 'Run the liquid benchmark with strict parsing'
|
||||
desc "Run the liquid benchmark with strict parsing"
|
||||
task :strict do
|
||||
ruby './performance/benchmark.rb strict'
|
||||
ruby "./performance/benchmark.rb strict"
|
||||
end
|
||||
end
|
||||
|
||||
namespace :profile do
|
||||
desc 'Run the liquid profile/performance coverage'
|
||||
desc "Run the liquid profile/performance coverage"
|
||||
task :run do
|
||||
ruby './performance/profile.rb'
|
||||
ruby "./performance/profile.rb"
|
||||
end
|
||||
|
||||
desc 'Run the liquid profile/performance coverage with strict parsing'
|
||||
desc "Run the liquid profile/performance coverage with strict parsing"
|
||||
task :strict do
|
||||
ruby './performance/profile.rb strict'
|
||||
ruby "./performance/profile.rb strict"
|
||||
end
|
||||
end
|
||||
|
||||
namespace :memory_profile do
|
||||
desc 'Run memory profiler'
|
||||
desc "Run memory profiler"
|
||||
task :run do
|
||||
ruby './performance/memory_profile.rb'
|
||||
ruby "./performance/memory_profile.rb"
|
||||
end
|
||||
end
|
||||
|
||||
desc('Run example')
|
||||
desc("Run example")
|
||||
task :example do
|
||||
ruby '-w -d -Ilib example/server/server.rb'
|
||||
ruby "-w -d -Ilib example/server/server.rb"
|
||||
end
|
||||
|
||||
task :console do
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
module ProductsFilter
|
||||
def price(integer)
|
||||
format('$%.2d USD', integer / 100.0)
|
||||
format("$%.2d USD", integer / 100.0)
|
||||
end
|
||||
|
||||
def prettyprint(text)
|
||||
@@ -30,17 +30,17 @@ class Servlet < LiquidServlet
|
||||
private
|
||||
|
||||
def products_list
|
||||
[{ 'name' => 'Arbor Draft', 'price' => 39_900, 'description' => 'the *arbor draft* is a excellent product' },
|
||||
{ 'name' => 'Arbor Element', 'price' => 40_000, 'description' => 'the *arbor element* rocks for freestyling' },
|
||||
{ 'name' => 'Arbor Diamond', 'price' => 59_900, 'description' => 'the *arbor diamond* is a made up product because im obsessed with arbor and have no creativity' }]
|
||||
[{ 'name' => 'Arbor Draft', 'price' => 39900, 'description' => 'the *arbor draft* is a excellent product' },
|
||||
{ 'name' => 'Arbor Element', 'price' => 40000, 'description' => 'the *arbor element* rocks for freestyling' },
|
||||
{ 'name' => 'Arbor Diamond', 'price' => 59900, 'description' => 'the *arbor diamond* is a made up product because im obsessed with arbor and have no creativity' }]
|
||||
end
|
||||
|
||||
def more_products_list
|
||||
[{ 'name' => 'Arbor Catalyst', 'price' => 39_900, 'description' => 'the *arbor catalyst* is an advanced drop-through for freestyle and flatground performance and versatility' },
|
||||
{ 'name' => 'Arbor Fish', 'price' => 40_000, 'description' => 'the *arbor fish* is a compact pin that features an extended wheelbase and time-honored teardrop shape' }]
|
||||
[{ 'name' => 'Arbor Catalyst', 'price' => 39900, 'description' => 'the *arbor catalyst* is an advanced drop-through for freestyle and flatground performance and versatility' },
|
||||
{ 'name' => 'Arbor Fish', 'price' => 40000, 'description' => 'the *arbor fish* is a compact pin that features an extended wheelbase and time-honored teardrop shape' }]
|
||||
end
|
||||
|
||||
def description
|
||||
'List of Products ~ This is a list of products with price and description.'
|
||||
"List of Products ~ This is a list of products with price and description."
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,30 +1,27 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet
|
||||
def get(req, res)
|
||||
def do_GET(req, res)
|
||||
handle(:get, req, res)
|
||||
end
|
||||
|
||||
def post(req, res)
|
||||
def do_POST(req, res)
|
||||
handle(:post, req, res)
|
||||
end
|
||||
|
||||
alias_method :do_GET, :get
|
||||
alias_method :do_POST, :post
|
||||
|
||||
private
|
||||
|
||||
def handle(_type, req, res)
|
||||
@request = req
|
||||
@request = req
|
||||
@response = res
|
||||
|
||||
@request.path_info =~ /(\w+)\z/
|
||||
@action = Regexp.last_match(1) || 'index'
|
||||
@action = Regexp.last_match(1) || 'index'
|
||||
@assigns = send(@action) if respond_to?(@action)
|
||||
|
||||
@response['Content-Type'] = 'text/html'
|
||||
@response['Content-Type'] = "text/html"
|
||||
@response.status = 200
|
||||
@response.body = Liquid::Template.parse(read_template).render(@assigns, filters: [ProductsFilter])
|
||||
@response.body = Liquid::Template.parse(read_template).render(@assigns, filters: [ProductsFilter])
|
||||
end
|
||||
|
||||
def read_template(filename = @action)
|
||||
|
||||
@@ -10,5 +10,5 @@ require_relative 'example_servlet'
|
||||
# Setup webrick
|
||||
server = WEBrick::HTTPServer.new(Port: ARGV[1] || 3000)
|
||||
server.mount('/', Servlet)
|
||||
trap('INT') { server.shutdown }
|
||||
trap("INT") { server.shutdown }
|
||||
server.start
|
||||
|
||||
@@ -22,31 +22,33 @@
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
module Liquid
|
||||
FILTER_SEPARATOR = /\|/.freeze
|
||||
ARGUMENT_SEPARATOR = ','
|
||||
FILTER_ARGUMENT_SEPARATOR = ':'
|
||||
VARIABLE_ATTRIBUTE_SEPARATOR = '.'
|
||||
WHITESPACE_CONTROL = '-'
|
||||
TAG_START = /\{\%/.freeze
|
||||
TAG_END = /\%\}/.freeze
|
||||
VARIABLE_SIGNATURE = /\(?[\w\-\.\[\]]\)?/.freeze
|
||||
VARIABLE_SEGMENT = /[\w\-]/.freeze
|
||||
VARIABLE_START = /\{\{/.freeze
|
||||
VARIABLE_END = /\}\}/.freeze
|
||||
VARIABLE_INCOMPLETE_END = /\}\}?/.freeze
|
||||
QUOTED_STRING = /"[^"]*"|'[^']*'/.freeze
|
||||
QUOTED_FRAGMENT = /#{QUOTED_STRING}|(?:[^\s,\|'"]|#{QUOTED_STRING})+/o.freeze
|
||||
TAG_ATTRIBUTES = /(\w+)\s*\:\s*(#{QUOTED_FRAGMENT})/o.freeze
|
||||
ANY_STARTING_TAG = /#{TAG_START}|#{VARIABLE_START}/o.freeze
|
||||
PARTIAL_TEMPLATE_PARSER = /#{TAG_START}.*?#{TAG_END}|#{VARIABLE_START}.*?#{VARIABLE_INCOMPLETE_END}/om.freeze
|
||||
TEMPLATE_PARSER = /(#{PARTIAL_TEMPLATE_PARSER}|#{ANY_STARTING_TAG})/om.freeze
|
||||
VARIABLE_PARSER = /\[[^\]]+\]|#{VARIABLE_SEGMENT}+\??/o.freeze
|
||||
FilterSeparator = /\|/
|
||||
ArgumentSeparator = ','
|
||||
FilterArgumentSeparator = ':'
|
||||
VariableAttributeSeparator = '.'
|
||||
WhitespaceControl = '-'
|
||||
TagStart = /\{\%/
|
||||
TagEnd = /\%\}/
|
||||
VariableSignature = /\(?[\w\-\.\[\]]\)?/
|
||||
VariableSegment = /[\w\-]/
|
||||
VariableStart = /\{\{/
|
||||
VariableEnd = /\}\}/
|
||||
VariableIncompleteEnd = /\}\}?/
|
||||
QuotedString = /"[^"]*"|'[^']*'/
|
||||
QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o
|
||||
TagAttributes = /(\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
|
||||
|
||||
require 'liquid/version'
|
||||
require "liquid/version"
|
||||
require 'liquid/parse_tree_visitor'
|
||||
require 'liquid/lexer'
|
||||
require 'liquid/parser'
|
||||
@@ -57,11 +59,14 @@ require 'liquid/forloop_drop'
|
||||
require 'liquid/extensions'
|
||||
require 'liquid/errors'
|
||||
require 'liquid/interrupts'
|
||||
require 'liquid/strainer'
|
||||
require 'liquid/strainer_factory'
|
||||
require 'liquid/strainer_template'
|
||||
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'
|
||||
@@ -76,6 +81,11 @@ require 'liquid/condition'
|
||||
require 'liquid/utils'
|
||||
require 'liquid/tokenizer'
|
||||
require 'liquid/parse_context'
|
||||
require 'liquid/partial_cache'
|
||||
require 'liquid/usage'
|
||||
require 'liquid/register'
|
||||
require 'liquid/static_registers'
|
||||
require 'liquid/template_factory'
|
||||
|
||||
# Load all the tags of the standard library
|
||||
#
|
||||
|
||||
@@ -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,20 +29,29 @@ 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)
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_else",
|
||||
block_name: block_name)
|
||||
elsif tag.start_with?('end')
|
||||
raise SyntaxError, parse_context.locale.t('errors.syntax.invalid_delimiter',
|
||||
tag: tag,
|
||||
block_name: block_name,
|
||||
block_delimiter: block_delimiter)
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.invalid_delimiter",
|
||||
tag: tag,
|
||||
block_name: block_name,
|
||||
block_delimiter: block_delimiter)
|
||||
else
|
||||
raise SyntaxError, parse_context.locale.t('errors.syntax.unknown_tag', tag: tag)
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag", tag: tag)
|
||||
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,18 +60,25 @@ 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)
|
||||
raise StackLevelError, 'Nesting too deep' if parse_context.depth >= MAX_DEPTH
|
||||
|
||||
if parse_context.depth >= MAX_DEPTH
|
||||
raise StackLevelError, "Nesting too deep"
|
||||
end
|
||||
parse_context.depth += 1
|
||||
begin
|
||||
body.parse(tokens, parse_context) do |end_tag_name, end_tag_params|
|
||||
@blank &&= body.blank?
|
||||
|
||||
return false if end_tag_name == block_delimiter
|
||||
raise SyntaxError, parse_context.locale.t('errors.syntax.tag_never_closed', block_name: block_name) unless end_tag_name
|
||||
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
|
||||
|
||||
@@ -1,50 +1,156 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'English'
|
||||
|
||||
module Liquid
|
||||
class BlockBody
|
||||
FULL_TOKEN = /\A#{TAG_START}#{WHITESPACE_CONTROL}?\s*(\w+)\s*(.*?)#{WHITESPACE_CONTROL}?#{TAG_END}\z/om.freeze
|
||||
CONTENT_OF_VARIABLE = /\A#{VARIABLE_START}#{WHITESPACE_CONTROL}?(.*?)#{WHITESPACE_CONTROL}?#{VARIABLE_END}\z/om.freeze
|
||||
WHITESPACE_OR_NOTHING = /\A\s*\z/.freeze
|
||||
TAG_START = '{%'
|
||||
VAR_START = '{{'
|
||||
LiquidTagToken = /\A\s*(\w+)\s*(.*?)\z/o
|
||||
FullToken = /\A#{TagStart}#{WhitespaceControl}?(\s*)(\w+)(\s*)(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
|
||||
ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
|
||||
WhitespaceOrNothing = /\A\s*\z/
|
||||
TAGSTART = "{%"
|
||||
VARSTART = "{{"
|
||||
|
||||
attr_reader :nodelist
|
||||
|
||||
def initialize
|
||||
@nodelist = []
|
||||
@blank = true
|
||||
@blank = true
|
||||
end
|
||||
|
||||
def parse(tokenizer, parse_context)
|
||||
parse_context.line_number = tokenizer.line_number
|
||||
while (token = tokenizer.shift)
|
||||
next if token.empty?
|
||||
def parse(tokenizer, parse_context, &block)
|
||||
raise FrozenError, "can't modify frozen Liquid::BlockBody" if frozen?
|
||||
|
||||
case
|
||||
when token.start_with?(TAG_START)
|
||||
whitespace_handler(token, parse_context)
|
||||
raise_missing_tag_terminator(token, parse_context) unless token =~ FULL_TOKEN
|
||||
parse_context.line_number = tokenizer.line_number
|
||||
|
||||
if tokenizer.for_liquid_tag
|
||||
parse_for_liquid_tag(tokenizer, parse_context, &block)
|
||||
else
|
||||
parse_for_document(tokenizer, parse_context, &block)
|
||||
end
|
||||
end
|
||||
|
||||
def freeze
|
||||
@nodelist.freeze
|
||||
super
|
||||
end
|
||||
|
||||
private def parse_for_liquid_tag(tokenizer, parse_context)
|
||||
while (token = tokenizer.shift)
|
||||
unless token.empty? || token =~ WhitespaceOrNothing
|
||||
unless token =~ LiquidTagToken
|
||||
# line isn't empty but didn't match tag syntax, yield and let the
|
||||
# caller raise a syntax error
|
||||
return yield token, token
|
||||
end
|
||||
tag_name = Regexp.last_match(1)
|
||||
markup = Regexp.last_match(2)
|
||||
# fetch the tag from registered blocks
|
||||
markup = Regexp.last_match(2)
|
||||
unless (tag = registered_tags[tag_name])
|
||||
# end parsing if we reach an unknown tag and let the caller decide
|
||||
# determine how to proceed
|
||||
return yield tag_name, markup
|
||||
end
|
||||
|
||||
new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
|
||||
@blank &&= new_tag.blank?
|
||||
@nodelist << new_tag
|
||||
when token.start_with?(VAR_START)
|
||||
end
|
||||
parse_context.line_number = tokenizer.line_number
|
||||
end
|
||||
|
||||
yield nil, nil
|
||||
end
|
||||
|
||||
# @api private
|
||||
def self.unknown_tag_in_liquid_tag(tag, parse_context)
|
||||
Block.raise_unknown_tag(tag, 'liquid', '%}', parse_context)
|
||||
end
|
||||
|
||||
# @api private
|
||||
def self.raise_missing_tag_terminator(token, parse_context)
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_termination", token: token, tag_end: TagEnd.inspect)
|
||||
end
|
||||
|
||||
# @api private
|
||||
def self.raise_missing_variable_terminator(token, parse_context)
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.variable_termination", token: token, tag_end: VariableEnd.inspect)
|
||||
end
|
||||
|
||||
# @api private
|
||||
def self.render_node(context, output, node)
|
||||
node.render_to_output_buffer(context, output)
|
||||
rescue => exc
|
||||
blank_tag = !node.instance_of?(Variable) && node.blank?
|
||||
rescue_render_node(context, output, node.line_number, exc, blank_tag)
|
||||
end
|
||||
|
||||
# @api private
|
||||
def self.rescue_render_node(context, output, line_number, exc, blank_tag)
|
||||
case exc
|
||||
when MemoryError
|
||||
raise
|
||||
when UndefinedVariable, UndefinedDropMethod, UndefinedFilter
|
||||
context.handle_error(exc, line_number)
|
||||
else
|
||||
error_message = context.handle_error(exc, line_number)
|
||||
unless blank_tag # conditional for backwards compatibility
|
||||
output << error_message
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private def parse_liquid_tag(markup, parse_context)
|
||||
liquid_tag_tokenizer = parse_context.new_tokenizer(
|
||||
markup, start_line_number: parse_context.line_number, for_liquid_tag: true
|
||||
)
|
||||
parse_for_liquid_tag(liquid_tag_tokenizer, parse_context) do |end_tag_name, _end_tag_markup|
|
||||
if end_tag_name
|
||||
BlockBody.unknown_tag_in_liquid_tag(end_tag_name, parse_context)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private 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
|
||||
BlockBody.raise_missing_tag_terminator(token, parse_context)
|
||||
end
|
||||
tag_name = Regexp.last_match(2)
|
||||
markup = Regexp.last_match(4)
|
||||
|
||||
if parse_context.line_number
|
||||
# newlines inside the tag should increase the line number,
|
||||
# particularly important for multiline {% liquid %} tags
|
||||
parse_context.line_number += Regexp.last_match(1).count("\n") + Regexp.last_match(3).count("\n")
|
||||
end
|
||||
|
||||
if tag_name == 'liquid'
|
||||
parse_liquid_tag(markup, parse_context)
|
||||
next
|
||||
end
|
||||
|
||||
unless (tag = registered_tags[tag_name])
|
||||
# end parsing if we reach an unknown tag and let the caller decide
|
||||
# determine how to proceed
|
||||
return yield tag_name, markup
|
||||
end
|
||||
new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
|
||||
@blank &&= new_tag.blank?
|
||||
@nodelist << new_tag
|
||||
when token.start_with?(VARSTART)
|
||||
whitespace_handler(token, parse_context)
|
||||
@nodelist << create_variable(token, parse_context)
|
||||
@blank = false
|
||||
else
|
||||
token.lstrip! if parse_context.trim_whitespace
|
||||
if parse_context.trim_whitespace
|
||||
token.lstrip!
|
||||
end
|
||||
parse_context.trim_whitespace = false
|
||||
@nodelist << token
|
||||
@blank &&= token.match?(WHITESPACE_OR_NOTHING)
|
||||
@blank &&= !!(token =~ WhitespaceOrNothing)
|
||||
end
|
||||
parse_context.line_number = tokenizer.line_number
|
||||
end
|
||||
@@ -53,49 +159,66 @@ module Liquid
|
||||
end
|
||||
|
||||
def whitespace_handler(token, parse_context)
|
||||
if token[2] == WHITESPACE_CONTROL
|
||||
if token[2] == WhitespaceControl
|
||||
previous_token = @nodelist.last
|
||||
previous_token.rstrip! if previous_token.is_a?(String)
|
||||
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] == WHITESPACE_CONTROL)
|
||||
parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
|
||||
end
|
||||
|
||||
def blank?
|
||||
@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
|
||||
@@ -104,35 +227,25 @@ module Liquid
|
||||
private
|
||||
|
||||
def render_node(context, output, node)
|
||||
node.render_to_output_buffer(context, output)
|
||||
rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
|
||||
context.handle_error(e, node.line_number)
|
||||
rescue ::StandardError => e
|
||||
line_number = node.is_a?(String) ? nil : node.line_number
|
||||
output << context.handle_error(e, line_number)
|
||||
end
|
||||
|
||||
def raise_if_resource_limits_reached(context, length)
|
||||
context.resource_limits.render_length += length
|
||||
return unless context.resource_limits.reached?
|
||||
|
||||
raise MemoryError, 'Memory limits exceeded'
|
||||
BlockBody.render_node(context, output, node)
|
||||
end
|
||||
|
||||
def create_variable(token, parse_context)
|
||||
token.scan(CONTENT_OF_VARIABLE) do |content|
|
||||
token.scan(ContentOfVariable) do |content|
|
||||
markup = content.first
|
||||
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: TAG_END.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: VARIABLE_END.inspect)
|
||||
BlockBody.raise_missing_variable_terminator(token, parse_context)
|
||||
end
|
||||
|
||||
def registered_tags
|
||||
|
||||
@@ -9,7 +9,7 @@ module Liquid
|
||||
# c.evaluate #=> true
|
||||
#
|
||||
class Condition #:nodoc:
|
||||
@operators = {
|
||||
@@operators = {
|
||||
'==' => ->(cond, left, right) { cond.send(:equal_variables, left, right) },
|
||||
'!=' => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
|
||||
'<>' => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
|
||||
@@ -27,18 +27,37 @@ module Liquid
|
||||
end,
|
||||
}
|
||||
|
||||
class << self
|
||||
attr_accessor :operators
|
||||
class MethodLiteral
|
||||
attr_reader :method_name, :to_s
|
||||
|
||||
def initialize(method_name, to_s)
|
||||
@method_name = method_name
|
||||
@to_s = to_s
|
||||
end
|
||||
end
|
||||
|
||||
@@method_literals = {
|
||||
'blank' => MethodLiteral.new(:blank?, '').freeze,
|
||||
'empty' => MethodLiteral.new(:empty?, '').freeze,
|
||||
}
|
||||
|
||||
def self.operators
|
||||
@@operators
|
||||
end
|
||||
|
||||
def self.parse_expression(parse_context, markup)
|
||||
@@method_literals[markup] || parse_context.parse_expression(markup)
|
||||
end
|
||||
|
||||
attr_reader :attachment, :child_condition
|
||||
attr_accessor :left, :operator, :right
|
||||
|
||||
def initialize(left = nil, operator = nil, right = nil)
|
||||
@left = left
|
||||
@left = left
|
||||
@operator = operator
|
||||
@right = right
|
||||
@child_relation = nil
|
||||
@right = right
|
||||
|
||||
@child_relation = nil
|
||||
@child_condition = nil
|
||||
end
|
||||
|
||||
@@ -62,12 +81,12 @@ module Liquid
|
||||
end
|
||||
|
||||
def or(condition)
|
||||
@child_relation = :or
|
||||
@child_relation = :or
|
||||
@child_condition = condition
|
||||
end
|
||||
|
||||
def and(condition)
|
||||
@child_relation = :and
|
||||
@child_relation = :and
|
||||
@child_condition = condition
|
||||
end
|
||||
|
||||
@@ -90,7 +109,7 @@ module Liquid
|
||||
private
|
||||
|
||||
def equal_variables(left, right)
|
||||
if left.is_a?(Liquid::Expression::MethodLiteral)
|
||||
if left.is_a?(MethodLiteral)
|
||||
if right.respond_to?(left.method_name)
|
||||
return right.send(left.method_name)
|
||||
else
|
||||
@@ -98,7 +117,7 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
if right.is_a?(Liquid::Expression::MethodLiteral)
|
||||
if right.is_a?(MethodLiteral)
|
||||
if left.respond_to?(right.method_name)
|
||||
return left.send(right.method_name)
|
||||
else
|
||||
@@ -115,7 +134,7 @@ module Liquid
|
||||
# return this as the result.
|
||||
return context.evaluate(left) if op.nil?
|
||||
|
||||
left = context.evaluate(left)
|
||||
left = context.evaluate(left)
|
||||
right = context.evaluate(right)
|
||||
|
||||
operation = self.class.operators[op] || raise(Liquid::ArgumentError, "Unknown operator #{op}")
|
||||
|
||||
@@ -14,35 +14,49 @@ module Liquid
|
||||
#
|
||||
# context['bob'] #=> nil class Context
|
||||
class Context
|
||||
attr_reader :scopes, :errors, :registers, :environments, :resource_limits
|
||||
attr_reader :scopes, :errors, :registers, :environments, :resource_limits, :static_registers, :static_environments
|
||||
attr_accessor :exception_renderer, :template_name, :partial, :global_filter, :strict_variables, :strict_filters
|
||||
|
||||
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil)
|
||||
@environments = [environments].flatten
|
||||
@scopes = [(outer_scope || {})]
|
||||
@registers = registers
|
||||
@errors = []
|
||||
@partial = false
|
||||
@strict_variables = false
|
||||
@resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
|
||||
squash_instance_assigns_with_environments
|
||||
# rubocop:disable Metrics/ParameterLists
|
||||
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
|
||||
|
||||
@this_stack_used = false
|
||||
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil, static_environments = {})
|
||||
@environments = [environments]
|
||||
@environments.flatten!
|
||||
|
||||
@static_environments = [static_environments].flat_map(&:freeze).freeze
|
||||
@scopes = [(outer_scope || {})]
|
||||
@registers = registers
|
||||
@errors = []
|
||||
@partial = false
|
||||
@strict_variables = false
|
||||
@resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
|
||||
@base_scope_depth = 0
|
||||
@interrupts = []
|
||||
@filters = []
|
||||
@global_filter = nil
|
||||
@disabled_tags = {}
|
||||
|
||||
self.exception_renderer = Template.default_exception_renderer
|
||||
self.exception_renderer = ->(_e) { raise } if rethrow_errors
|
||||
if rethrow_errors
|
||||
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
|
||||
|
||||
def warnings
|
||||
@warnings ||= []
|
||||
end
|
||||
|
||||
def strainer
|
||||
@strainer ||= Strainer.create(self, @filters)
|
||||
@strainer ||= StrainerFactory.create(self, @filters)
|
||||
end
|
||||
|
||||
# Adds filters to this context.
|
||||
@@ -77,7 +91,7 @@ module Liquid
|
||||
def handle_error(e, line_number = nil)
|
||||
e = internal_error unless e.is_a?(Liquid::Error)
|
||||
e.template_name ||= template_name
|
||||
e.line_number ||= line_number
|
||||
e.line_number ||= line_number
|
||||
errors.push(e)
|
||||
exception_renderer.call(e).to_s
|
||||
end
|
||||
@@ -89,7 +103,7 @@ module Liquid
|
||||
# Push new local scope on the stack. use <tt>Context#stack</tt> instead
|
||||
def push(new_scope = {})
|
||||
@scopes.unshift(new_scope)
|
||||
raise StackLevelError, 'Nesting too deep' if @scopes.length > Block::MAX_DEPTH
|
||||
check_overflow
|
||||
end
|
||||
|
||||
# Merge a hash of variables in the current local scope
|
||||
@@ -100,7 +114,6 @@ module Liquid
|
||||
# Pop from the stack. use <tt>Context#stack</tt> instead
|
||||
def pop
|
||||
raise ContextError if @scopes.size == 1
|
||||
|
||||
@scopes.shift
|
||||
end
|
||||
|
||||
@@ -112,19 +125,31 @@ module Liquid
|
||||
# end
|
||||
#
|
||||
# context['var] #=> nil
|
||||
def stack(new_scope = nil)
|
||||
old_stack_used = @this_stack_used
|
||||
if new_scope
|
||||
push(new_scope)
|
||||
@this_stack_used = true
|
||||
else
|
||||
@this_stack_used = false
|
||||
end
|
||||
|
||||
def stack(new_scope = {})
|
||||
push(new_scope)
|
||||
yield
|
||||
ensure
|
||||
pop if @this_stack_used
|
||||
@this_stack_used = old_stack_used
|
||||
pop
|
||||
end
|
||||
|
||||
# Creates a new context inheriting resource limits, filters, environment etc.,
|
||||
# but with an isolated scope.
|
||||
def new_isolated_subcontext
|
||||
check_overflow
|
||||
|
||||
self.class.build(
|
||||
resource_limits: resource_limits,
|
||||
static_environments: static_environments,
|
||||
registers: StaticRegisters.new(registers)
|
||||
).tap do |subcontext|
|
||||
subcontext.base_scope_depth = base_scope_depth + 1
|
||||
subcontext.exception_renderer = exception_renderer
|
||||
subcontext.filters = @filters
|
||||
subcontext.strainer = nil
|
||||
subcontext.errors = errors
|
||||
subcontext.warnings = warnings
|
||||
subcontext.disabled_tags = @disabled_tags
|
||||
end
|
||||
end
|
||||
|
||||
def clear_instance_assigns
|
||||
@@ -133,10 +158,6 @@ module Liquid
|
||||
|
||||
# Only allow String, Numeric, Hash, Array, Proc, Boolean or <tt>Liquid::Drop</tt>
|
||||
def []=(key, value)
|
||||
unless @this_stack_used
|
||||
@this_stack_used = true
|
||||
push({})
|
||||
end
|
||||
@scopes[0][key] = value
|
||||
end
|
||||
|
||||
@@ -165,33 +186,23 @@ module Liquid
|
||||
# This was changed from find() to find_index() because this is a very hot
|
||||
# path and find_index() is optimized in MRI to reduce object allocation
|
||||
index = @scopes.find_index { |s| s.key?(key) }
|
||||
scope = @scopes[index] if index
|
||||
|
||||
variable = nil
|
||||
|
||||
if scope.nil?
|
||||
@environments.each do |e|
|
||||
variable = lookup_and_evaluate(e, key, raise_on_not_found: raise_on_not_found)
|
||||
# When lookup returned a value OR there is no value but the lookup also did not raise
|
||||
# then it is the value we are looking for.
|
||||
if !variable.nil? || @strict_variables && raise_on_not_found
|
||||
scope = e
|
||||
break
|
||||
end
|
||||
end
|
||||
variable = if index
|
||||
lookup_and_evaluate(@scopes[index], key, raise_on_not_found: raise_on_not_found)
|
||||
else
|
||||
try_variable_find_in_environments(key, raise_on_not_found: raise_on_not_found)
|
||||
end
|
||||
|
||||
scope ||= @environments.last || @scopes.last
|
||||
variable ||= lookup_and_evaluate(scope, key, raise_on_not_found: raise_on_not_found)
|
||||
|
||||
variable = variable.to_liquid
|
||||
variable = variable.to_liquid
|
||||
variable.context = self if variable.respond_to?(:context=)
|
||||
|
||||
variable
|
||||
end
|
||||
|
||||
def lookup_and_evaluate(obj, key, raise_on_not_found: true)
|
||||
raise Liquid::UndefinedVariable, "undefined variable #{key}" if @strict_variables && raise_on_not_found && obj.respond_to?(:key?) && !obj.key?(key)
|
||||
if @strict_variables && raise_on_not_found && obj.respond_to?(:key?) && !obj.key?(key)
|
||||
raise Liquid::UndefinedVariable, "undefined variable #{key}"
|
||||
end
|
||||
|
||||
value = obj[key]
|
||||
|
||||
@@ -202,13 +213,58 @@ 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, :disabled_tags
|
||||
|
||||
private
|
||||
|
||||
attr_reader :base_scope_depth
|
||||
|
||||
def try_variable_find_in_environments(key, raise_on_not_found:)
|
||||
@environments.each do |environment|
|
||||
found_variable = lookup_and_evaluate(environment, key, raise_on_not_found: raise_on_not_found)
|
||||
if !found_variable.nil? || @strict_variables && raise_on_not_found
|
||||
return found_variable
|
||||
end
|
||||
end
|
||||
@static_environments.each do |environment|
|
||||
found_variable = lookup_and_evaluate(environment, key, raise_on_not_found: raise_on_not_found)
|
||||
if !found_variable.nil? || @strict_variables && raise_on_not_found
|
||||
return found_variable
|
||||
end
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
def check_overflow
|
||||
raise StackLevelError, "Nesting too deep" if overflow?
|
||||
end
|
||||
|
||||
def overflow?
|
||||
base_scope_depth + @scopes.length > Block::MAX_DEPTH
|
||||
end
|
||||
|
||||
def internal_error
|
||||
# raise and catch to set backtrace and cause on exception
|
||||
raise Liquid::InternalError, 'internal'
|
||||
rescue Liquid::InternalError => e
|
||||
e
|
||||
rescue Liquid::InternalError => exc
|
||||
exc
|
||||
end
|
||||
|
||||
def squash_instance_assigns_with_environments
|
||||
|
||||
@@ -1,28 +1,64 @@
|
||||
# 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)
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_outer_tag", tag: tag)
|
||||
else
|
||||
raise SyntaxError, parse_context.locale.t('errors.syntax.unknown_tag', tag: tag)
|
||||
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
|
||||
|
||||
@@ -28,7 +28,6 @@ module Liquid
|
||||
# Catch all for the method
|
||||
def liquid_method_missing(method)
|
||||
return nil unless @context&.strict_variables
|
||||
|
||||
raise Liquid::UndefinedDropMethod, "undefined method #{method}"
|
||||
end
|
||||
|
||||
@@ -70,7 +69,7 @@ module Liquid
|
||||
|
||||
if include?(Enumerable)
|
||||
blacklist += Enumerable.public_instance_methods
|
||||
blacklist -= %i[sort count first min max include?]
|
||||
blacklist -= [:sort, :count, :first, :min, :max]
|
||||
end
|
||||
|
||||
whitelist = [:to_liquid] + (public_instance_methods - blacklist)
|
||||
|
||||
@@ -7,12 +7,12 @@ module Liquid
|
||||
attr_accessor :markup_context
|
||||
|
||||
def to_s(with_prefix = true)
|
||||
str = +''
|
||||
str = +""
|
||||
str << message_prefix if with_prefix
|
||||
str << super()
|
||||
|
||||
if markup_context
|
||||
str << ' '
|
||||
str << " "
|
||||
str << markup_context
|
||||
end
|
||||
|
||||
@@ -22,37 +22,37 @@ module Liquid
|
||||
private
|
||||
|
||||
def message_prefix
|
||||
str = +''
|
||||
str = +""
|
||||
str << if is_a?(SyntaxError)
|
||||
'Liquid syntax error'
|
||||
"Liquid syntax error"
|
||||
else
|
||||
'Liquid error'
|
||||
"Liquid error"
|
||||
end
|
||||
|
||||
if line_number
|
||||
str << ' ('
|
||||
str << template_name << ' ' if template_name
|
||||
str << 'line ' << line_number.to_s << ')'
|
||||
str << " ("
|
||||
str << template_name << " " if template_name
|
||||
str << "line " << line_number.to_s << ")"
|
||||
end
|
||||
|
||||
str << ': '
|
||||
str << ": "
|
||||
str
|
||||
end
|
||||
end
|
||||
|
||||
ArgumentError = Class.new(Error)
|
||||
ContextError = Class.new(Error)
|
||||
FileSystemError = Class.new(Error)
|
||||
StandardError = Class.new(Error)
|
||||
SyntaxError = Class.new(Error)
|
||||
StackLevelError = Class.new(Error)
|
||||
TaintedError = Class.new(Error)
|
||||
MemoryError = Class.new(Error)
|
||||
ZeroDivisionError = Class.new(Error)
|
||||
FloatDomainError = Class.new(Error)
|
||||
UndefinedVariable = Class.new(Error)
|
||||
ArgumentError = Class.new(Error)
|
||||
ContextError = Class.new(Error)
|
||||
FileSystemError = Class.new(Error)
|
||||
StandardError = Class.new(Error)
|
||||
SyntaxError = Class.new(Error)
|
||||
StackLevelError = Class.new(Error)
|
||||
MemoryError = Class.new(Error)
|
||||
ZeroDivisionError = Class.new(Error)
|
||||
FloatDomainError = Class.new(Error)
|
||||
UndefinedVariable = Class.new(Error)
|
||||
UndefinedDropMethod = Class.new(Error)
|
||||
UndefinedFilter = Class.new(Error)
|
||||
UndefinedFilter = Class.new(Error)
|
||||
MethodOverrideError = Class.new(Error)
|
||||
InternalError = Class.new(Error)
|
||||
DisabledError = Class.new(Error)
|
||||
InternalError = Class.new(Error)
|
||||
end
|
||||
|
||||
@@ -2,46 +2,39 @@
|
||||
|
||||
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.freeze
|
||||
DOUBLE_QUOTED_STRING = /\A"(.*)"\z/m.freeze
|
||||
INTEGERS_REGEX = /\A(-?\d+)\z/.freeze
|
||||
FLOATS_REGEX = /\A(-?\d[\d\.]+)\z/.freeze
|
||||
RANGES_REGEX = /\A\((\S+)\.\.(\S+)\)\z/.freeze
|
||||
SINGLE_QUOTED_STRING = /\A\s*'(.*)'\s*\z/m
|
||||
DOUBLE_QUOTED_STRING = /\A\s*"(.*)"\s*\z/m
|
||||
INTEGERS_REGEX = /\A\s*(-?\d+)\s*\z/
|
||||
FLOATS_REGEX = /\A\s*(-?\d[\d\.]+)\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+)\s*\)\s*\z/
|
||||
|
||||
def self.parse(markup)
|
||||
if LITERALS.key?(markup)
|
||||
LITERALS[markup]
|
||||
case markup
|
||||
when nil
|
||||
nil
|
||||
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
|
||||
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
|
||||
markup = markup.strip
|
||||
if LITERALS.key?(markup)
|
||||
LITERALS[markup]
|
||||
else
|
||||
VariableLookup.parse(markup)
|
||||
end
|
||||
|
||||
@@ -17,7 +17,7 @@ module Liquid
|
||||
class BlankFileSystem
|
||||
# Called by Liquid to retrieve a template file
|
||||
def read_template_file(_template_path)
|
||||
raise FileSystemError, 'This liquid context does not allow includes.'
|
||||
raise FileSystemError, "This liquid context does not allow includes."
|
||||
end
|
||||
end
|
||||
|
||||
@@ -46,8 +46,8 @@ module Liquid
|
||||
class LocalFileSystem
|
||||
attr_accessor :root
|
||||
|
||||
def initialize(root, pattern = '_%s.liquid')
|
||||
@root = root
|
||||
def initialize(root, pattern = "_%s.liquid")
|
||||
@root = root
|
||||
@pattern = pattern
|
||||
end
|
||||
|
||||
@@ -59,7 +59,7 @@ module Liquid
|
||||
end
|
||||
|
||||
def full_path(template_path)
|
||||
raise FileSystemError, "Illegal template name '#{template_path}'" unless template_path =~ %r{\A[^./][a-zA-Z0-9_/]+\z}
|
||||
raise FileSystemError, "Illegal template name '#{template_path}'" unless %r{\A[^./][a-zA-Z0-9_/]+\z}.match?(template_path)
|
||||
|
||||
full_path = if template_path.include?('/')
|
||||
File.join(root, File.dirname(template_path), @pattern % File.basename(template_path))
|
||||
|
||||
@@ -3,13 +3,18 @@
|
||||
module Liquid
|
||||
class ForloopDrop < Drop
|
||||
def initialize(name, length, parentloop)
|
||||
@name = name
|
||||
@length = length
|
||||
@name = name
|
||||
@length = length
|
||||
@parentloop = parentloop
|
||||
@index = 0
|
||||
@index = 0
|
||||
end
|
||||
|
||||
attr_reader :name, :length, :parentloop
|
||||
attr_reader :length, :parentloop
|
||||
|
||||
def name
|
||||
Usage.increment('forloop_drop_name')
|
||||
@name
|
||||
end
|
||||
|
||||
def index
|
||||
@index + 1
|
||||
|
||||
@@ -4,7 +4,7 @@ require 'yaml'
|
||||
|
||||
module Liquid
|
||||
class I18n
|
||||
DEFAULT_LOCALE = File.join(File.expand_path(__dir__), 'locales', 'en.yml')
|
||||
DEFAULT_LOCALE = File.join(File.expand_path(__dir__), "locales", "en.yml")
|
||||
|
||||
TranslationError = Class.new(StandardError)
|
||||
|
||||
|
||||
@@ -6,13 +6,13 @@ module Liquid
|
||||
attr_reader :message
|
||||
|
||||
def initialize(message = nil)
|
||||
@message = message || 'interrupt'
|
||||
@message = message || "interrupt"
|
||||
end
|
||||
end
|
||||
|
||||
# Interrupt that is thrown whenever a {% break %} is called.
|
||||
class BreakInterrupt < RuntimeError; end
|
||||
class BreakInterrupt < Interrupt; end
|
||||
|
||||
# Interrupt that is thrown whenever a {% continue %} is called.
|
||||
class ContinueInterrupt < RuntimeError; end
|
||||
class ContinueInterrupt < Interrupt; end
|
||||
end
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
FilterSeparator = FILTER_SEPARATOR
|
||||
ArgumentSeparator = ARGUMENT_SEPARATOR
|
||||
FilterArgumentSeparator = FILTER_ARGUMENT_SEPARATOR
|
||||
VariableAttributeSeparator = VARIABLE_ATTRIBUTE_SEPARATOR
|
||||
WhitespaceControl = WHITESPACE_CONTROL
|
||||
TagStart = TAG_START
|
||||
TagEnd = TAG_END
|
||||
VariableSignature = VARIABLE_SIGNATURE
|
||||
VariableSegment = VARIABLE_SEGMENT
|
||||
VariableStart = VARIABLE_START
|
||||
VariableEnd = VARIABLE_END
|
||||
VariableIncompleteEnd = VARIABLE_INCOMPLETE_END
|
||||
QuotedString = QUOTED_STRING
|
||||
QuotedFragment = QUOTED_FRAGMENT
|
||||
TagAttributes = TAG_ATTRIBUTES
|
||||
AnyStartingTag = ANY_STARTING_TAG
|
||||
PartialTemplateParser = PARTIAL_TEMPLATE_PARSER
|
||||
TemplateParser = TEMPLATE_PARSER
|
||||
VariableParser = VARIABLE_PARSER
|
||||
|
||||
class BlockBody
|
||||
FullToken = FULL_TOKEN
|
||||
ContentOfVariable = CONTENT_OF_VARIABLE
|
||||
WhitespaceOrNothing = WHITESPACE_OR_NOTHING
|
||||
TAGSTART = TAG_START
|
||||
VARSTART = VAR_START
|
||||
end
|
||||
|
||||
class Assign < Tag
|
||||
Syntax = SYNTAX
|
||||
end
|
||||
|
||||
class Capture < Block
|
||||
Syntax = SYNTAX
|
||||
end
|
||||
|
||||
class Case < Block
|
||||
Syntax = SYNTAX
|
||||
WhenSyntax = WHEN_SYNTAX
|
||||
end
|
||||
|
||||
class Cycle < Tag
|
||||
SimpleSyntax = SIMPLE_SYNTAX
|
||||
NamedSyntax = NAMED_SYNTAX
|
||||
end
|
||||
|
||||
class For < Block
|
||||
Syntax = SYNTAX
|
||||
end
|
||||
|
||||
class If < Block
|
||||
Syntax = SYNTAX
|
||||
ExpressionsAndOperators = EXPRESSIONS_AND_OPERATORS
|
||||
end
|
||||
|
||||
class Include < Tag
|
||||
Syntax = SYNTAX
|
||||
end
|
||||
|
||||
class Raw < Block
|
||||
Syntax = SYNTAX
|
||||
FullTokenPossiblyInvalid = FULL_TOKEN_POSSIBLY_INVALID
|
||||
end
|
||||
|
||||
class TableRow < Block
|
||||
Syntax = SYNTAX
|
||||
end
|
||||
|
||||
class Variable
|
||||
FilterMarkupRegex = FILTER_MARKUP_REGEX
|
||||
FilterParser = FILTER_PARSER
|
||||
FilterArgsRegex = FILTER_ARGS_REGEX
|
||||
JustTagAttributes = JUST_TAG_ATTRIBUTES
|
||||
MarkupWithQuotedFragment = MARKUP_WITH_QUOTED_FRAGMENT
|
||||
end
|
||||
end
|
||||
@@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'strscan'
|
||||
require "strscan"
|
||||
module Liquid
|
||||
class Lexer
|
||||
SPECIALS = {
|
||||
@@ -15,13 +15,13 @@ module Liquid
|
||||
'?' => :question,
|
||||
'-' => :dash,
|
||||
}.freeze
|
||||
IDENTIFIER = /[a-zA-Z_][\w-]*\??/.freeze
|
||||
SINGLE_STRING_LITERAL = /'[^\']*'/.freeze
|
||||
DOUBLE_STRING_LITERAL = /"[^\"]*"/.freeze
|
||||
NUMBER_LITERAL = /-?\d+(\.\d+)?/.freeze
|
||||
DOTDOT = /\.\./.freeze
|
||||
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/.freeze
|
||||
WHITESPACE_OR_NOTHING = /\s*/.freeze
|
||||
IDENTIFIER = /[a-zA-Z_][\w-]*\??/
|
||||
SINGLE_STRING_LITERAL = /'[^\']*'/
|
||||
DOUBLE_STRING_LITERAL = /"[^\"]*"/
|
||||
NUMBER_LITERAL = /-?\d+(\.\d+)?/
|
||||
DOTDOT = /\.\./
|
||||
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/
|
||||
WHITESPACE_OR_NOTHING = /\s*/
|
||||
|
||||
def initialize(input)
|
||||
@ss = StringScanner.new(input)
|
||||
@@ -33,15 +33,20 @@ module Liquid
|
||||
until @ss.eos?
|
||||
@ss.skip(WHITESPACE_OR_NOTHING)
|
||||
break if @ss.eos?
|
||||
|
||||
tok = if (t = @ss.scan(COMPARISON_OPERATOR)) then [:comparison, t]
|
||||
elsif (t = @ss.scan(SINGLE_STRING_LITERAL)) then [:string, t]
|
||||
elsif (t = @ss.scan(DOUBLE_STRING_LITERAL)) then [:string, t]
|
||||
elsif (t = @ss.scan(NUMBER_LITERAL)) then [:number, t]
|
||||
elsif (t = @ss.scan(IDENTIFIER)) then [:id, t]
|
||||
elsif (t = @ss.scan(DOTDOT)) then [:dotdot, t]
|
||||
tok = if (t = @ss.scan(COMPARISON_OPERATOR))
|
||||
[:comparison, t]
|
||||
elsif (t = @ss.scan(SINGLE_STRING_LITERAL))
|
||||
[:string, t]
|
||||
elsif (t = @ss.scan(DOUBLE_STRING_LITERAL))
|
||||
[:string, t]
|
||||
elsif (t = @ss.scan(NUMBER_LITERAL))
|
||||
[:number, t]
|
||||
elsif (t = @ss.scan(IDENTIFIER))
|
||||
[:id, t]
|
||||
elsif (t = @ss.scan(DOTDOT))
|
||||
[:dotdot, t]
|
||||
else
|
||||
c = @ss.getch
|
||||
c = @ss.getch
|
||||
if (s = SPECIALS[c])
|
||||
[s, c]
|
||||
else
|
||||
|
||||
@@ -20,7 +20,9 @@
|
||||
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:
|
||||
include: "Argument error in tag 'include' - Illegal template name"
|
||||
disabled:
|
||||
tag: "usage is not allowed in this context"
|
||||
|
||||
@@ -7,9 +7,11 @@ module Liquid
|
||||
|
||||
def initialize(options = {})
|
||||
@template_options = options ? options.dup : {}
|
||||
@locale = @template_options[:locale] ||= I18n.new
|
||||
|
||||
@locale = @template_options[:locale] ||= I18n.new
|
||||
@warnings = []
|
||||
self.depth = 0
|
||||
|
||||
self.depth = 0
|
||||
self.partial = false
|
||||
end
|
||||
|
||||
@@ -17,9 +19,22 @@ module Liquid
|
||||
@options[option_key]
|
||||
end
|
||||
|
||||
def new_block_body
|
||||
Liquid::BlockBody.new
|
||||
end
|
||||
|
||||
def new_tokenizer(markup, start_line_number: nil, for_liquid_tag: false)
|
||||
Tokenizer.new(markup, line_number: start_line_number, for_liquid_tag: for_liquid_tag)
|
||||
end
|
||||
|
||||
def parse_expression(markup)
|
||||
Expression.parse(markup)
|
||||
end
|
||||
|
||||
def partial=(value)
|
||||
@partial = value
|
||||
@options = value ? partial_options : @template_options
|
||||
|
||||
@error_mode = @options[:error_mode] || Template.error_mode
|
||||
end
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ module Liquid
|
||||
end
|
||||
|
||||
def initialize(node, callbacks)
|
||||
@node = node
|
||||
@node = node
|
||||
@callbacks = callbacks
|
||||
end
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
module Liquid
|
||||
class Parser
|
||||
def initialize(input)
|
||||
l = Lexer.new(input)
|
||||
l = Lexer.new(input)
|
||||
@tokens = l.tokenize
|
||||
@p = 0 # pointer to current location
|
||||
@p = 0 # pointer to current location
|
||||
end
|
||||
|
||||
def jump(point)
|
||||
@@ -14,8 +14,9 @@ module Liquid
|
||||
|
||||
def consume(type = nil)
|
||||
token = @tokens[@p]
|
||||
raise SyntaxError, "Expected #{type} but found #{@tokens[@p].first}" if type && token[0] != type
|
||||
|
||||
if type && token[0] != type
|
||||
raise SyntaxError, "Expected #{type} but found #{@tokens[@p].first}"
|
||||
end
|
||||
@p += 1
|
||||
token[1]
|
||||
end
|
||||
@@ -26,7 +27,6 @@ module Liquid
|
||||
def consume?(type)
|
||||
token = @tokens[@p]
|
||||
return false unless token && token[0] == type
|
||||
|
||||
@p += 1
|
||||
token[1]
|
||||
end
|
||||
@@ -36,7 +36,6 @@ module Liquid
|
||||
token = @tokens[@p]
|
||||
return false unless token && token[0] == :id
|
||||
return false unless token[1] == str
|
||||
|
||||
@p += 1
|
||||
token[1]
|
||||
end
|
||||
@@ -44,20 +43,23 @@ module Liquid
|
||||
def look(type, ahead = 0)
|
||||
tok = @tokens[@p + ahead]
|
||||
return false unless tok
|
||||
|
||||
tok[0] == type
|
||||
end
|
||||
|
||||
SINGLE_TOKEN_EXPRESSION_TYPES = %i[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)
|
||||
@@ -70,24 +72,29 @@ module Liquid
|
||||
end
|
||||
|
||||
def argument
|
||||
str = +''
|
||||
str = +""
|
||||
# might be a keyword argument (identifier: expression)
|
||||
str << consume << consume << ' ' if look(:id) && look(:colon, 1)
|
||||
if look(:id) && look(:colon, 1)
|
||||
str << consume << consume << ' '
|
||||
end
|
||||
|
||||
str << expression
|
||||
str
|
||||
end
|
||||
|
||||
def variable_signature
|
||||
str = consume(:id)
|
||||
while look(:open_square)
|
||||
str << consume
|
||||
str << expression
|
||||
str << consume(:close_square)
|
||||
end
|
||||
if look(:dot)
|
||||
str << consume
|
||||
str << variable_signature
|
||||
def variable_lookups
|
||||
str = +""
|
||||
loop do
|
||||
if look(:open_square)
|
||||
str << consume
|
||||
str << expression
|
||||
str << consume(:close_square)
|
||||
elsif look(:dot)
|
||||
str << consume
|
||||
str << consume(:id)
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
str
|
||||
end
|
||||
|
||||
@@ -2,16 +2,28 @@
|
||||
|
||||
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)
|
||||
when :lax then lax_parse(markup)
|
||||
when :warn
|
||||
begin
|
||||
return strict_parse_with_error_context(markup)
|
||||
strict_parse_with_error_context(markup)
|
||||
rescue SyntaxError => e
|
||||
parse_context.warnings << e
|
||||
return lax_parse(markup)
|
||||
lax_parse(markup)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -21,7 +33,7 @@ module Liquid
|
||||
def strict_parse_with_error_context(markup)
|
||||
strict_parse(markup)
|
||||
rescue SyntaxError => e
|
||||
e.line_number = line_number
|
||||
e.line_number = line_number
|
||||
e.markup_context = markup_context(markup)
|
||||
raise e
|
||||
end
|
||||
|
||||
24
lib/liquid/partial_cache.rb
Normal file
24
lib/liquid/partial_cache.rb
Normal file
@@ -0,0 +1,24 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class PartialCache
|
||||
def self.load(template_name, context:, parse_context:)
|
||||
cached_partials = (context.registers[:cached_partials] ||= {})
|
||||
cached = cached_partials[template_name]
|
||||
return cached if cached
|
||||
|
||||
file_system = (context.registers[:file_system] ||= Liquid::Template.file_system)
|
||||
source = file_system.read_template_file(template_name)
|
||||
|
||||
parse_context.partial = true
|
||||
|
||||
template_factory = (context.registers[:template_factory] ||= Liquid::TemplateFactory.new)
|
||||
template = template_factory.for(template_name)
|
||||
|
||||
partial = template.parse(source, parse_context)
|
||||
cached_partials[template_name] = partial
|
||||
ensure
|
||||
parse_context.partial = false
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -25,7 +25,7 @@ module Liquid
|
||||
# node.code
|
||||
#
|
||||
# # Which template and line number of this node.
|
||||
# # If top level, this will be "<root>".
|
||||
# # The top-level template name is `nil` by default, but can be set in the Liquid::Context before rendering.
|
||||
# node.partial
|
||||
# node.line_number
|
||||
#
|
||||
@@ -46,115 +46,94 @@ module Liquid
|
||||
include Enumerable
|
||||
|
||||
class Timing
|
||||
attr_reader :code, :partial, :line_number, :children
|
||||
attr_reader :code, :template_name, :line_number, :children
|
||||
attr_accessor :total_time
|
||||
alias_method :render_time, :total_time
|
||||
alias_method :partial, :template_name
|
||||
|
||||
def initialize(node, partial)
|
||||
@code = node.respond_to?(:raw) ? node.raw : node
|
||||
@partial = partial
|
||||
@line_number = node.respond_to?(:line_number) ? node.line_number : nil
|
||||
@children = []
|
||||
def initialize(code: nil, template_name: nil, line_number: nil)
|
||||
@code = code
|
||||
@template_name = template_name
|
||||
@line_number = line_number
|
||||
@children = []
|
||||
end
|
||||
|
||||
def self.start(node, partial)
|
||||
new(node, partial).tap(&:start)
|
||||
end
|
||||
|
||||
def start
|
||||
@start_time = Time.now
|
||||
end
|
||||
|
||||
def finish
|
||||
@end_time = Time.now
|
||||
end
|
||||
|
||||
def render_time
|
||||
@end_time - @start_time
|
||||
def self_time
|
||||
@self_time ||= begin
|
||||
total_children_time = 0.0
|
||||
@children.each do |child|
|
||||
total_children_time += child.total_time
|
||||
end
|
||||
@total_time - total_children_time
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.profile_node_render(node)
|
||||
if Profiler.current_profile && node.respond_to?(:render)
|
||||
Profiler.current_profile.start_node(node)
|
||||
output = yield
|
||||
Profiler.current_profile.end_node(node)
|
||||
output
|
||||
else
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
def self.profile_children(template_name)
|
||||
if Profiler.current_profile
|
||||
Profiler.current_profile.push_partial(template_name)
|
||||
output = yield
|
||||
Profiler.current_profile.pop_partial
|
||||
output
|
||||
else
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
def self.current_profile
|
||||
Thread.current[:liquid_profiler]
|
||||
end
|
||||
attr_reader :total_time
|
||||
alias_method :total_render_time, :total_time
|
||||
|
||||
def initialize
|
||||
@partial_stack = ['<root>']
|
||||
|
||||
@root_timing = Timing.new('', current_partial)
|
||||
@timing_stack = [@root_timing]
|
||||
|
||||
@render_start_at = Time.now
|
||||
@render_end_at = @render_start_at
|
||||
@root_children = []
|
||||
@current_children = nil
|
||||
@total_time = 0.0
|
||||
end
|
||||
|
||||
def start
|
||||
Thread.current[:liquid_profiler] = self
|
||||
@render_start_at = Time.now
|
||||
def profile(template_name, &block)
|
||||
# nested renders are done from a tag that already has a timing node
|
||||
return yield if @current_children
|
||||
|
||||
root_children = @root_children
|
||||
render_idx = root_children.length
|
||||
begin
|
||||
@current_children = root_children
|
||||
profile_node(template_name, &block)
|
||||
ensure
|
||||
@current_children = nil
|
||||
if (timing = root_children[render_idx])
|
||||
@total_time += timing.total_time
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def stop
|
||||
Thread.current[:liquid_profiler] = nil
|
||||
@render_end_at = Time.now
|
||||
end
|
||||
|
||||
def total_render_time
|
||||
@render_end_at - @render_start_at
|
||||
def children
|
||||
children = @root_children
|
||||
if children.length == 1
|
||||
children.first.children
|
||||
else
|
||||
children
|
||||
end
|
||||
end
|
||||
|
||||
def each(&block)
|
||||
@root_timing.children.each(&block)
|
||||
children.each(&block)
|
||||
end
|
||||
|
||||
def [](idx)
|
||||
@root_timing.children[idx]
|
||||
children[idx]
|
||||
end
|
||||
|
||||
def length
|
||||
@root_timing.children.length
|
||||
children.length
|
||||
end
|
||||
|
||||
def start_node(node)
|
||||
@timing_stack.push(Timing.start(node, current_partial))
|
||||
def profile_node(template_name, code: nil, line_number: nil)
|
||||
timing = Timing.new(code: code, template_name: template_name, line_number: line_number)
|
||||
parent_children = @current_children
|
||||
start_time = monotonic_time
|
||||
begin
|
||||
@current_children = timing.children
|
||||
yield
|
||||
ensure
|
||||
@current_children = parent_children
|
||||
timing.total_time = monotonic_time - start_time
|
||||
parent_children << timing
|
||||
end
|
||||
end
|
||||
|
||||
def end_node(_node)
|
||||
timing = @timing_stack.pop
|
||||
timing.finish
|
||||
private
|
||||
|
||||
@timing_stack.last.children << timing
|
||||
end
|
||||
|
||||
def current_partial
|
||||
@partial_stack.last
|
||||
end
|
||||
|
||||
def push_partial(partial_name)
|
||||
@partial_stack.push(partial_name)
|
||||
end
|
||||
|
||||
def pop_partial
|
||||
@partial_stack.pop
|
||||
def monotonic_time
|
||||
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,25 +1,35 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class BlockBody
|
||||
def render_node_with_profiling(context, output, node)
|
||||
Profiler.profile_node_render(node) do
|
||||
render_node_without_profiling(context, output, node)
|
||||
module BlockBodyProfilingHook
|
||||
def render_node(context, output, node)
|
||||
if (profiler = context.profiler)
|
||||
profiler.profile_node(context.template_name, code: node.raw, line_number: node.line_number) do
|
||||
super
|
||||
end
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
alias_method :render_node_without_profiling, :render_node
|
||||
alias_method :render_node, :render_node_with_profiling
|
||||
end
|
||||
BlockBody.prepend(BlockBodyProfilingHook)
|
||||
|
||||
class Include < Tag
|
||||
def render_to_output_buffer_with_profiling(context, output)
|
||||
Profiler.profile_children(context.evaluate(@template_name_expr).to_s) do
|
||||
render_to_output_buffer_without_profiling(context, output)
|
||||
end
|
||||
module DocumentProfilingHook
|
||||
def render_to_output_buffer(context, output)
|
||||
return super unless context.profiler
|
||||
context.profiler.profile(context.template_name) { super }
|
||||
end
|
||||
|
||||
alias_method :render_to_output_buffer_without_profiling, :render_to_output_buffer
|
||||
alias_method :render_to_output_buffer, :render_to_output_buffer_with_profiling
|
||||
end
|
||||
Document.prepend(DocumentProfilingHook)
|
||||
|
||||
module ContextProfilingHook
|
||||
attr_accessor :profiler
|
||||
|
||||
def new_isolated_subcontext
|
||||
new_context = super
|
||||
new_context.profiler = profiler
|
||||
new_context
|
||||
end
|
||||
end
|
||||
Context.prepend(ContextProfilingHook)
|
||||
end
|
||||
|
||||
@@ -4,7 +4,7 @@ module Liquid
|
||||
class RangeLookup
|
||||
def self.parse(start_markup, end_markup)
|
||||
start_obj = Expression.parse(start_markup)
|
||||
end_obj = Expression.parse(end_markup)
|
||||
end_obj = Expression.parse(end_markup)
|
||||
if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate)
|
||||
new(start_obj, end_obj)
|
||||
else
|
||||
@@ -14,12 +14,12 @@ module Liquid
|
||||
|
||||
def initialize(start_obj, end_obj)
|
||||
@start_obj = start_obj
|
||||
@end_obj = end_obj
|
||||
@end_obj = end_obj
|
||||
end
|
||||
|
||||
def evaluate(context)
|
||||
start_int = to_integer(context.evaluate(@start_obj))
|
||||
end_int = to_integer(context.evaluate(@end_obj))
|
||||
end_int = to_integer(context.evaluate(@end_obj))
|
||||
start_int..end_int
|
||||
end
|
||||
|
||||
|
||||
6
lib/liquid/register.rb
Normal file
6
lib/liquid/register.rb
Normal file
@@ -0,0 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Register
|
||||
end
|
||||
end
|
||||
@@ -2,24 +2,61 @@
|
||||
|
||||
module Liquid
|
||||
class ResourceLimits
|
||||
attr_accessor :render_length, :render_score, :assign_score,
|
||||
:render_length_limit, :render_score_limit, :assign_score_limit
|
||||
attr_accessor :render_length_limit, :render_score_limit, :assign_score_limit
|
||||
attr_reader :render_score, :assign_score
|
||||
|
||||
def initialize(limits)
|
||||
@render_length_limit = limits[:render_length_limit]
|
||||
@render_score_limit = limits[:render_score_limit]
|
||||
@assign_score_limit = limits[:assign_score_limit]
|
||||
@render_score_limit = limits[:render_score_limit]
|
||||
@assign_score_limit = limits[:assign_score_limit]
|
||||
reset
|
||||
end
|
||||
|
||||
def increment_render_score(amount)
|
||||
@render_score += amount
|
||||
raise_limits_reached if @render_score_limit && @render_score > @render_score_limit
|
||||
end
|
||||
|
||||
def increment_assign_score(amount)
|
||||
@assign_score += amount
|
||||
raise_limits_reached if @assign_score_limit && @assign_score > @assign_score_limit
|
||||
end
|
||||
|
||||
# update either render_length or assign_score based on whether or not the writes are captured
|
||||
def increment_write_score(output)
|
||||
if (last_captured = @last_capture_length)
|
||||
captured = output.bytesize
|
||||
increment = captured - last_captured
|
||||
@last_capture_length = captured
|
||||
increment_assign_score(increment)
|
||||
elsif @render_length_limit && output.bytesize > @render_length_limit
|
||||
raise_limits_reached
|
||||
end
|
||||
end
|
||||
|
||||
def raise_limits_reached
|
||||
@reached_limit = true
|
||||
raise MemoryError, "Memory limits exceeded"
|
||||
end
|
||||
|
||||
def reached?
|
||||
(@render_length_limit && @render_length > @render_length_limit) ||
|
||||
(@render_score_limit && @render_score > @render_score_limit) ||
|
||||
(@assign_score_limit && @assign_score > @assign_score_limit)
|
||||
@reached_limit
|
||||
end
|
||||
|
||||
def reset
|
||||
@render_length = @render_score = @assign_score = 0
|
||||
@reached_limit = false
|
||||
@last_capture_length = nil
|
||||
@render_score = @assign_score = 0
|
||||
end
|
||||
|
||||
def with_capture
|
||||
old_capture_length = @last_capture_length
|
||||
begin
|
||||
@last_capture_length = 0
|
||||
yield
|
||||
ensure
|
||||
@last_capture_length = old_capture_length
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,6 +5,7 @@ require 'bigdecimal'
|
||||
|
||||
module Liquid
|
||||
module StandardFilters
|
||||
MAX_INT = (1 << 31) - 1
|
||||
HTML_ESCAPE = {
|
||||
'&' => '&',
|
||||
'>' => '>',
|
||||
@@ -12,13 +13,13 @@ module Liquid
|
||||
'"' => '"',
|
||||
"'" => ''',
|
||||
}.freeze
|
||||
HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/.freeze
|
||||
STRIP_HTML_BLOCKS = Regexp.union(
|
||||
HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/
|
||||
STRIP_HTML_BLOCKS = Regexp.union(
|
||||
%r{<script.*?</script>}m,
|
||||
/<!--.*?-->/m,
|
||||
%r{<style.*?</style>}m
|
||||
)
|
||||
STRIP_HTML_TAGS = /<.*?>/m.freeze
|
||||
STRIP_HTML_TAGS = /<.*?>/m
|
||||
|
||||
# Return the size of an array or of an string
|
||||
def size(input)
|
||||
@@ -41,7 +42,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
|
||||
|
||||
@@ -74,25 +75,36 @@ module Liquid
|
||||
end
|
||||
|
||||
# Truncate a string down to x characters
|
||||
def truncate(input, length = 50, truncate_string = '...')
|
||||
def truncate(input, length = 50, truncate_string = "...")
|
||||
return if input.nil?
|
||||
|
||||
input_str = input.to_s
|
||||
length = Utils.to_integer(length)
|
||||
length = Utils.to_integer(length)
|
||||
|
||||
truncate_string_str = truncate_string.to_s
|
||||
|
||||
l = length - truncate_string_str.length
|
||||
l = 0 if l < 0
|
||||
|
||||
input_str.length > length ? input_str[0...l].concat(truncate_string_str) : input_str
|
||||
end
|
||||
|
||||
def truncatewords(input, words = 15, truncate_string = '...')
|
||||
def truncatewords(input, words = 15, truncate_string = "...")
|
||||
return if input.nil?
|
||||
|
||||
wordlist = input.to_s.split
|
||||
input = input.to_s
|
||||
words = Utils.to_integer(words)
|
||||
l = words - 1
|
||||
l = 0 if l < 0
|
||||
wordlist.length > l ? wordlist[0..l].join(' ').concat(truncate_string.to_s) : input
|
||||
words = 1 if words <= 0
|
||||
|
||||
wordlist = begin
|
||||
input.split(" ", words + 1)
|
||||
rescue RangeError
|
||||
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.pop
|
||||
wordlist.join(" ").concat(truncate_string.to_s)
|
||||
end
|
||||
|
||||
# Split input string into an array of substrings separated by given pattern.
|
||||
@@ -117,7 +129,7 @@ module Liquid
|
||||
end
|
||||
|
||||
def strip_html(input)
|
||||
empty = ''
|
||||
empty = ''
|
||||
result = input.to_s.gsub(STRIP_HTML_BLOCKS, empty)
|
||||
result.gsub!(STRIP_HTML_TAGS, empty)
|
||||
result
|
||||
@@ -130,13 +142,13 @@ module Liquid
|
||||
|
||||
# Join elements of the array with certain character between them
|
||||
def join(input, glue = ' ')
|
||||
InputIterator.new(input).join(glue)
|
||||
InputIterator.new(input, context).join(glue)
|
||||
end
|
||||
|
||||
# Sort elements of the array
|
||||
# provide optional property with which to sort an array of hashes or drops
|
||||
def sort(input, property = nil)
|
||||
ary = InputIterator.new(input)
|
||||
ary = InputIterator.new(input, context)
|
||||
|
||||
return [] if ary.empty?
|
||||
|
||||
@@ -156,7 +168,7 @@ module Liquid
|
||||
# Sort elements of an array ignoring case if strings
|
||||
# provide optional property with which to sort an array of hashes or drops
|
||||
def sort_natural(input, property = nil)
|
||||
ary = InputIterator.new(input)
|
||||
ary = InputIterator.new(input, context)
|
||||
|
||||
return [] if ary.empty?
|
||||
|
||||
@@ -176,7 +188,7 @@ module Liquid
|
||||
# Filter the elements of an array to those with a certain property value.
|
||||
# By default the target is any truthy value.
|
||||
def where(input, property, target_value = nil)
|
||||
ary = InputIterator.new(input)
|
||||
ary = InputIterator.new(input, context)
|
||||
|
||||
if ary.empty?
|
||||
[]
|
||||
@@ -198,7 +210,7 @@ module Liquid
|
||||
# Remove duplicate elements from an array
|
||||
# provide optional property with which to determine uniqueness
|
||||
def uniq(input, property = nil)
|
||||
ary = InputIterator.new(input)
|
||||
ary = InputIterator.new(input, context)
|
||||
|
||||
if property.nil?
|
||||
ary.uniq
|
||||
@@ -215,16 +227,16 @@ module Liquid
|
||||
|
||||
# Reverse the elements of an array
|
||||
def reverse(input)
|
||||
ary = InputIterator.new(input)
|
||||
ary = InputIterator.new(input, context)
|
||||
ary.reverse
|
||||
end
|
||||
|
||||
# map/collect on a given property
|
||||
def map(input, property)
|
||||
InputIterator.new(input).map do |e|
|
||||
InputIterator.new(input, context).map do |e|
|
||||
e = e.call if e.is_a?(Proc)
|
||||
|
||||
if property == 'to_liquid'
|
||||
if property == "to_liquid"
|
||||
e
|
||||
elsif e.respond_to?(:[])
|
||||
r = e[property]
|
||||
@@ -238,7 +250,7 @@ module Liquid
|
||||
# Remove nils within an array
|
||||
# provide optional property with which to check for nil
|
||||
def compact(input, property = nil)
|
||||
ary = InputIterator.new(input)
|
||||
ary = InputIterator.new(input, context)
|
||||
|
||||
if property.nil?
|
||||
ary.compact
|
||||
@@ -279,9 +291,10 @@ module Liquid
|
||||
end
|
||||
|
||||
def concat(input, array)
|
||||
raise ArgumentError, 'concat filter requires an array argument' unless array.respond_to?(:to_ary)
|
||||
|
||||
InputIterator.new(input).concat(array)
|
||||
unless array.respond_to?(:to_ary)
|
||||
raise ArgumentError, "concat filter requires an array argument"
|
||||
end
|
||||
InputIterator.new(input, context).concat(array)
|
||||
end
|
||||
|
||||
# prepend a string to another
|
||||
@@ -291,7 +304,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
|
||||
@@ -422,16 +435,26 @@ module Liquid
|
||||
result.is_a?(BigDecimal) ? result.to_f : result
|
||||
end
|
||||
|
||||
def default(input, default_value = '')
|
||||
if !input || input.respond_to?(:empty?) && input.empty?
|
||||
default_value
|
||||
else
|
||||
input
|
||||
end
|
||||
# Set a default value when the input is nil, false or empty
|
||||
#
|
||||
# Example:
|
||||
# {{ product.title | default: "No Title" }}
|
||||
#
|
||||
# Use `allow_false` when an input should only be tested against nil or empty and not false.
|
||||
#
|
||||
# Example:
|
||||
# {{ product.title | default: "No Title", allow_false: true }}
|
||||
#
|
||||
def default(input, default_value = '', options = {})
|
||||
options = {} unless options.is_a?(Hash)
|
||||
false_check = options['allow_false'] ? input.nil? : !Liquid::Utils.to_liquid_value(input)
|
||||
false_check || (input.respond_to?(:empty?) && input.empty?) ? default_value : input
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :context
|
||||
|
||||
def raise_property_error(property)
|
||||
raise Liquid::ArgumentError, "cannot select the property '#{property}'"
|
||||
end
|
||||
@@ -460,8 +483,9 @@ module Liquid
|
||||
class InputIterator
|
||||
include Enumerable
|
||||
|
||||
def initialize(input)
|
||||
@input = if input.is_a?(Array)
|
||||
def initialize(input, context)
|
||||
@context = context
|
||||
@input = if input.is_a?(Array)
|
||||
input.flatten
|
||||
elsif input.is_a?(Hash)
|
||||
[input]
|
||||
@@ -499,6 +523,7 @@ module Liquid
|
||||
|
||||
def each
|
||||
@input.each do |e|
|
||||
e.context = @context if e.respond_to?(:context=)
|
||||
yield(e.respond_to?(:to_liquid) ? e.to_liquid : e)
|
||||
end
|
||||
end
|
||||
|
||||
44
lib/liquid/static_registers.rb
Normal file
44
lib/liquid/static_registers.rb
Normal file
@@ -0,0 +1,44 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class StaticRegisters
|
||||
attr_reader :static
|
||||
|
||||
def initialize(registers = {})
|
||||
@static = registers.is_a?(StaticRegisters) ? registers.static : registers
|
||||
@registers = {}
|
||||
end
|
||||
|
||||
def []=(key, value)
|
||||
@registers[key] = value
|
||||
end
|
||||
|
||||
def [](key)
|
||||
if @registers.key?(key)
|
||||
@registers[key]
|
||||
else
|
||||
@static[key]
|
||||
end
|
||||
end
|
||||
|
||||
def delete(key)
|
||||
@registers.delete(key)
|
||||
end
|
||||
|
||||
UNDEFINED = Object.new
|
||||
|
||||
def fetch(key, default = UNDEFINED, &block)
|
||||
if @registers.key?(key)
|
||||
@registers.fetch(key)
|
||||
elsif default != UNDEFINED
|
||||
@static.fetch(key, default, &block)
|
||||
else
|
||||
@static.fetch(key, &block)
|
||||
end
|
||||
end
|
||||
|
||||
def key?(key)
|
||||
@registers.key?(key) || @static.key?(key)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,70 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'set'
|
||||
|
||||
module Liquid
|
||||
# Strainer is the parent class for the filters system.
|
||||
# New filters are mixed into the strainer class which is then instantiated for each liquid template render run.
|
||||
#
|
||||
# The Strainer only allows method calls defined in filters given to it via Strainer.global_filter,
|
||||
# Context#add_filters or Template.register_filter
|
||||
class Strainer #:nodoc:
|
||||
@global_strainer = Class.new(Strainer) do
|
||||
@filter_methods = Set.new
|
||||
end
|
||||
@strainer_class_cache = Hash.new do |hash, filters|
|
||||
hash[filters] = Class.new(@global_strainer) do
|
||||
@filter_methods = Strainer.global_strainer.filter_methods.dup
|
||||
filters.each { |f| add_filter(f) }
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(context)
|
||||
@context = context
|
||||
end
|
||||
|
||||
class << self
|
||||
attr_accessor :strainer_class_cache, :global_strainer
|
||||
attr_reader :filter_methods
|
||||
end
|
||||
|
||||
def self.add_filter(filter)
|
||||
raise ArgumentError, "Expected module but got: #{filter.class}" unless filter.is_a?(Module)
|
||||
|
||||
unless include?(filter)
|
||||
invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) }
|
||||
if invokable_non_public_methods.any?
|
||||
raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}"
|
||||
else
|
||||
send(:include, filter)
|
||||
@filter_methods.merge(filter.public_instance_methods.map(&:to_s))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.global_filter(filter)
|
||||
@strainer_class_cache.clear
|
||||
@global_strainer.add_filter(filter)
|
||||
end
|
||||
|
||||
def self.invokable?(method)
|
||||
@filter_methods.include?(method.to_s)
|
||||
end
|
||||
|
||||
def self.create(context, filters = [])
|
||||
@strainer_class_cache[filters].new(context)
|
||||
end
|
||||
|
||||
def invoke(method, *args)
|
||||
if self.class.invokable?(method)
|
||||
send(method, *args)
|
||||
elsif @context&.strict_filters
|
||||
raise Liquid::UndefinedFilter, "undefined filter #{method}"
|
||||
else
|
||||
args.first
|
||||
end
|
||||
rescue ::ArgumentError => e
|
||||
raise Liquid::ArgumentError, e.message, e.backtrace
|
||||
end
|
||||
end
|
||||
end
|
||||
36
lib/liquid/strainer_factory.rb
Normal file
36
lib/liquid/strainer_factory.rb
Normal file
@@ -0,0 +1,36 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# StrainerFactory is the factory for the filters system.
|
||||
module StrainerFactory
|
||||
extend self
|
||||
|
||||
def add_global_filter(filter)
|
||||
strainer_class_cache.clear
|
||||
global_filters << filter
|
||||
end
|
||||
|
||||
def create(context, filters = [])
|
||||
strainer_from_cache(filters).new(context)
|
||||
end
|
||||
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
def strainer_class_cache
|
||||
@strainer_class_cache ||= {}
|
||||
end
|
||||
end
|
||||
end
|
||||
53
lib/liquid/strainer_template.rb
Normal file
53
lib/liquid/strainer_template.rb
Normal file
@@ -0,0 +1,53 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'set'
|
||||
|
||||
module Liquid
|
||||
# StrainerTemplate is the computed class for the filters system.
|
||||
# New filters are mixed into the strainer class which is then instantiated for each liquid template render run.
|
||||
#
|
||||
# The Strainer only allows method calls defined in filters given to it via StrainerFactory.add_global_filter,
|
||||
# Context#add_filters or Template.register_filter
|
||||
class StrainerTemplate
|
||||
def initialize(context)
|
||||
@context = context
|
||||
end
|
||||
|
||||
class << self
|
||||
def add_filter(filter)
|
||||
return if include?(filter)
|
||||
|
||||
invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) }
|
||||
if invokable_non_public_methods.any?
|
||||
raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}"
|
||||
end
|
||||
|
||||
include(filter)
|
||||
|
||||
filter_methods.merge(filter.public_instance_methods.map(&:to_s))
|
||||
end
|
||||
|
||||
def invokable?(method)
|
||||
filter_methods.include?(method.to_s)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def filter_methods
|
||||
@filter_methods ||= Set.new
|
||||
end
|
||||
end
|
||||
|
||||
def invoke(method, *args)
|
||||
if self.class.invokable?(method)
|
||||
send(method, *args)
|
||||
elsif @context.strict_filters
|
||||
raise Liquid::UndefinedFilter, "undefined filter #{method}"
|
||||
else
|
||||
args.first
|
||||
end
|
||||
rescue ::ArgumentError => e
|
||||
raise Liquid::ArgumentError, e.message, e.backtrace
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -4,10 +4,10 @@ module Liquid
|
||||
class TablerowloopDrop < Drop
|
||||
def initialize(length, cols)
|
||||
@length = length
|
||||
@row = 1
|
||||
@col = 1
|
||||
@cols = cols
|
||||
@index = 0
|
||||
@row = 1
|
||||
@col = 1
|
||||
@cols = cols
|
||||
@index = 0
|
||||
end
|
||||
|
||||
attr_reader :length, :col, :row
|
||||
|
||||
@@ -7,23 +7,30 @@ module Liquid
|
||||
include ParserSwitching
|
||||
|
||||
class << self
|
||||
def parse(tag_name, markup, tokenizer, options)
|
||||
tag = new(tag_name, markup, options)
|
||||
def parse(tag_name, markup, tokenizer, parse_context)
|
||||
tag = new(tag_name, markup, parse_context)
|
||||
tag.parse(tokenizer)
|
||||
tag
|
||||
end
|
||||
|
||||
def disable_tags(*tag_names)
|
||||
@disabled_tags ||= []
|
||||
@disabled_tags.concat(tag_names)
|
||||
prepend(Disabler)
|
||||
end
|
||||
|
||||
private :new
|
||||
end
|
||||
|
||||
def initialize(tag_name, markup, parse_context)
|
||||
@tag_name = tag_name
|
||||
@markup = markup
|
||||
@tag_name = tag_name
|
||||
@markup = markup
|
||||
@parse_context = parse_context
|
||||
@line_number = parse_context.line_number
|
||||
@line_number = parse_context.line_number
|
||||
end
|
||||
|
||||
def parse(_tokens); end
|
||||
def parse(_tokens)
|
||||
end
|
||||
|
||||
def raw
|
||||
"#{@tag_name} #{@markup}"
|
||||
@@ -48,5 +55,11 @@ module Liquid
|
||||
def blank?
|
||||
false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse_expression(markup)
|
||||
parse_context.parse_expression(markup)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
22
lib/liquid/tag/disableable.rb
Normal file
22
lib/liquid/tag/disableable.rb
Normal file
@@ -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
|
||||
21
lib/liquid/tag/disabler.rb
Normal file
21
lib/liquid/tag/disabler.rb
Normal file
@@ -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
|
||||
@@ -10,24 +10,29 @@ module Liquid
|
||||
# {{ foo }}
|
||||
#
|
||||
class Assign < Tag
|
||||
SYNTAX = /(#{VARIABLE_SIGNATURE}+)\s*=\s*(.*)\s*/om.freeze
|
||||
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
|
||||
|
||||
# @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)
|
||||
if markup =~ Syntax
|
||||
@to = Regexp.last_match(1)
|
||||
@from = Variable.new(Regexp.last_match(2), parse_context)
|
||||
else
|
||||
raise SyntaxError, options[:locale].t('errors.syntax.assign')
|
||||
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
|
||||
|
||||
@@ -40,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
|
||||
|
||||
|
||||
@@ -13,22 +13,22 @@ module Liquid
|
||||
# in a sidebar or footer.
|
||||
#
|
||||
class Capture < Block
|
||||
SYNTAX = /(#{VARIABLE_SIGNATURE}+)/o.freeze
|
||||
Syntax = /(#{VariableSignature}+)/o
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
if markup =~ SYNTAX
|
||||
if markup =~ Syntax
|
||||
@to = Regexp.last_match(1)
|
||||
else
|
||||
raise SyntaxError, options[:locale].t('errors.syntax.capture')
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.capture")
|
||||
end
|
||||
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
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
module Liquid
|
||||
class Case < Block
|
||||
SYNTAX = /(#{QUOTED_FRAGMENT})/o.freeze
|
||||
WHEN_SYNTAX = /(#{QUOTED_FRAGMENT})(?:(?:\s+or\s+|\s*\,\s*)(#{QUOTED_FRAGMENT}.*))?/om.freeze
|
||||
Syntax = /(#{QuotedFragment})/o
|
||||
WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om
|
||||
|
||||
attr_reader :blocks, :left
|
||||
|
||||
@@ -11,16 +11,24 @@ module Liquid
|
||||
super
|
||||
@blocks = []
|
||||
|
||||
if markup =~ SYNTAX
|
||||
@left = Expression.parse(Regexp.last_match(1))
|
||||
if markup =~ Syntax
|
||||
@left = parse_expression(Regexp.last_match(1))
|
||||
else
|
||||
raise SyntaxError, options[:locale].t('errors.syntax.case')
|
||||
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
|
||||
@@ -39,16 +47,21 @@ module Liquid
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
context.stack do
|
||||
execute_else_block = true
|
||||
execute_else_block = true
|
||||
|
||||
@blocks.each do |block|
|
||||
if block.else?
|
||||
block.attachment.render_to_output_buffer(context, output) if execute_else_block
|
||||
elsif block.evaluate(context)
|
||||
execute_else_block = false
|
||||
block.attachment.render_to_output_buffer(context, output)
|
||||
end
|
||||
@blocks.each do |block|
|
||||
if block.else?
|
||||
block.attachment.render_to_output_buffer(context, output) if execute_else_block
|
||||
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
|
||||
end
|
||||
|
||||
@@ -58,24 +71,28 @@ module Liquid
|
||||
private
|
||||
|
||||
def record_when_condition(markup)
|
||||
body = BlockBody.new
|
||||
body = new_body
|
||||
|
||||
while markup
|
||||
raise SyntaxError, options[:locale].t('errors.syntax.case_invalid_when') unless markup =~ WHEN_SYNTAX
|
||||
unless markup =~ WhenSyntax
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.case_invalid_when")
|
||||
end
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
def record_else_condition(markup)
|
||||
raise SyntaxError, options[:locale].t('errors.syntax.case_invalid_else') unless markup.strip.empty?
|
||||
unless markup.strip.empty?
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.case_invalid_else")
|
||||
end
|
||||
|
||||
block = ElseCondition.new
|
||||
block.attach(BlockBody.new)
|
||||
block.attach(new_body)
|
||||
@blocks << block
|
||||
end
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@ module Liquid
|
||||
output
|
||||
end
|
||||
|
||||
def unknown_tag(_tag, _markup, _tokens); end
|
||||
def unknown_tag(_tag, _markup, _tokens)
|
||||
end
|
||||
|
||||
def blank?
|
||||
true
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -14,47 +14,45 @@ module Liquid
|
||||
# <div class="green"> Item five</div>
|
||||
#
|
||||
class Cycle < Tag
|
||||
SIMPLE_SYNTAX = /\A#{QUOTED_FRAGMENT}+/o.freeze
|
||||
NAMED_SYNTAX = /\A(#{QUOTED_FRAGMENT})\s*\:\s*(.*)/om.freeze
|
||||
SimpleSyntax = /\A#{QuotedFragment}+/o
|
||||
NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om
|
||||
|
||||
attr_reader :variables
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
case markup
|
||||
when NAMED_SYNTAX
|
||||
when NamedSyntax
|
||||
@variables = variables_from_string(Regexp.last_match(2))
|
||||
@name = Expression.parse(Regexp.last_match(1))
|
||||
when SIMPLE_SYNTAX
|
||||
@name = parse_expression(Regexp.last_match(1))
|
||||
when SimpleSyntax
|
||||
@variables = variables_from_string(markup)
|
||||
@name = @variables.to_s
|
||||
@name = @variables.to_s
|
||||
else
|
||||
raise SyntaxError, options[:locale].t('errors.syntax.cycle')
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.cycle")
|
||||
end
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
context.registers[:cycle] ||= {}
|
||||
|
||||
context.stack do
|
||||
key = context.evaluate(@name)
|
||||
iteration = context.registers[:cycle][key].to_i
|
||||
key = context.evaluate(@name)
|
||||
iteration = context.registers[:cycle][key].to_i
|
||||
|
||||
val = context.evaluate(@variables[iteration])
|
||||
val = context.evaluate(@variables[iteration])
|
||||
|
||||
if val.is_a?(Array)
|
||||
val = val.join
|
||||
elsif !val.is_a?(String)
|
||||
val = val.to_s
|
||||
end
|
||||
|
||||
output << val
|
||||
|
||||
iteration += 1
|
||||
iteration = 0 if iteration >= @variables.size
|
||||
context.registers[:cycle][key] = iteration
|
||||
if val.is_a?(Array)
|
||||
val = val.join
|
||||
elsif !val.is_a?(String)
|
||||
val = val.to_s
|
||||
end
|
||||
|
||||
output << val
|
||||
|
||||
iteration += 1
|
||||
iteration = 0 if iteration >= @variables.size
|
||||
|
||||
context.registers[:cycle][key] = iteration
|
||||
output
|
||||
end
|
||||
|
||||
@@ -62,8 +60,8 @@ module Liquid
|
||||
|
||||
def variables_from_string(markup)
|
||||
markup.split(',').collect do |var|
|
||||
var =~ /\s*(#{QUOTED_FRAGMENT})\s*/o
|
||||
Regexp.last_match(1) ? Expression.parse(Regexp.last_match(1)) : nil
|
||||
var =~ /\s*(#{QuotedFragment})\s*/o
|
||||
Regexp.last_match(1) ? parse_expression(Regexp.last_match(1)) : nil
|
||||
end.compact
|
||||
end
|
||||
|
||||
|
||||
34
lib/liquid/tags/echo.rb
Normal file
34
lib/liquid/tags/echo.rb
Normal file
@@ -0,0 +1,34 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# Echo outputs an expression
|
||||
#
|
||||
# {% echo monkey %}
|
||||
# {% echo user.name %}
|
||||
#
|
||||
# This is identical to variable output syntax, like {{ foo }}, but works
|
||||
# inside {% liquid %} tags. The full syntax is supported, including filters:
|
||||
#
|
||||
# {% echo user | link %}
|
||||
#
|
||||
class Echo < Tag
|
||||
attr_reader :variable
|
||||
|
||||
def initialize(tag_name, markup, parse_context)
|
||||
super
|
||||
@variable = Variable.new(markup, parse_context)
|
||||
end
|
||||
|
||||
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)
|
||||
end
|
||||
@@ -46,7 +46,7 @@ module Liquid
|
||||
# forloop.parentloop:: Provides access to the parent loop, if present.
|
||||
#
|
||||
class For < Block
|
||||
SYNTAX = /\A(#{VARIABLE_SEGMENT}+)\s+in\s+(#{QUOTED_FRAGMENT}+)\s*(reversed)?/o.freeze
|
||||
Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
|
||||
|
||||
attr_reader :collection_name, :variable_name, :limit, :from
|
||||
|
||||
@@ -54,14 +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
|
||||
@@ -70,8 +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)
|
||||
@@ -89,35 +94,35 @@ module Liquid
|
||||
protected
|
||||
|
||||
def lax_parse(markup)
|
||||
if markup =~ SYNTAX
|
||||
@variable_name = Regexp.last_match(1)
|
||||
collection_name = Regexp.last_match(2)
|
||||
@reversed = !!Regexp.last_match(3)
|
||||
@name = "#{@variable_name}-#{collection_name}"
|
||||
@collection_name = Expression.parse(collection_name)
|
||||
markup.scan(TAG_ATTRIBUTES) do |key, value|
|
||||
if markup =~ Syntax
|
||||
@variable_name = Regexp.last_match(1)
|
||||
collection_name = Regexp.last_match(2)
|
||||
@reversed = !!Regexp.last_match(3)
|
||||
@name = "#{@variable_name}-#{collection_name}"
|
||||
@collection_name = parse_expression(collection_name)
|
||||
markup.scan(TagAttributes) do |key, value|
|
||||
set_attribute(key, value)
|
||||
end
|
||||
else
|
||||
raise SyntaxError, options[:locale].t('errors.syntax.for')
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.for")
|
||||
end
|
||||
end
|
||||
|
||||
def strict_parse(markup)
|
||||
p = Parser.new(markup)
|
||||
@variable_name = p.consume(:id)
|
||||
raise SyntaxError, options[:locale].t('errors.syntax.for_invalid_in') unless p.id?('in')
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in") unless p.id?('in')
|
||||
|
||||
collection_name = p.expression
|
||||
@name = "#{@variable_name}-#{collection_name}"
|
||||
@collection_name = Expression.parse(collection_name)
|
||||
collection_name = p.expression
|
||||
@collection_name = parse_expression(collection_name)
|
||||
|
||||
@name = "#{@variable_name}-#{collection_name}"
|
||||
@reversed = p.id?('reversed')
|
||||
|
||||
while p.look(:id) && p.look(:colon, 1)
|
||||
unless (attribute = p.id?('limit') || p.id?('offset'))
|
||||
raise SyntaxError, options[:locale].t('errors.syntax.for_invalid_attribute')
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_attribute")
|
||||
end
|
||||
|
||||
p.consume
|
||||
set_attribute(attribute, p.expression)
|
||||
end
|
||||
@@ -160,7 +165,7 @@ module Liquid
|
||||
|
||||
def render_segment(context, output, segment)
|
||||
for_stack = context.registers[:for_stack] ||= []
|
||||
length = segment.length
|
||||
length = segment.length
|
||||
|
||||
context.stack do
|
||||
loop_vars = Liquid::ForloopDrop.new(@name, length, for_stack[-1])
|
||||
@@ -177,7 +182,6 @@ module Liquid
|
||||
|
||||
# Handle any interrupts if they exist.
|
||||
next unless context.interrupt?
|
||||
|
||||
interrupt = context.pop_interrupt
|
||||
break if interrupt.is_a?(BreakInterrupt)
|
||||
next if interrupt.is_a?(ContinueInterrupt)
|
||||
@@ -194,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
|
||||
|
||||
|
||||
@@ -12,9 +12,9 @@ module Liquid
|
||||
# There are {% if count < 5 %} less {% else %} more {% endif %} items than you need.
|
||||
#
|
||||
class If < Block
|
||||
SYNTAX = /(#{QUOTED_FRAGMENT})\s*([=!<>a-z_]+)?\s*(#{QUOTED_FRAGMENT})?/o.freeze
|
||||
EXPRESSIONS_AND_OPERATORS = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QUOTED_FRAGMENT}|\S+)\s*)+)/o.freeze
|
||||
BOOLEAN_OPERATORS = %w[and or].freeze
|
||||
Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o
|
||||
ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o
|
||||
BOOLEAN_OPERATORS = %w(and or).freeze
|
||||
|
||||
attr_reader :blocks
|
||||
|
||||
@@ -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 %w[elsif else].include?(tag)
|
||||
if ELSE_TAG_NAMES.include?(tag)
|
||||
push_block(tag, markup)
|
||||
else
|
||||
super
|
||||
@@ -42,9 +49,13 @@ module Liquid
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
context.stack do
|
||||
@blocks.each do |block|
|
||||
return block.attachment.render_to_output_buffer(context, output) if block.evaluate(context)
|
||||
@blocks.each do |block|
|
||||
result = Liquid::Utils.to_liquid_value(
|
||||
block.evaluate(context)
|
||||
)
|
||||
|
||||
if result
|
||||
return block.attachment.render_to_output_buffer(context, output)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -61,23 +72,26 @@ 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(EXPRESSIONS_AND_OPERATORS)
|
||||
raise SyntaxError, options[:locale].t('errors.syntax.if') unless expressions.pop =~ SYNTAX
|
||||
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)))
|
||||
raise SyntaxError, options[:locale].t('errors.syntax.if') unless BOOLEAN_OPERATORS.include?(operator)
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop.to_s =~ Syntax
|
||||
|
||||
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
|
||||
end
|
||||
@@ -104,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)
|
||||
|
||||
@@ -3,14 +3,12 @@
|
||||
module Liquid
|
||||
class Ifchanged < Block
|
||||
def render_to_output_buffer(context, output)
|
||||
context.stack do
|
||||
block_output = +''
|
||||
super(context, block_output)
|
||||
block_output = +''
|
||||
super(context, block_output)
|
||||
|
||||
if block_output != context.registers[:ifchanged]
|
||||
context.registers[:ifchanged] = block_output
|
||||
output << block_output
|
||||
end
|
||||
if block_output != context.registers[:ifchanged]
|
||||
context.registers[:ifchanged] = block_output
|
||||
output << block_output
|
||||
end
|
||||
|
||||
output
|
||||
|
||||
@@ -16,7 +16,10 @@ module Liquid
|
||||
# {% include 'product' for products %}
|
||||
#
|
||||
class Include < Tag
|
||||
SYNTAX = /(#{QUOTED_FRAGMENT}+)(\s+(?:with|for)\s+(#{QUOTED_FRAGMENT}+))?/o.freeze
|
||||
prepend Tag::Disableable
|
||||
|
||||
SYNTAX = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
|
||||
Syntax = SYNTAX
|
||||
|
||||
attr_reader :template_name_expr, :variable_name_expr, :attributes
|
||||
|
||||
@@ -28,27 +31,34 @@ module Liquid
|
||||
template_name = Regexp.last_match(1)
|
||||
variable_name = Regexp.last_match(3)
|
||||
|
||||
@variable_name_expr = variable_name ? Expression.parse(variable_name) : nil
|
||||
@template_name_expr = Expression.parse(template_name)
|
||||
@attributes = {}
|
||||
@alias_name = Regexp.last_match(5)
|
||||
@variable_name_expr = variable_name ? parse_expression(variable_name) : nil
|
||||
@template_name_expr = parse_expression(template_name)
|
||||
@attributes = {}
|
||||
|
||||
markup.scan(TAG_ATTRIBUTES) do |key, value|
|
||||
@attributes[key] = Expression.parse(value)
|
||||
markup.scan(TagAttributes) do |key, value|
|
||||
@attributes[key] = parse_expression(value)
|
||||
end
|
||||
|
||||
else
|
||||
raise SyntaxError, options[:locale].t('errors.syntax.include')
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.include")
|
||||
end
|
||||
end
|
||||
|
||||
def parse(_tokens); end
|
||||
def parse(_tokens)
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
template_name = context.evaluate(@template_name_expr)
|
||||
raise ArgumentError, options[:locale].t('errors.argument.include') unless template_name
|
||||
raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name
|
||||
|
||||
partial = load_cached_partial(template_name, context)
|
||||
context_variable_name = template_name.split('/').last
|
||||
partial = PartialCache.load(
|
||||
template_name,
|
||||
context: context,
|
||||
parse_context: parse_context
|
||||
)
|
||||
|
||||
context_variable_name = @alias_name || template_name.split('/').last
|
||||
|
||||
variable = if @variable_name_expr
|
||||
context.evaluate(@variable_name_expr)
|
||||
@@ -57,10 +67,10 @@ module Liquid
|
||||
end
|
||||
|
||||
old_template_name = context.template_name
|
||||
old_partial = context.partial
|
||||
old_partial = context.partial
|
||||
begin
|
||||
context.template_name = template_name
|
||||
context.partial = true
|
||||
context.partial = true
|
||||
context.stack do
|
||||
@attributes.each do |key, value|
|
||||
context[key] = context.evaluate(value)
|
||||
@@ -78,42 +88,15 @@ module Liquid
|
||||
end
|
||||
ensure
|
||||
context.template_name = old_template_name
|
||||
context.partial = old_partial
|
||||
context.partial = old_partial
|
||||
end
|
||||
|
||||
output
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
alias_method :parse_context, :options
|
||||
private :parse_context
|
||||
|
||||
def load_cached_partial(template_name, context)
|
||||
cached_partials = context.registers[:cached_partials] || {}
|
||||
|
||||
if (cached = cached_partials[template_name])
|
||||
return cached
|
||||
end
|
||||
|
||||
source = read_template_from_file_system(context)
|
||||
begin
|
||||
parse_context.partial = true
|
||||
partial = Liquid::Template.parse(source, parse_context)
|
||||
ensure
|
||||
parse_context.partial = false
|
||||
end
|
||||
cached_partials[template_name] = partial
|
||||
context.registers[:cached_partials] = cached_partials
|
||||
partial
|
||||
end
|
||||
|
||||
def read_template_from_file_system(context)
|
||||
file_system = context.registers[:file_system] || Liquid::Template.file_system
|
||||
|
||||
file_system.read_template_file(context.evaluate(@template_name_expr))
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
[
|
||||
|
||||
@@ -25,6 +25,7 @@ module Liquid
|
||||
def render_to_output_buffer(context, output)
|
||||
value = context.environments.first[@variable] ||= 0
|
||||
context.environments.first[@variable] = value + 1
|
||||
|
||||
output << value.to_s
|
||||
output
|
||||
end
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
module Liquid
|
||||
class Raw < Block
|
||||
SYNTAX = /\A\s*\z/.freeze
|
||||
FULL_TOKEN_POSSIBLY_INVALID = /\A(.*)#{TAG_START}\s*(\w+)\s*(.*)?#{TAG_END}\z/om.freeze
|
||||
Syntax = /\A\s*\z/
|
||||
FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
|
||||
|
||||
def initialize(tag_name, markup, parse_context)
|
||||
super
|
||||
@@ -14,14 +14,14 @@ module Liquid
|
||||
def parse(tokens)
|
||||
@body = +''
|
||||
while (token = tokens.shift)
|
||||
if token =~ FULL_TOKEN_POSSIBLY_INVALID
|
||||
@body << Regexp.last_match(1) if Regexp.last_match(1) != ''
|
||||
return if block_delimiter == Regexp.last_match(2)
|
||||
if token =~ FullTokenPossiblyInvalid && block_delimiter == Regexp.last_match(2)
|
||||
@body << Regexp.last_match(1) if Regexp.last_match(1) != ""
|
||||
return
|
||||
end
|
||||
@body << token unless token.empty?
|
||||
end
|
||||
|
||||
raise SyntaxError, parse_context.locale.t('errors.syntax.tag_never_closed', block_name: block_name)
|
||||
raise_tag_never_closed(block_name)
|
||||
end
|
||||
|
||||
def render_to_output_buffer(_context, output)
|
||||
@@ -40,7 +40,9 @@ module Liquid
|
||||
protected
|
||||
|
||||
def ensure_valid_markup(tag_name, markup, parse_context)
|
||||
raise SyntaxError, parse_context.locale.t('errors.syntax.tag_unexpected_args', tag: tag_name) unless markup =~ SYNTAX
|
||||
unless Syntax.match?(markup)
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_unexpected_args", tag: tag_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
84
lib/liquid/tags/render.rb
Normal file
84
lib/liquid/tags/render.rb
Normal file
@@ -0,0 +1,84 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Render < Tag
|
||||
FOR = 'for'
|
||||
SYNTAX = /(#{QuotedString}+)(\s+(with|#{FOR})\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
|
||||
|
||||
disable_tags "include"
|
||||
|
||||
attr_reader :template_name_expr, :attributes
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.render") unless markup =~ SYNTAX
|
||||
|
||||
template_name = Regexp.last_match(1)
|
||||
with_or_for = Regexp.last_match(3)
|
||||
variable_name = Regexp.last_match(4)
|
||||
|
||||
@alias_name = Regexp.last_match(6)
|
||||
@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] = parse_expression(value)
|
||||
end
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
render_tag(context, output)
|
||||
end
|
||||
|
||||
def render_tag(context, output)
|
||||
# Though we evaluate this here we will only ever parse it as a string literal.
|
||||
template_name = context.evaluate(@template_name_expr)
|
||||
raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name
|
||||
|
||||
partial = PartialCache.load(
|
||||
template_name,
|
||||
context: context,
|
||||
parse_context: parse_context
|
||||
)
|
||||
|
||||
context_variable_name = @alias_name || template_name.split('/').last
|
||||
|
||||
render_partial_func = ->(var, forloop) {
|
||||
inner_context = context.new_isolated_subcontext
|
||||
inner_context.template_name = template_name
|
||||
inner_context.partial = true
|
||||
inner_context['forloop'] = forloop if forloop
|
||||
|
||||
@attributes.each do |key, value|
|
||||
inner_context[key] = context.evaluate(value)
|
||||
end
|
||||
inner_context[context_variable_name] = var unless var.nil?
|
||||
partial.render_to_output_buffer(inner_context, output)
|
||||
forloop&.send(:increment!)
|
||||
}
|
||||
|
||||
variable = @variable_name_expr ? context.evaluate(@variable_name_expr) : nil
|
||||
if @for && variable.respond_to?(:each) && variable.respond_to?(:count)
|
||||
forloop = Liquid::ForloopDrop.new(template_name, variable.count, nil)
|
||||
variable.each { |var| render_partial_func.call(var, forloop) }
|
||||
else
|
||||
render_partial_func.call(variable, nil)
|
||||
end
|
||||
|
||||
output
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
[
|
||||
@node.template_name_expr,
|
||||
] + @node.attributes.values
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('render', Render)
|
||||
end
|
||||
@@ -2,21 +2,21 @@
|
||||
|
||||
module Liquid
|
||||
class TableRow < Block
|
||||
SYNTAX = /(\w+)\s+in\s+(#{QUOTED_FRAGMENT}+)/o.freeze
|
||||
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
|
||||
|
||||
attr_reader :variable_name, :collection_name, :attributes
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
if markup =~ SYNTAX
|
||||
@variable_name = Regexp.last_match(1)
|
||||
@collection_name = Expression.parse(Regexp.last_match(2))
|
||||
@attributes = {}
|
||||
markup.scan(TAG_ATTRIBUTES) do |key, value|
|
||||
@attributes[key] = Expression.parse(value)
|
||||
if markup =~ Syntax
|
||||
@variable_name = Regexp.last_match(1)
|
||||
@collection_name = parse_expression(Regexp.last_match(2))
|
||||
@attributes = {}
|
||||
markup.scan(TagAttributes) do |key, value|
|
||||
@attributes[key] = parse_expression(value)
|
||||
end
|
||||
else
|
||||
raise SyntaxError, options[:locale].t('errors.syntax.table_row')
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.table_row")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -24,11 +24,10 @@ module Liquid
|
||||
(collection = context.evaluate(@collection_name)) || (return '')
|
||||
|
||||
from = @attributes.key?('offset') ? context.evaluate(@attributes['offset']).to_i : 0
|
||||
to = @attributes.key?('limit') ? from + context.evaluate(@attributes['limit']).to_i : nil
|
||||
to = @attributes.key?('limit') ? from + context.evaluate(@attributes['limit']).to_i : nil
|
||||
|
||||
collection = Utils.slice_collection(collection, from, to)
|
||||
|
||||
length = collection.length
|
||||
length = collection.length
|
||||
|
||||
cols = context.evaluate(@attributes['cols']).to_i
|
||||
|
||||
@@ -44,7 +43,9 @@ module Liquid
|
||||
super
|
||||
output << '</td>'
|
||||
|
||||
output << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">" if tablerowloop.col_last && !tablerowloop.last
|
||||
if tablerowloop.col_last && !tablerowloop.last
|
||||
output << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">"
|
||||
end
|
||||
|
||||
tablerowloop.send(:increment!)
|
||||
end
|
||||
|
||||
@@ -9,14 +9,23 @@ module Liquid
|
||||
#
|
||||
class Unless < If
|
||||
def render_to_output_buffer(context, output)
|
||||
context.stack do
|
||||
# First condition is interpreted backwards ( if not )
|
||||
first_block = @blocks.first
|
||||
return first_block.attachment.render_to_output_buffer(context, output) unless first_block.evaluate(context)
|
||||
# First condition is interpreted backwards ( if not )
|
||||
first_block = @blocks.first
|
||||
result = Liquid::Utils.to_liquid_value(
|
||||
first_block.evaluate(context)
|
||||
)
|
||||
|
||||
# After the first condition unless works just like if
|
||||
@blocks[1..-1].each do |block|
|
||||
return block.attachment.render_to_output_buffer(context, output) if 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|
|
||||
result = block.evaluate(context)
|
||||
result = result.to_liquid_value if result.is_a?(Liquid::Drop)
|
||||
|
||||
if result
|
||||
return block.attachment.render_to_output_buffer(context, output)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -18,17 +18,11 @@ module Liquid
|
||||
attr_accessor :root
|
||||
attr_reader :resource_limits, :warnings
|
||||
|
||||
@file_system = BlankFileSystem.new
|
||||
|
||||
class << self
|
||||
attr_accessor :file_system
|
||||
end
|
||||
|
||||
class TagRegistry
|
||||
include Enumerable
|
||||
|
||||
def initialize
|
||||
@tags = {}
|
||||
@tags = {}
|
||||
@cache = {}
|
||||
end
|
||||
|
||||
@@ -67,68 +61,54 @@ module Liquid
|
||||
# :lax acts like liquid 2.5 and silently ignores malformed tags in most cases.
|
||||
# :warn is the default and will give deprecation warnings when invalid syntax is used.
|
||||
# :strict will enforce correct syntax.
|
||||
attr_writer :error_mode
|
||||
|
||||
# Sets how strict the taint checker should be.
|
||||
# :lax is the default, and ignores the taint flag completely
|
||||
# :warn adds a warning, but does not interrupt the rendering
|
||||
# :error raises an error when tainted output is used
|
||||
attr_writer :taint_mode
|
||||
attr_accessor :error_mode
|
||||
Template.error_mode = :lax
|
||||
|
||||
attr_accessor :default_exception_renderer
|
||||
Template.default_exception_renderer = lambda do |exception|
|
||||
exception
|
||||
end
|
||||
|
||||
attr_accessor :file_system
|
||||
Template.file_system = BlankFileSystem.new
|
||||
|
||||
attr_accessor :tags
|
||||
Template.tags = TagRegistry.new
|
||||
private :tags=
|
||||
|
||||
def register_tag(name, klass)
|
||||
tags[name.to_s] = klass
|
||||
end
|
||||
|
||||
def tags
|
||||
@tags ||= TagRegistry.new
|
||||
end
|
||||
|
||||
def error_mode
|
||||
@error_mode ||= :lax
|
||||
end
|
||||
|
||||
def taint_mode
|
||||
@taint_mode ||= :lax
|
||||
end
|
||||
|
||||
# Pass a module with filter methods which should be available
|
||||
# to all liquid views. Good for registering the standard library
|
||||
def register_filter(mod)
|
||||
Strainer.global_filter(mod)
|
||||
StrainerFactory.add_global_filter(mod)
|
||||
end
|
||||
|
||||
def default_resource_limits
|
||||
@default_resource_limits ||= {}
|
||||
end
|
||||
attr_accessor :default_resource_limits
|
||||
Template.default_resource_limits = {}
|
||||
private :default_resource_limits=
|
||||
|
||||
# creates a new <tt>Template</tt> object from liquid source code
|
||||
# To enable profiling, pass in <tt>profile: true</tt> as an option.
|
||||
# See Liquid::Profiler for more information
|
||||
def parse(source, options = {})
|
||||
template = Template.new
|
||||
template.parse(source, options)
|
||||
new.parse(source, options)
|
||||
end
|
||||
end
|
||||
|
||||
def initialize
|
||||
@rethrow_errors = false
|
||||
@resource_limits = ResourceLimits.new(self.class.default_resource_limits)
|
||||
@rethrow_errors = false
|
||||
@resource_limits = ResourceLimits.new(Template.default_resource_limits)
|
||||
end
|
||||
|
||||
# Parse source code.
|
||||
# Returns self for easy chaining
|
||||
def parse(source, options = {})
|
||||
@options = options
|
||||
@profiling = options[:profile]
|
||||
@line_numbers = options[:line_numbers] || @profiling
|
||||
parse_context = options.is_a?(ParseContext) ? options : ParseContext.new(options)
|
||||
@root = Document.parse(tokenize(source), parse_context)
|
||||
@warnings = parse_context.warnings
|
||||
parse_context = configure_options(options)
|
||||
tokenizer = parse_context.new_tokenizer(source, start_line_number: @line_numbers && 1)
|
||||
@root = Document.parse(tokenizer, parse_context)
|
||||
self
|
||||
end
|
||||
|
||||
@@ -169,28 +149,34 @@ module Liquid
|
||||
when Liquid::Context
|
||||
c = args.shift
|
||||
|
||||
c.exception_renderer = ->(_e) { raise } if @rethrow_errors
|
||||
if @rethrow_errors
|
||||
c.exception_renderer = Liquid::RAISE_EXCEPTION_LAMBDA
|
||||
end
|
||||
|
||||
c
|
||||
when Liquid::Drop
|
||||
drop = args.shift
|
||||
drop = args.shift
|
||||
drop.context = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits)
|
||||
when Hash
|
||||
Context.new([args.shift, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits)
|
||||
when nil
|
||||
Context.new(assigns, instance_assigns, registers, @rethrow_errors, @resource_limits)
|
||||
else
|
||||
raise ArgumentError, 'Expected Hash or Liquid::Context as parameter'
|
||||
raise ArgumentError, "Expected Hash or Liquid::Context as parameter"
|
||||
end
|
||||
|
||||
output = nil
|
||||
|
||||
context_register = context.registers.is_a?(StaticRegisters) ? context.registers.static : context.registers
|
||||
|
||||
case args.last
|
||||
when Hash
|
||||
options = args.pop
|
||||
output = options[:output] if options[:output]
|
||||
output = options[:output] if options[:output]
|
||||
|
||||
registers.merge!(options[:registers]) if options[:registers].is_a?(Hash)
|
||||
options[:registers]&.each do |key, register|
|
||||
context_register[key] = register
|
||||
end
|
||||
|
||||
apply_options_to_context(context, options)
|
||||
when Module, Array
|
||||
@@ -200,12 +186,13 @@ module Liquid
|
||||
# Retrying a render resets resource usage
|
||||
context.resource_limits.reset
|
||||
|
||||
if @profiling && context.profiler.nil?
|
||||
@profiler = context.profiler = Liquid::Profiler.new
|
||||
end
|
||||
|
||||
begin
|
||||
# render the nodelist.
|
||||
# for performance reasons we get an array back here. join will make a string out of it.
|
||||
with_profiling(context) do
|
||||
@root.render_to_output_buffer(context, output || +'')
|
||||
end
|
||||
@root.render_to_output_buffer(context, output || +'')
|
||||
rescue Liquid::MemoryError => e
|
||||
context.handle_error(e)
|
||||
ensure
|
||||
@@ -224,33 +211,25 @@ module Liquid
|
||||
|
||||
private
|
||||
|
||||
def tokenize(source)
|
||||
Tokenizer.new(source, @line_numbers)
|
||||
end
|
||||
|
||||
def with_profiling(context)
|
||||
if @profiling && !context.partial
|
||||
def configure_options(options)
|
||||
if (profiling = options[:profile])
|
||||
raise "Profiler not loaded, require 'liquid/profiler' first" unless defined?(Liquid::Profiler)
|
||||
|
||||
@profiler = Profiler.new
|
||||
@profiler.start
|
||||
|
||||
begin
|
||||
yield
|
||||
ensure
|
||||
@profiler.stop
|
||||
end
|
||||
else
|
||||
yield
|
||||
end
|
||||
|
||||
@options = options
|
||||
@profiling = profiling
|
||||
@line_numbers = options[:line_numbers] || @profiling
|
||||
parse_context = options.is_a?(ParseContext) ? options : ParseContext.new(options)
|
||||
@warnings = parse_context.warnings
|
||||
parse_context
|
||||
end
|
||||
|
||||
def apply_options_to_context(context, options)
|
||||
context.add_filters(options[:filters]) if options[:filters]
|
||||
context.global_filter = options[:global_filter] if options[:global_filter]
|
||||
context.global_filter = options[:global_filter] if options[:global_filter]
|
||||
context.exception_renderer = options[:exception_renderer] if options[:exception_renderer]
|
||||
context.strict_variables = options[:strict_variables] if options[:strict_variables]
|
||||
context.strict_filters = options[:strict_filters] if options[:strict_filters]
|
||||
context.strict_variables = options[:strict_variables] if options[:strict_variables]
|
||||
context.strict_filters = options[:strict_filters] if options[:strict_filters]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
9
lib/liquid/template_factory.rb
Normal file
9
lib/liquid/template_factory.rb
Normal file
@@ -0,0 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class TemplateFactory
|
||||
def for(_template_name)
|
||||
Liquid::Template.new
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -2,27 +2,33 @@
|
||||
|
||||
module Liquid
|
||||
class Tokenizer
|
||||
attr_reader :line_number
|
||||
attr_reader :line_number, :for_liquid_tag
|
||||
|
||||
def initialize(source, line_numbers = false)
|
||||
@source = source
|
||||
@line_number = line_numbers ? 1 : nil
|
||||
@tokens = tokenize
|
||||
def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false)
|
||||
@source = source.to_s.to_str
|
||||
@line_number = line_number || (line_numbers ? 1 : nil)
|
||||
@for_liquid_tag = for_liquid_tag
|
||||
@tokens = tokenize
|
||||
end
|
||||
|
||||
def shift
|
||||
token = @tokens.shift
|
||||
@line_number += token.count("\n") if @line_number && token
|
||||
(token = @tokens.shift) || return
|
||||
|
||||
if @line_number
|
||||
@line_number += @for_liquid_tag ? 1 : token.count("\n")
|
||||
end
|
||||
|
||||
token
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def tokenize
|
||||
@source = @source.source if @source.respond_to?(:source)
|
||||
return [] if @source.to_s.empty?
|
||||
return [] if @source.empty?
|
||||
|
||||
tokens = @source.split(TEMPLATE_PARSER)
|
||||
return @source.split("\n") if @for_liquid_tag
|
||||
|
||||
tokens = @source.split(TemplateParser)
|
||||
|
||||
# removes the rogue empty element at the beginning of the array
|
||||
tokens.shift if tokens[0]&.empty?
|
||||
|
||||
8
lib/liquid/usage.rb
Normal file
8
lib/liquid/usage.rb
Normal file
@@ -0,0 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
module Usage
|
||||
def self.increment(name)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -12,7 +12,7 @@ module Liquid
|
||||
|
||||
def self.slice_collection_using_each(collection, from, to)
|
||||
segments = []
|
||||
index = 0
|
||||
index = 0
|
||||
|
||||
# Maintains Ruby 1.8.7 String#each behaviour on 1.9
|
||||
if collection.is_a?(String)
|
||||
@@ -21,9 +21,13 @@ module Liquid
|
||||
return [] unless collection.respond_to?(:each)
|
||||
|
||||
collection.each do |item|
|
||||
break if to && to <= index
|
||||
if to && to <= index
|
||||
break
|
||||
end
|
||||
|
||||
segments << item if from <= index
|
||||
if from <= index
|
||||
segments << item
|
||||
end
|
||||
|
||||
index += 1
|
||||
end
|
||||
@@ -33,12 +37,11 @@ module Liquid
|
||||
|
||||
def self.to_integer(num)
|
||||
return num if num.is_a?(Integer)
|
||||
|
||||
num = num.to_s
|
||||
begin
|
||||
Integer(num)
|
||||
rescue ::ArgumentError
|
||||
raise Liquid::ArgumentError, 'invalid integer'
|
||||
raise Liquid::ArgumentError, "invalid integer"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -49,7 +52,7 @@ module Liquid
|
||||
when Numeric
|
||||
obj
|
||||
when String
|
||||
obj.strip =~ /\A-?\d+\.\d+\z/ ? BigDecimal(obj) : obj.to_i
|
||||
/\A-?\d+\.\d+\z/.match?(obj.strip) ? BigDecimal(obj) : obj.to_i
|
||||
else
|
||||
if obj.respond_to?(:to_number)
|
||||
obj.to_number
|
||||
@@ -64,7 +67,6 @@ module Liquid
|
||||
|
||||
if obj.is_a?(String)
|
||||
return nil if obj.empty?
|
||||
|
||||
obj = obj.downcase
|
||||
end
|
||||
|
||||
@@ -79,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,11 +12,11 @@ module Liquid
|
||||
# {{ user | link }}
|
||||
#
|
||||
class Variable
|
||||
FILTER_MARKUP_REGEX = /#{FILTER_SEPARATOR}\s*(.*)/om.freeze
|
||||
FILTER_PARSER = /(?:\s+|#{QUOTED_FRAGMENT}|#{ARGUMENT_SEPARATOR})+/o.freeze
|
||||
FILTER_ARGS_REGEX = /(?:#{FILTER_ARGUMENT_SEPARATOR}|#{ARGUMENT_SEPARATOR})\s*((?:\w+\s*\:\s*)?#{QUOTED_FRAGMENT})/o.freeze
|
||||
JUST_TAG_ATTRIBUTES = /\A#{TAG_ATTRIBUTES}\z/o.freeze
|
||||
MARKUP_WITH_QUOTED_FRAGMENT = /(#{QUOTED_FRAGMENT})(.*)/om.freeze
|
||||
FilterMarkupRegex = /#{FilterSeparator}\s*(.*)/om
|
||||
FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o
|
||||
FilterArgsRegex = /(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o
|
||||
JustTagAttributes = /\A#{TagAttributes}\z/o
|
||||
MarkupWithQuotedFragment = /(#{QuotedFragment})(.*)/om
|
||||
|
||||
attr_accessor :filters, :name, :line_number
|
||||
attr_reader :parse_context
|
||||
@@ -25,12 +25,12 @@ module Liquid
|
||||
include ParserSwitching
|
||||
|
||||
def initialize(markup, parse_context)
|
||||
@markup = markup
|
||||
@name = nil
|
||||
@markup = markup
|
||||
@name = nil
|
||||
@parse_context = parse_context
|
||||
@line_number = parse_context.line_number
|
||||
@line_number = parse_context.line_number
|
||||
|
||||
parse_with_selected_parser(markup)
|
||||
strict_parse_with_error_mode_fallback(markup)
|
||||
end
|
||||
|
||||
def raw
|
||||
@@ -43,18 +43,17 @@ module Liquid
|
||||
|
||||
def lax_parse(markup)
|
||||
@filters = []
|
||||
return unless markup =~ MARKUP_WITH_QUOTED_FRAGMENT
|
||||
return unless markup =~ MarkupWithQuotedFragment
|
||||
|
||||
name_markup = Regexp.last_match(1)
|
||||
name_markup = Regexp.last_match(1)
|
||||
filter_markup = Regexp.last_match(2)
|
||||
@name = Expression.parse(name_markup)
|
||||
if filter_markup =~ FILTER_MARKUP_REGEX
|
||||
filters = Regexp.last_match(1).scan(FILTER_PARSER)
|
||||
@name = Expression.parse(name_markup)
|
||||
if filter_markup =~ FilterMarkupRegex
|
||||
filters = Regexp.last_match(1).scan(FilterParser)
|
||||
filters.each do |f|
|
||||
next unless f =~ /\w+/
|
||||
|
||||
filtername = Regexp.last_match(0)
|
||||
filterargs = f.scan(FILTER_ARGS_REGEX).flatten
|
||||
filterargs = f.scan(FilterArgsRegex).flatten
|
||||
@filters << parse_filter_expressions(filtername, filterargs)
|
||||
end
|
||||
end
|
||||
@@ -64,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)
|
||||
@@ -82,14 +83,14 @@ module Liquid
|
||||
end
|
||||
|
||||
def render(context)
|
||||
obj = @filters.inject(context.evaluate(@name)) do |output, (filter_name, filter_args, filter_kwargs)|
|
||||
obj = context.evaluate(@name)
|
||||
|
||||
@filters.each do |filter_name, filter_args, filter_kwargs|
|
||||
filter_args = evaluate_filter_expressions(context, filter_args, filter_kwargs)
|
||||
context.invoke(filter_name, output, *filter_args)
|
||||
obj = context.invoke(filter_name, obj, *filter_args)
|
||||
end
|
||||
|
||||
obj = context.apply_global_filter(obj)
|
||||
taint_check(context, obj)
|
||||
obj
|
||||
context.apply_global_filter(obj)
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
@@ -105,14 +106,22 @@ module Liquid
|
||||
output
|
||||
end
|
||||
|
||||
def disabled?(_context)
|
||||
false
|
||||
end
|
||||
|
||||
def disabled_tags
|
||||
[]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse_filter_expressions(filter_name, unparsed_args)
|
||||
filter_args = []
|
||||
filter_args = []
|
||||
keyword_args = nil
|
||||
unparsed_args.each do |a|
|
||||
if (matches = a.match(JUST_TAG_ATTRIBUTES))
|
||||
keyword_args ||= {}
|
||||
if (matches = a.match(JustTagAttributes))
|
||||
keyword_args ||= {}
|
||||
keyword_args[matches[1]] = Expression.parse(matches[2])
|
||||
else
|
||||
filter_args << Expression.parse(a)
|
||||
@@ -135,25 +144,6 @@ module Liquid
|
||||
parsed_args
|
||||
end
|
||||
|
||||
def taint_check(context, obj)
|
||||
return unless obj.tainted?
|
||||
return if Template.taint_mode == :lax
|
||||
|
||||
@markup =~ QUOTED_FRAGMENT
|
||||
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,8 @@
|
||||
|
||||
module Liquid
|
||||
class VariableLookup
|
||||
SQUARE_BRACKETED = /\A\[(.*)\]\z/m.freeze
|
||||
COMMAND_METHODS = %w[size first last].freeze
|
||||
SQUARE_BRACKETED = /\A\[(.*)\]\z/m
|
||||
COMMAND_METHODS = ['size', 'first', 'last'].freeze
|
||||
|
||||
attr_reader :name, :lookups
|
||||
|
||||
@@ -12,13 +12,15 @@ module Liquid
|
||||
end
|
||||
|
||||
def initialize(markup)
|
||||
lookups = markup.scan(VARIABLE_PARSER)
|
||||
lookups = markup.scan(VariableParser)
|
||||
|
||||
name = lookups.shift
|
||||
name = Expression.parse(Regexp.last_match(1)) if name =~ SQUARE_BRACKETED
|
||||
if name =~ SQUARE_BRACKETED
|
||||
name = Expression.parse(Regexp.last_match(1))
|
||||
end
|
||||
@name = name
|
||||
|
||||
@lookups = lookups
|
||||
@lookups = lookups
|
||||
@command_flags = 0
|
||||
|
||||
@lookups.each_index do |i|
|
||||
@@ -32,20 +34,23 @@ module Liquid
|
||||
end
|
||||
|
||||
def evaluate(context)
|
||||
name = context.evaluate(@name)
|
||||
name = context.evaluate(@name)
|
||||
object = context.find_variable(name)
|
||||
|
||||
@lookups.each_index do |i|
|
||||
key = context.evaluate(@lookups[i])
|
||||
|
||||
# Cast "key" to its liquid value to enable it to act as a primitive value
|
||||
key = Liquid::Utils.to_liquid_value(key)
|
||||
|
||||
# If object is a hash- or array-like object we look for the
|
||||
# presence of the key and if its available we return it
|
||||
if object.respond_to?(:[]) &&
|
||||
((object.respond_to?(:key?) && object.key?(key)) ||
|
||||
(object.respond_to?(:fetch) && key.is_a?(Integer)))
|
||||
((object.respond_to?(:key?) && object.key?(key)) ||
|
||||
(object.respond_to?(:fetch) && key.is_a?(Integer)))
|
||||
|
||||
# if its a proc we will replace the entry with the proc
|
||||
res = context.lookup_and_evaluate(object, key)
|
||||
res = context.lookup_and_evaluate(object, key)
|
||||
object = res.to_liquid
|
||||
|
||||
# Some special cases. If the part wasn't in square brackets and
|
||||
@@ -59,7 +64,6 @@ module Liquid
|
||||
# raise an exception if `strict_variables` option is set to true
|
||||
else
|
||||
return nil unless context.strict_variables
|
||||
|
||||
raise Liquid::UndefinedVariable, "undefined variable #{key}"
|
||||
end
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# encoding: utf-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
VERSION = '4.0.3'
|
||||
VERSION = "5.0.2.alpha"
|
||||
end
|
||||
|
||||
@@ -1,31 +1,34 @@
|
||||
# encoding: utf-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
lib = File.expand_path('lib', __dir__)
|
||||
lib = File.expand_path('../lib/', __FILE__)
|
||||
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
||||
|
||||
require 'liquid/version'
|
||||
require "liquid/version"
|
||||
|
||||
Gem::Specification.new do |s|
|
||||
s.name = 'liquid'
|
||||
s.name = "liquid"
|
||||
s.version = Liquid::VERSION
|
||||
s.platform = Gem::Platform::RUBY
|
||||
s.summary = 'A secure, non-evaling end user template engine with aesthetic markup.'
|
||||
s.authors = ['Tobias Lütke']
|
||||
s.email = ['tobi@leetsoft.com']
|
||||
s.homepage = 'http://www.liquidmarkup.org'
|
||||
s.license = 'MIT'
|
||||
s.summary = "A secure, non-evaling end user template engine with aesthetic markup."
|
||||
s.authors = ["Tobias Lütke"]
|
||||
s.email = ["tobi@leetsoft.com"]
|
||||
s.homepage = "http://www.liquidmarkup.org"
|
||||
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_rubygems_version = '>= 1.3.7'
|
||||
s.required_ruby_version = ">= 2.5.0"
|
||||
s.required_rubygems_version = ">= 1.3.7"
|
||||
|
||||
s.test_files = Dir.glob('{test}/**/*')
|
||||
s.files = Dir.glob('{lib}/**/*') + %w[LICENSE README.md]
|
||||
s.metadata['allowed_push_host'] = 'https://rubygems.org'
|
||||
|
||||
s.extra_rdoc_files = ['History.md', 'README.md']
|
||||
s.test_files = Dir.glob("{test}/**/*")
|
||||
s.files = Dir.glob("{lib}/**/*") + %w(LICENSE README.md)
|
||||
|
||||
s.require_path = 'lib'
|
||||
s.extra_rdoc_files = ["History.md", "README.md"]
|
||||
|
||||
s.require_path = "lib"
|
||||
|
||||
s.add_development_dependency('rake', '~> 13.0')
|
||||
s.add_development_dependency('minitest')
|
||||
s.add_development_dependency('rake', '~> 11.3')
|
||||
end
|
||||
|
||||
@@ -7,14 +7,14 @@ Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
|
||||
profiler = ThemeRunner.new
|
||||
|
||||
Benchmark.ips do |x|
|
||||
x.time = 10
|
||||
x.time = 10
|
||||
x.warmup = 5
|
||||
|
||||
puts
|
||||
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 }
|
||||
x.report("parse:") { profiler.compile }
|
||||
x.report("render:") { profiler.render }
|
||||
x.report("parse & render:") { profiler.run }
|
||||
end
|
||||
|
||||
@@ -2,25 +2,62 @@
|
||||
|
||||
require 'benchmark/ips'
|
||||
require 'memory_profiler'
|
||||
require 'terminal-table'
|
||||
require_relative 'theme_runner'
|
||||
|
||||
def profile(phase, &block)
|
||||
puts
|
||||
puts "#{phase}:"
|
||||
puts
|
||||
class Profiler
|
||||
LOG_LABEL = "Profiling: ".rjust(14).freeze
|
||||
REPORTS_DIR = File.expand_path('.memprof', __dir__).freeze
|
||||
|
||||
report = MemoryProfiler.report(&block)
|
||||
def self.run
|
||||
puts
|
||||
yield new
|
||||
end
|
||||
|
||||
report.pretty_print(
|
||||
color_output: true,
|
||||
scale_bytes: true,
|
||||
detailed_report: true
|
||||
)
|
||||
def initialize
|
||||
@allocated = []
|
||||
@retained = []
|
||||
@headings = []
|
||||
end
|
||||
|
||||
def profile(phase, &block)
|
||||
print(LOG_LABEL)
|
||||
print("#{phase}.. ".ljust(10))
|
||||
report = MemoryProfiler.report(&block)
|
||||
puts 'Done.'
|
||||
@headings << phase.capitalize
|
||||
@allocated << "#{report.scale_bytes(report.total_allocated_memsize)} (#{report.total_allocated} objects)"
|
||||
@retained << "#{report.scale_bytes(report.total_retained_memsize)} (#{report.total_retained} objects)"
|
||||
|
||||
return if ENV['CI']
|
||||
|
||||
require 'fileutils'
|
||||
report_file = File.join(REPORTS_DIR, "#{sanitize(phase)}.txt")
|
||||
FileUtils.mkdir_p(REPORTS_DIR)
|
||||
report.pretty_print(to_file: report_file, scale_bytes: true)
|
||||
end
|
||||
|
||||
def tabulate
|
||||
table = Terminal::Table.new(headings: @headings.unshift('Phase')) do |t|
|
||||
t << @allocated.unshift('Total allocated')
|
||||
t << @retained.unshift('Total retained')
|
||||
end
|
||||
|
||||
puts
|
||||
puts table
|
||||
puts "\nDetailed report(s) saved to #{REPORTS_DIR}/" unless ENV['CI']
|
||||
end
|
||||
|
||||
def sanitize(string)
|
||||
string.downcase.gsub(/[\W]/, '-').squeeze('-')
|
||||
end
|
||||
end
|
||||
|
||||
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
|
||||
|
||||
profiler = ThemeRunner.new
|
||||
|
||||
profile('Parsing') { profiler.compile }
|
||||
profile('Rendering') { profiler.render }
|
||||
runner = ThemeRunner.new
|
||||
Profiler.run do |x|
|
||||
x.profile('parse') { runner.compile }
|
||||
x.profile('render') { runner.render }
|
||||
x.tabulate
|
||||
end
|
||||
|
||||
@@ -7,7 +7,7 @@ Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
|
||||
profiler = ThemeRunner.new
|
||||
profiler.run
|
||||
|
||||
%i[cpu object].each do |profile_type|
|
||||
[:cpu, :object].each do |profile_type|
|
||||
puts "Profiling in #{profile_type} mode..."
|
||||
results = StackProf.run(mode: profile_type) do
|
||||
200.times do
|
||||
@@ -22,5 +22,5 @@ profiler.run
|
||||
end
|
||||
|
||||
StackProf::Report.new(results).print_text(false, 20)
|
||||
File.write(ENV['FILENAME'] + '.' + profile_type.to_s, Marshal.dump(results)) if ENV['FILENAME']
|
||||
File.write(ENV['FILENAME'] + "." + profile_type.to_s, Marshal.dump(results)) if ENV['FILENAME']
|
||||
end
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class CommentForm < Liquid::Block
|
||||
SYNTAX = /(#{Liquid::VARIABLE_SIGNATURE}+)/.freeze
|
||||
Syntax = /(#{Liquid::VariableSignature}+)/
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
|
||||
if markup =~ SYNTAX
|
||||
if markup =~ Syntax
|
||||
@variable_name = Regexp.last_match(1)
|
||||
@attributes = {}
|
||||
@attributes = {}
|
||||
else
|
||||
raise SyntaxError, "Syntax Error in 'comment_form' - Valid syntax: comment_form [article]"
|
||||
end
|
||||
|
||||
@@ -21,9 +21,7 @@ module Database
|
||||
db = db.each_with_object({}) do |(key, values), assigns|
|
||||
assigns[key] = values.each_with_object({}) do |v, h|
|
||||
h[v['handle']] = v
|
||||
h
|
||||
end
|
||||
assigns
|
||||
end
|
||||
|
||||
# Some standard direct accessors so that the specialized templates
|
||||
|
||||
@@ -4,6 +4,6 @@ require 'json'
|
||||
|
||||
module JsonFilter
|
||||
def json(object)
|
||||
JSON.dump(object.reject { |k, _v| k == 'collections' })
|
||||
JSON.dump(object.reject { |k, _v| k == "collections" })
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,14 +3,12 @@
|
||||
module MoneyFilter
|
||||
def money_with_currency(money)
|
||||
return '' if money.nil?
|
||||
|
||||
format('$ %.2f USD', money / 100.0)
|
||||
format("$ %.2f USD", money / 100.0)
|
||||
end
|
||||
|
||||
def money(money)
|
||||
return '' if money.nil?
|
||||
|
||||
format('$ %.2f', money / 100.0)
|
||||
format("$ %.2f", money / 100.0)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Paginate < Liquid::Block
|
||||
SYNTAX = /(#{Liquid::QUOTED_FRAGMENT})\s*(by\s*(\d+))?/.freeze
|
||||
Syntax = /(#{Liquid::QuotedFragment})\s*(by\s*(\d+))?/
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
|
||||
if markup =~ SYNTAX
|
||||
if markup =~ Syntax
|
||||
@collection_name = Regexp.last_match(1)
|
||||
@page_size = if Regexp.last_match(2)
|
||||
@page_size = if Regexp.last_match(2)
|
||||
Regexp.last_match(3).to_i
|
||||
else
|
||||
20
|
||||
end
|
||||
|
||||
@attributes = { 'window_size' => 3 }
|
||||
markup.scan(Liquid::TAG_ATTRIBUTES) do |key, value|
|
||||
markup.scan(Liquid::TagAttributes) do |key, value|
|
||||
@attributes[key] = value
|
||||
end
|
||||
else
|
||||
@@ -45,7 +45,7 @@ class Paginate < Liquid::Block
|
||||
|
||||
pagination['items'] = collection_size
|
||||
pagination['pages'] = page_count - 1
|
||||
pagination['previous'] = link('« Previous', current_page - 1) unless current_page <= 1
|
||||
pagination['previous'] = link('« Previous', current_page - 1) unless 1 >= current_page
|
||||
pagination['next'] = link('Next »', current_page + 1) unless page_count <= current_page + 1
|
||||
pagination['parts'] = []
|
||||
|
||||
@@ -61,7 +61,6 @@ class Paginate < Liquid::Block
|
||||
pagination['parts'] << link(page, page)
|
||||
elsif page <= current_page - @attributes['window_size'] || page >= current_page + @attributes['window_size']
|
||||
next if hellip_break
|
||||
|
||||
pagination['parts'] << no_link('…')
|
||||
hellip_break = true
|
||||
next
|
||||
@@ -88,6 +87,6 @@ class Paginate < Liquid::Block
|
||||
end
|
||||
|
||||
def current_url
|
||||
'/collections/frontpage'
|
||||
"/collections/frontpage"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -17,21 +17,21 @@ module ShopFilter
|
||||
%(<script src="#{url}" type="text/javascript"></script>)
|
||||
end
|
||||
|
||||
def stylesheet_tag(url, media = 'all')
|
||||
def stylesheet_tag(url, media = "all")
|
||||
%(<link href="#{url}" rel="stylesheet" type="text/css" media="#{media}" />)
|
||||
end
|
||||
|
||||
def link_to(link, url, title = '')
|
||||
def link_to(link, url, title = "")
|
||||
%(<a href="#{url}" title="#{title}">#{link}</a>)
|
||||
end
|
||||
|
||||
def img_tag(url, alt = '')
|
||||
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
|
||||
link_to(vendor, url_for_vendor(vendor), vendor)
|
||||
else
|
||||
'Unknown Vendor'
|
||||
end
|
||||
@@ -39,7 +39,7 @@ module ShopFilter
|
||||
|
||||
def link_to_type(type)
|
||||
if type
|
||||
link_to type, url_for_type(type), type
|
||||
link_to(type, url_for_type(type), type)
|
||||
else
|
||||
'Unknown Vendor'
|
||||
end
|
||||
@@ -54,11 +54,13 @@ module ShopFilter
|
||||
end
|
||||
|
||||
def product_img_url(url, style = 'small')
|
||||
raise ArgumentError, 'filter "size" can only be called on product images' unless url =~ %r{\Aproducts/([\w\-\_]+)\.(\w{2,4})}
|
||||
unless url =~ %r{\Aproducts/([\w\-\_]+)\.(\w{2,4})}
|
||||
raise ArgumentError, 'filter "size" can only be called on product images'
|
||||
end
|
||||
|
||||
case style
|
||||
when 'original'
|
||||
return '/files/shops/random_number/' + url
|
||||
'/files/shops/random_number/' + url
|
||||
when 'grande', 'large', 'medium', 'compact', 'small', 'thumb', 'icon'
|
||||
"/files/shops/random_number/products/#{Regexp.last_match(1)}_#{style}.#{Regexp.last_match(2)}"
|
||||
else
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
module WeightFilter
|
||||
def weight(grams)
|
||||
format('%.2f', grams / 1000)
|
||||
format("%.2f", grams / 1000)
|
||||
end
|
||||
|
||||
def weight_with_unit(grams)
|
||||
|
||||
@@ -58,9 +58,9 @@ class ThemeRunner
|
||||
# `render` is called to benchmark just the render portion of liquid
|
||||
def render
|
||||
@compiled_tests.each do |test|
|
||||
tmpl = test[:tmpl]
|
||||
tmpl = test[:tmpl]
|
||||
assigns = test[:assigns]
|
||||
layout = test[:layout]
|
||||
layout = test[:layout]
|
||||
|
||||
if layout
|
||||
assigns['content_for_layout'] = tmpl.render!(assigns)
|
||||
@@ -73,10 +73,14 @@ class ThemeRunner
|
||||
|
||||
private
|
||||
|
||||
def render_layout(template, layout, assigns)
|
||||
assigns['content_for_layout'] = template.render!(assigns)
|
||||
layout&.render!(assigns)
|
||||
end
|
||||
|
||||
def compile_and_render(template, layout, assigns, page_template, template_file)
|
||||
compiled_test = compile_test(template, layout, assigns, page_template, template_file)
|
||||
assigns['content_for_layout'] = compiled_test[:tmpl].render!(assigns)
|
||||
compiled_test[:layout].render!(assigns) if layout
|
||||
render_layout(compiled_test[:tmpl], compiled_test[:layout], compiled_test[:assigns])
|
||||
end
|
||||
|
||||
def compile_all_tests
|
||||
@@ -88,7 +92,7 @@ class ThemeRunner
|
||||
end
|
||||
|
||||
def compile_test(template, layout, assigns, page_template, template_file)
|
||||
tmpl = init_template(page_template, template_file)
|
||||
tmpl = init_template(page_template, template_file)
|
||||
parsed_template = tmpl.parse(template).dup
|
||||
|
||||
if layout
|
||||
@@ -113,9 +117,9 @@ class ThemeRunner
|
||||
|
||||
# set up a new Liquid::Template object for use in `compile_and_render` and `compile_test`
|
||||
def init_template(page_template, template_file)
|
||||
tmpl = Liquid::Template.new
|
||||
tmpl.assigns['page_title'] = 'Page title'
|
||||
tmpl.assigns['template'] = page_template
|
||||
tmpl = Liquid::Template.new
|
||||
tmpl.assigns['page_title'] = 'Page title'
|
||||
tmpl.assigns['template'] = page_template
|
||||
tmpl.registers[:file_system] = ThemeRunner::FileSystem.new(File.dirname(template_file))
|
||||
tmpl
|
||||
end
|
||||
|
||||
@@ -10,31 +10,31 @@ class AssignTest < Minitest::Test
|
||||
{% assign this-thing = 'Print this-thing' %}
|
||||
{{ this-thing }}
|
||||
END_TEMPLATE
|
||||
template = Template.parse(template_source)
|
||||
rendered = template.render!
|
||||
assert_equal 'Print this-thing', rendered.strip
|
||||
template = Template.parse(template_source)
|
||||
rendered = template.render!
|
||||
assert_equal("Print this-thing", rendered.strip)
|
||||
end
|
||||
|
||||
def test_assigned_variable
|
||||
assert_template_result('.foo.',
|
||||
'{% assign foo = values %}.{{ foo[0] }}.',
|
||||
'values' => %w[foo bar baz])
|
||||
'{% assign foo = values %}.{{ foo[0] }}.',
|
||||
'values' => %w(foo bar baz))
|
||||
|
||||
assert_template_result('.bar.',
|
||||
'{% assign foo = values %}.{{ foo[1] }}.',
|
||||
'values' => %w[foo bar baz])
|
||||
'{% assign foo = values %}.{{ foo[1] }}.',
|
||||
'values' => %w(foo bar baz))
|
||||
end
|
||||
|
||||
def test_assign_with_filter
|
||||
assert_template_result('.bar.',
|
||||
'{% assign foo = values | split: "," %}.{{ foo[1] }}.',
|
||||
'values' => 'foo,bar,baz')
|
||||
'{% assign foo = values | split: "," %}.{{ foo[1] }}.',
|
||||
'values' => "foo,bar,baz")
|
||||
end
|
||||
|
||||
def test_assign_syntax_error
|
||||
assert_match_syntax_error(/assign/,
|
||||
'{% assign foo not values %}.',
|
||||
'values' => 'foo,bar,baz')
|
||||
'{% assign foo not values %}.',
|
||||
'values' => "foo,bar,baz")
|
||||
end
|
||||
|
||||
def test_assign_uses_error_mode
|
||||
@@ -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
|
||||
|
||||
@@ -33,77 +33,77 @@ class BlankTest < Minitest::Test
|
||||
|
||||
def test_new_tags_are_not_blank_by_default
|
||||
with_custom_tag('foobar', FoobarTag) do
|
||||
assert_template_result(' ' * N, wrap_in_for('{% foobar %}'))
|
||||
assert_template_result(" " * N, wrap_in_for("{% foobar %}"))
|
||||
end
|
||||
end
|
||||
|
||||
def test_loops_are_blank
|
||||
assert_template_result('', wrap_in_for(' '))
|
||||
assert_template_result("", wrap_in_for(" "))
|
||||
end
|
||||
|
||||
def test_if_else_are_blank
|
||||
assert_template_result('', '{% if true %} {% elsif false %} {% else %} {% endif %}')
|
||||
assert_template_result("", "{% if true %} {% elsif false %} {% else %} {% endif %}")
|
||||
end
|
||||
|
||||
def test_unless_is_blank
|
||||
assert_template_result('', wrap('{% unless true %} {% endunless %}'))
|
||||
assert_template_result("", wrap("{% unless true %} {% endunless %}"))
|
||||
end
|
||||
|
||||
def test_mark_as_blank_only_during_parsing
|
||||
assert_template_result(' ' * (N + 1), wrap(' {% if false %} this never happens, but still, this block is not blank {% endif %}'))
|
||||
assert_template_result(" " * (N + 1), wrap(" {% if false %} this never happens, but still, this block is not blank {% endif %}"))
|
||||
end
|
||||
|
||||
def test_comments_are_blank
|
||||
assert_template_result('', wrap(' {% comment %} whatever {% endcomment %} '))
|
||||
assert_template_result("", wrap(" {% comment %} whatever {% endcomment %} "))
|
||||
end
|
||||
|
||||
def test_captures_are_blank
|
||||
assert_template_result('', wrap(' {% capture foo %} whatever {% endcapture %} '))
|
||||
assert_template_result("", wrap(" {% capture foo %} whatever {% endcapture %} "))
|
||||
end
|
||||
|
||||
def test_nested_blocks_are_blank_but_only_if_all_children_are
|
||||
assert_template_result('', wrap(wrap(' ')))
|
||||
assert_template_result("", wrap(wrap(" ")))
|
||||
assert_template_result("\n but this is not " * (N + 1),
|
||||
wrap('{% if true %} {% comment %} this is blank {% endcomment %} {% endif %}
|
||||
wrap('{% if true %} {% comment %} this is blank {% endcomment %} {% endif %}
|
||||
{% if true %} but this is not {% endif %}'))
|
||||
end
|
||||
|
||||
def test_assigns_are_blank
|
||||
assert_template_result('', wrap(' {% assign foo = "bar" %} '))
|
||||
assert_template_result("", wrap(' {% assign foo = "bar" %} '))
|
||||
end
|
||||
|
||||
def test_whitespace_is_blank
|
||||
assert_template_result('', wrap(' '))
|
||||
assert_template_result('', wrap("\t"))
|
||||
assert_template_result("", wrap(" "))
|
||||
assert_template_result("", wrap("\t"))
|
||||
end
|
||||
|
||||
def test_whitespace_is_not_blank_if_other_stuff_is_present
|
||||
body = ' x '
|
||||
body = " x "
|
||||
assert_template_result(body * (N + 1), wrap(body))
|
||||
end
|
||||
|
||||
def test_increment_is_not_blank
|
||||
assert_template_result(' 0' * 2 * (N + 1), wrap('{% assign foo = 0 %} {% increment foo %} {% decrement foo %}'))
|
||||
assert_template_result(" 0" * 2 * (N + 1), wrap("{% assign foo = 0 %} {% increment foo %} {% decrement foo %}"))
|
||||
end
|
||||
|
||||
def test_cycle_is_not_blank
|
||||
assert_template_result(' ' * ((N + 1) / 2) + ' ', wrap("{% cycle ' ', ' ' %}"))
|
||||
assert_template_result(" " * ((N + 1) / 2) + " ", wrap("{% cycle ' ', ' ' %}"))
|
||||
end
|
||||
|
||||
def test_raw_is_not_blank
|
||||
assert_template_result(' ' * (N + 1), wrap(' {% raw %} {% endraw %}'))
|
||||
assert_template_result(" " * (N + 1), wrap(" {% raw %} {% endraw %}"))
|
||||
end
|
||||
|
||||
def test_include_is_blank
|
||||
Liquid::Template.file_system = BlankTestFileSystem.new
|
||||
assert_template_result 'foobar' * (N + 1), wrap("{% include 'foobar' %}")
|
||||
assert_template_result ' foobar ' * (N + 1), wrap("{% include ' foobar ' %}")
|
||||
assert_template_result ' ' * (N + 1), wrap(" {% include ' ' %} ")
|
||||
assert_template_result("foobar" * (N + 1), wrap("{% include 'foobar' %}"))
|
||||
assert_template_result(" foobar " * (N + 1), wrap("{% include ' foobar ' %}"))
|
||||
assert_template_result(" " * (N + 1), wrap(" {% include ' ' %} "))
|
||||
end
|
||||
|
||||
def test_case_is_blank
|
||||
assert_template_result('', wrap(" {% assign foo = 'bar' %} {% case foo %} {% when 'bar' %} {% when 'whatever' %} {% else %} {% endcase %} "))
|
||||
assert_template_result('', wrap(" {% assign foo = 'else' %} {% case foo %} {% when 'bar' %} {% when 'whatever' %} {% else %} {% endcase %} "))
|
||||
assert_template_result(' x ' * (N + 1), wrap(" {% assign foo = 'else' %} {% case foo %} {% when 'bar' %} {% when 'whatever' %} {% else %} x {% endcase %} "))
|
||||
assert_template_result("", wrap(" {% assign foo = 'bar' %} {% case foo %} {% when 'bar' %} {% when 'whatever' %} {% else %} {% endcase %} "))
|
||||
assert_template_result("", wrap(" {% assign foo = 'else' %} {% case foo %} {% when 'bar' %} {% when 'whatever' %} {% else %} {% endcase %} "))
|
||||
assert_template_result(" x " * (N + 1), wrap(" {% assign foo = 'else' %} {% case foo %} {% when 'bar' %} {% when 'whatever' %} {% else %} x {% endcase %} "))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,8 +7,52 @@ class BlockTest < Minitest::Test
|
||||
|
||||
def test_unexpected_end_tag
|
||||
exc = assert_raises(SyntaxError) do
|
||||
Template.parse('{% if true %}{% endunless %}')
|
||||
Template.parse("{% if true %}{% endunless %}")
|
||||
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
|
||||
assert_equal exc.message, "Liquid syntax error: 'endunless' is not a valid delimiter for if tags. use endif"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,7 +6,7 @@ class CaptureTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def test_captures_block_content_in_variable
|
||||
assert_template_result('test string', "{% capture 'var' %}test string{% endcapture %}{{var}}", {})
|
||||
assert_template_result("test string", "{% capture 'var' %}test string{% endcapture %}{{var}}", {})
|
||||
end
|
||||
|
||||
def test_capture_with_hyphen_in_variable_name
|
||||
@@ -14,9 +14,9 @@ class CaptureTest < Minitest::Test
|
||||
{% capture this-thing %}Print this-thing{% endcapture %}
|
||||
{{ this-thing }}
|
||||
END_TEMPLATE
|
||||
template = Template.parse(template_source)
|
||||
rendered = template.render!
|
||||
assert_equal 'Print this-thing', rendered.strip
|
||||
template = Template.parse(template_source)
|
||||
rendered = template.render!
|
||||
assert_equal("Print this-thing", rendered.strip)
|
||||
end
|
||||
|
||||
def test_capture_to_variable_from_outer_scope_if_existing
|
||||
@@ -30,9 +30,9 @@ class CaptureTest < Minitest::Test
|
||||
{% endif %}
|
||||
{{var}}
|
||||
END_TEMPLATE
|
||||
template = Template.parse(template_source)
|
||||
rendered = template.render!
|
||||
assert_equal 'test-string', rendered.gsub(/\s/, '')
|
||||
template = Template.parse(template_source)
|
||||
rendered = template.render!
|
||||
assert_equal("test-string", rendered.gsub(/\s/, ''))
|
||||
end
|
||||
|
||||
def test_assigning_from_capture
|
||||
@@ -45,8 +45,14 @@ class CaptureTest < Minitest::Test
|
||||
{% endfor %}
|
||||
{{ first }}-{{ second }}
|
||||
END_TEMPLATE
|
||||
template = Template.parse(template_source)
|
||||
rendered = template.render!
|
||||
assert_equal '3-3', rendered.gsub(/\s/, '')
|
||||
template = Template.parse(template_source)
|
||||
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,597 @@
|
||||
|
||||
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 < Liquid::Drop
|
||||
attr_accessor :name
|
||||
|
||||
def initialize(name)
|
||||
@name = name
|
||||
end
|
||||
|
||||
def to_liquid
|
||||
CategoryDrop.new(self)
|
||||
end
|
||||
end
|
||||
|
||||
class CategoryDrop
|
||||
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
|
||||
@context['callcount'] = proc {
|
||||
@global ||= 0
|
||||
@global += 1
|
||||
@global.to_s
|
||||
}
|
||||
|
||||
assert_equal('1', @context['callcount'])
|
||||
assert_equal('1', @context['callcount'])
|
||||
assert_equal('1', @context['callcount'])
|
||||
|
||||
@global = nil
|
||||
end
|
||||
|
||||
def test_nested_lambda_is_called_once
|
||||
@context['callcount'] = { "lambda" => proc {
|
||||
@global ||= 0
|
||||
@global += 1
|
||||
@global.to_s
|
||||
} }
|
||||
|
||||
assert_equal('1', @context['callcount.lambda'])
|
||||
assert_equal('1', @context['callcount.lambda'])
|
||||
assert_equal('1', @context['callcount.lambda'])
|
||||
|
||||
@global = nil
|
||||
end
|
||||
|
||||
def test_lambda_in_array_is_called_once
|
||||
@context['callcount'] = [1, 2, proc {
|
||||
@global ||= 0
|
||||
@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]'])
|
||||
|
||||
@global = nil
|
||||
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 +607,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
|
||||
|
||||
@@ -7,15 +7,15 @@ class DocumentTest < Minitest::Test
|
||||
|
||||
def test_unexpected_outer_tag
|
||||
exc = assert_raises(SyntaxError) do
|
||||
Template.parse('{% else %}')
|
||||
Template.parse("{% else %}")
|
||||
end
|
||||
assert_equal exc.message, "Liquid syntax error: Unexpected outer 'else' tag"
|
||||
assert_equal(exc.message, "Liquid syntax error: Unexpected outer 'else' tag")
|
||||
end
|
||||
|
||||
def test_unknown_tag
|
||||
exc = assert_raises(SyntaxError) do
|
||||
Template.parse('{% foo %}')
|
||||
Template.parse("{% foo %}")
|
||||
end
|
||||
assert_equal exc.message, "Liquid syntax error: Unknown tag 'foo'"
|
||||
assert_equal(exc.message, "Liquid syntax error: Unknown tag 'foo'")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -23,7 +23,7 @@ end
|
||||
class ProductDrop < Liquid::Drop
|
||||
class TextDrop < Liquid::Drop
|
||||
def array
|
||||
%w[text1 text2]
|
||||
['text1', 'text2']
|
||||
end
|
||||
|
||||
def text
|
||||
@@ -33,7 +33,7 @@ class ProductDrop < Liquid::Drop
|
||||
|
||||
class CatchallDrop < Liquid::Drop
|
||||
def liquid_method_missing(method)
|
||||
+'catchall_method: ' << method.to_s
|
||||
"catchall_method: #{method}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -49,14 +49,10 @@ class ProductDrop < Liquid::Drop
|
||||
ContextDrop.new
|
||||
end
|
||||
|
||||
def user_input
|
||||
(+'foo').taint
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def callmenot
|
||||
'protected'
|
||||
"protected"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -111,165 +107,151 @@ class DropsTest < Minitest::Test
|
||||
|
||||
def test_product_drop
|
||||
tpl = Liquid::Template.parse(' ')
|
||||
assert_equal ' ', tpl.render!('product' => ProductDrop.new)
|
||||
end
|
||||
|
||||
def test_rendering_raises_on_tainted_attr
|
||||
with_taint_mode(:error) do
|
||||
tpl = Liquid::Template.parse('{{ product.user_input }}')
|
||||
assert_raises TaintedError do
|
||||
tpl.render!('product' => ProductDrop.new)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_rendering_warns_on_tainted_attr
|
||||
with_taint_mode(:warn) do
|
||||
tpl = Liquid::Template.parse('{{ product.user_input }}')
|
||||
context = Context.new('product' => ProductDrop.new)
|
||||
tpl.render!(context)
|
||||
assert_equal [Liquid::TaintedError], context.warnings.map(&:class)
|
||||
assert_equal "variable 'product.user_input' is tainted and was not escaped", context.warnings.first.to_s(false)
|
||||
end
|
||||
end
|
||||
|
||||
def test_rendering_doesnt_raise_on_escaped_tainted_attr
|
||||
with_taint_mode(:error) do
|
||||
tpl = Liquid::Template.parse('{{ product.user_input | escape }}')
|
||||
tpl.render!('product' => ProductDrop.new)
|
||||
end
|
||||
assert_equal(' ', tpl.render!('product' => ProductDrop.new))
|
||||
end
|
||||
|
||||
def test_drop_does_only_respond_to_whitelisted_methods
|
||||
assert_equal '', Liquid::Template.parse('{{ product.inspect }}').render!('product' => ProductDrop.new)
|
||||
assert_equal '', Liquid::Template.parse('{{ product.pretty_inspect }}').render!('product' => ProductDrop.new)
|
||||
assert_equal '', Liquid::Template.parse('{{ product.whatever }}').render!('product' => ProductDrop.new)
|
||||
assert_equal '', Liquid::Template.parse('{{ product | map: "inspect" }}').render!('product' => ProductDrop.new)
|
||||
assert_equal '', Liquid::Template.parse('{{ product | map: "pretty_inspect" }}').render!('product' => ProductDrop.new)
|
||||
assert_equal '', Liquid::Template.parse('{{ product | map: "whatever" }}').render!('product' => ProductDrop.new)
|
||||
assert_equal("", Liquid::Template.parse("{{ product.inspect }}").render!('product' => ProductDrop.new))
|
||||
assert_equal("", Liquid::Template.parse("{{ product.pretty_inspect }}").render!('product' => ProductDrop.new))
|
||||
assert_equal("", Liquid::Template.parse("{{ product.whatever }}").render!('product' => ProductDrop.new))
|
||||
assert_equal("", Liquid::Template.parse('{{ product | map: "inspect" }}').render!('product' => ProductDrop.new))
|
||||
assert_equal("", Liquid::Template.parse('{{ product | map: "pretty_inspect" }}').render!('product' => ProductDrop.new))
|
||||
assert_equal("", Liquid::Template.parse('{{ product | map: "whatever" }}').render!('product' => ProductDrop.new))
|
||||
end
|
||||
|
||||
def test_drops_respond_to_to_liquid
|
||||
assert_equal 'text1', Liquid::Template.parse('{{ product.to_liquid.texts.text }}').render!('product' => ProductDrop.new)
|
||||
assert_equal 'text1', Liquid::Template.parse('{{ product | map: "to_liquid" | map: "texts" | map: "text" }}').render!('product' => ProductDrop.new)
|
||||
assert_equal("text1", Liquid::Template.parse("{{ product.to_liquid.texts.text }}").render!('product' => ProductDrop.new))
|
||||
assert_equal("text1", Liquid::Template.parse('{{ product | map: "to_liquid" | map: "texts" | map: "text" }}').render!('product' => ProductDrop.new))
|
||||
end
|
||||
|
||||
def test_text_drop
|
||||
output = Liquid::Template.parse(' {{ product.texts.text }} ').render!('product' => ProductDrop.new)
|
||||
assert_equal ' text1 ', output
|
||||
assert_equal(' text1 ', output)
|
||||
end
|
||||
|
||||
def test_catchall_unknown_method
|
||||
output = Liquid::Template.parse(' {{ product.catchall.unknown }} ').render!('product' => ProductDrop.new)
|
||||
assert_equal ' catchall_method: unknown ', output
|
||||
assert_equal(' catchall_method: unknown ', output)
|
||||
end
|
||||
|
||||
def test_catchall_integer_argument_drop
|
||||
output = Liquid::Template.parse(' {{ product.catchall[8] }} ').render!('product' => ProductDrop.new)
|
||||
assert_equal ' catchall_method: 8 ', output
|
||||
assert_equal(' catchall_method: 8 ', output)
|
||||
end
|
||||
|
||||
def test_text_array_drop
|
||||
output = Liquid::Template.parse('{% for text in product.texts.array %} {{text}} {% endfor %}').render!('product' => ProductDrop.new)
|
||||
assert_equal ' text1 text2 ', output
|
||||
assert_equal(' text1 text2 ', output)
|
||||
end
|
||||
|
||||
def test_context_drop
|
||||
output = Liquid::Template.parse(' {{ context.bar }} ').render!('context' => ContextDrop.new, 'bar' => 'carrot')
|
||||
assert_equal ' carrot ', output
|
||||
output = Liquid::Template.parse(' {{ context.bar }} ').render!('context' => ContextDrop.new, 'bar' => "carrot")
|
||||
assert_equal(' carrot ', output)
|
||||
end
|
||||
|
||||
def test_context_drop_array_with_map
|
||||
output = Liquid::Template.parse(' {{ contexts | map: "bar" }} ').render!('contexts' => [ContextDrop.new, ContextDrop.new], 'bar' => "carrot")
|
||||
assert_equal(' carrotcarrot ', output)
|
||||
end
|
||||
|
||||
def test_nested_context_drop
|
||||
output = Liquid::Template.parse(' {{ product.context.foo }} ').render!('product' => ProductDrop.new, 'foo' => 'monkey')
|
||||
assert_equal ' monkey ', output
|
||||
output = Liquid::Template.parse(' {{ product.context.foo }} ').render!('product' => ProductDrop.new, 'foo' => "monkey")
|
||||
assert_equal(' monkey ', output)
|
||||
end
|
||||
|
||||
def test_protected
|
||||
output = Liquid::Template.parse(' {{ product.callmenot }} ').render!('product' => ProductDrop.new)
|
||||
assert_equal ' ', output
|
||||
assert_equal(' ', output)
|
||||
end
|
||||
|
||||
def test_object_methods_not_allowed
|
||||
%i[dup clone singleton_class eval class_eval inspect].each do |method|
|
||||
[:dup, :clone, :singleton_class, :eval, :class_eval, :inspect].each do |method|
|
||||
output = Liquid::Template.parse(" {{ product.#{method} }} ").render!('product' => ProductDrop.new)
|
||||
assert_equal ' ', output
|
||||
assert_equal(' ', output)
|
||||
end
|
||||
end
|
||||
|
||||
def test_scope
|
||||
assert_equal '1', Liquid::Template.parse('{{ context.scopes }}').render!('context' => ContextDrop.new)
|
||||
assert_equal '2', Liquid::Template.parse('{%for i in dummy%}{{ context.scopes }}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1])
|
||||
assert_equal '3', Liquid::Template.parse('{%for i in dummy%}{%for i in dummy%}{{ context.scopes }}{%endfor%}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1])
|
||||
assert_equal('1', Liquid::Template.parse('{{ context.scopes }}').render!('context' => ContextDrop.new))
|
||||
assert_equal('2', Liquid::Template.parse('{%for i in dummy%}{{ context.scopes }}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1]))
|
||||
assert_equal('3', Liquid::Template.parse('{%for i in dummy%}{%for i in dummy%}{{ context.scopes }}{%endfor%}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1]))
|
||||
end
|
||||
|
||||
def test_scope_though_proc
|
||||
assert_equal '1', Liquid::Template.parse('{{ s }}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] })
|
||||
assert_equal '2', Liquid::Template.parse('{%for i in dummy%}{{ s }}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] }, 'dummy' => [1])
|
||||
assert_equal '3', Liquid::Template.parse('{%for i in dummy%}{%for i in dummy%}{{ s }}{%endfor%}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] }, 'dummy' => [1])
|
||||
assert_equal('1', Liquid::Template.parse('{{ s }}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] }))
|
||||
assert_equal('2', Liquid::Template.parse('{%for i in dummy%}{{ s }}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] }, 'dummy' => [1]))
|
||||
assert_equal('3', Liquid::Template.parse('{%for i in dummy%}{%for i in dummy%}{{ s }}{%endfor%}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] }, 'dummy' => [1]))
|
||||
end
|
||||
|
||||
def test_scope_with_assigns
|
||||
assert_equal 'variable', Liquid::Template.parse('{% assign a = "variable"%}{{a}}').render!('context' => ContextDrop.new)
|
||||
assert_equal 'variable', Liquid::Template.parse('{% assign a = "variable"%}{%for i in dummy%}{{a}}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1])
|
||||
assert_equal 'test', Liquid::Template.parse('{% assign header_gif = "test"%}{{header_gif}}').render!('context' => ContextDrop.new)
|
||||
assert_equal 'test', Liquid::Template.parse("{% assign header_gif = 'test'%}{{header_gif}}").render!('context' => ContextDrop.new)
|
||||
assert_equal('variable', Liquid::Template.parse('{% assign a = "variable"%}{{a}}').render!('context' => ContextDrop.new))
|
||||
assert_equal('variable', Liquid::Template.parse('{% assign a = "variable"%}{%for i in dummy%}{{a}}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1]))
|
||||
assert_equal('test', Liquid::Template.parse('{% assign header_gif = "test"%}{{header_gif}}').render!('context' => ContextDrop.new))
|
||||
assert_equal('test', Liquid::Template.parse("{% assign header_gif = 'test'%}{{header_gif}}").render!('context' => ContextDrop.new))
|
||||
end
|
||||
|
||||
def test_scope_from_tags
|
||||
assert_equal '1', Liquid::Template.parse('{% for i in context.scopes_as_array %}{{i}}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1])
|
||||
assert_equal '12', Liquid::Template.parse('{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1])
|
||||
assert_equal '123', Liquid::Template.parse('{%for a in dummy%}{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1])
|
||||
assert_equal('1', Liquid::Template.parse('{% for i in context.scopes_as_array %}{{i}}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1]))
|
||||
assert_equal('12', Liquid::Template.parse('{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1]))
|
||||
assert_equal('123', Liquid::Template.parse('{%for a in dummy%}{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1]))
|
||||
end
|
||||
|
||||
def test_access_context_from_drop
|
||||
assert_equal '123', Liquid::Template.parse('{%for a in dummy%}{{ context.loop_pos }}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1, 2, 3])
|
||||
assert_equal('123', Liquid::Template.parse('{%for a in dummy%}{{ context.loop_pos }}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1, 2, 3]))
|
||||
end
|
||||
|
||||
def test_enumerable_drop
|
||||
assert_equal '123', Liquid::Template.parse('{% for c in collection %}{{c}}{% endfor %}').render!('collection' => EnumerableDrop.new)
|
||||
assert_equal('123', Liquid::Template.parse('{% for c in collection %}{{c}}{% endfor %}').render!('collection' => EnumerableDrop.new))
|
||||
end
|
||||
|
||||
def test_enumerable_drop_size
|
||||
assert_equal '3', Liquid::Template.parse('{{collection.size}}').render!('collection' => EnumerableDrop.new)
|
||||
assert_equal('3', Liquid::Template.parse('{{collection.size}}').render!('collection' => EnumerableDrop.new))
|
||||
end
|
||||
|
||||
def test_enumerable_drop_will_invoke_liquid_method_missing_for_clashing_method_names
|
||||
%w[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)
|
||||
["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))
|
||||
end
|
||||
end
|
||||
|
||||
def test_some_enumerable_methods_still_get_invoked
|
||||
%i[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)
|
||||
[: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))
|
||||
end
|
||||
|
||||
assert_equal 'yes', Liquid::Template.parse('{% if collection contains 3 %}yes{% endif %}').render!('collection' => RealEnumerableDrop.new)
|
||||
assert_equal("yes", Liquid::Template.parse("{% if collection contains 3 %}yes{% endif %}").render!('collection' => RealEnumerableDrop.new))
|
||||
|
||||
%i[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)
|
||||
[: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))
|
||||
end
|
||||
end
|
||||
|
||||
def test_empty_string_value_access
|
||||
assert_equal '', Liquid::Template.parse('{{ product[value] }}').render!('product' => ProductDrop.new, 'value' => '')
|
||||
assert_equal('', Liquid::Template.parse('{{ product[value] }}').render!('product' => ProductDrop.new, 'value' => ''))
|
||||
end
|
||||
|
||||
def test_nil_value_access
|
||||
assert_equal '', Liquid::Template.parse('{{ product[value] }}').render!('product' => ProductDrop.new, 'value' => nil)
|
||||
assert_equal('', Liquid::Template.parse('{{ product[value] }}').render!('product' => ProductDrop.new, 'value' => nil))
|
||||
end
|
||||
|
||||
def test_default_to_s_on_drops
|
||||
assert_equal 'ProductDrop', Liquid::Template.parse('{{ product }}').render!('product' => ProductDrop.new)
|
||||
assert_equal 'EnumerableDrop', Liquid::Template.parse('{{ collection }}').render!('collection' => EnumerableDrop.new)
|
||||
assert_equal('ProductDrop', Liquid::Template.parse("{{ product }}").render!('product' => ProductDrop.new))
|
||||
assert_equal('EnumerableDrop', Liquid::Template.parse('{{ collection }}').render!('collection' => EnumerableDrop.new))
|
||||
end
|
||||
|
||||
def test_invokable_methods
|
||||
assert_equal(%w(to_liquid catchall context texts).to_set, ProductDrop.invokable_methods)
|
||||
assert_equal(%w(to_liquid scopes_as_array loop_pos scopes).to_set, ContextDrop.invokable_methods)
|
||||
assert_equal(%w(to_liquid size max min first count).to_set, EnumerableDrop.invokable_methods)
|
||||
assert_equal(%w(to_liquid max min sort count first).to_set, RealEnumerableDrop.invokable_methods)
|
||||
end
|
||||
end # DropsTest
|
||||
|
||||
@@ -35,31 +35,31 @@ class ErrorHandlingTest < Minitest::Test
|
||||
TEXT
|
||||
|
||||
output = Liquid::Template.parse(template, line_numbers: true).render('errors' => ErrorDrop.new)
|
||||
assert_equal expected, output
|
||||
assert_equal(expected, output)
|
||||
end
|
||||
|
||||
def test_standard_error
|
||||
template = Liquid::Template.parse(' {{ errors.standard_error }} ')
|
||||
assert_equal ' Liquid error: standard error ', template.render('errors' => ErrorDrop.new)
|
||||
assert_equal(' Liquid error: standard error ', template.render('errors' => ErrorDrop.new))
|
||||
|
||||
assert_equal 1, template.errors.size
|
||||
assert_equal StandardError, template.errors.first.class
|
||||
assert_equal(1, template.errors.size)
|
||||
assert_equal(StandardError, template.errors.first.class)
|
||||
end
|
||||
|
||||
def test_syntax
|
||||
template = Liquid::Template.parse(' {{ errors.syntax_error }} ')
|
||||
assert_equal ' Liquid syntax error: syntax error ', template.render('errors' => ErrorDrop.new)
|
||||
assert_equal(' Liquid syntax error: syntax error ', template.render('errors' => ErrorDrop.new))
|
||||
|
||||
assert_equal 1, template.errors.size
|
||||
assert_equal SyntaxError, template.errors.first.class
|
||||
assert_equal(1, template.errors.size)
|
||||
assert_equal(SyntaxError, template.errors.first.class)
|
||||
end
|
||||
|
||||
def test_argument
|
||||
template = Liquid::Template.parse(' {{ errors.argument_error }} ')
|
||||
assert_equal ' Liquid error: argument error ', template.render('errors' => ErrorDrop.new)
|
||||
assert_equal(' Liquid error: argument error ', template.render('errors' => ErrorDrop.new))
|
||||
|
||||
assert_equal 1, template.errors.size
|
||||
assert_equal ArgumentError, template.errors.first.class
|
||||
assert_equal(1, template.errors.size)
|
||||
assert_equal(ArgumentError, template.errors.first.class)
|
||||
end
|
||||
|
||||
def test_missing_endtag_parse_time_error
|
||||
@@ -78,9 +78,9 @@ class ErrorHandlingTest < Minitest::Test
|
||||
|
||||
def test_lax_unrecognized_operator
|
||||
template = Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', error_mode: :lax)
|
||||
assert_equal ' Liquid error: Unknown operator =! ', template.render
|
||||
assert_equal 1, template.errors.size
|
||||
assert_equal Liquid::ArgumentError, template.errors.first.class
|
||||
assert_equal(' Liquid error: Unknown operator =! ', template.render)
|
||||
assert_equal(1, template.errors.size)
|
||||
assert_equal(Liquid::ArgumentError, template.errors.first.class)
|
||||
end
|
||||
|
||||
def test_with_line_numbers_adds_numbers_to_parser_errors
|
||||
@@ -92,7 +92,7 @@ class ErrorHandlingTest < Minitest::Test
|
||||
|
||||
bla
|
||||
',
|
||||
line_numbers: true)
|
||||
line_numbers: true)
|
||||
end
|
||||
|
||||
assert_match(/Liquid syntax error \(line 4\)/, err.message)
|
||||
@@ -107,7 +107,7 @@ class ErrorHandlingTest < Minitest::Test
|
||||
|
||||
bla
|
||||
',
|
||||
line_numbers: true)
|
||||
line_numbers: true)
|
||||
end
|
||||
|
||||
assert_match(/Liquid syntax error \(line 4\)/, err.message)
|
||||
@@ -121,11 +121,11 @@ class ErrorHandlingTest < Minitest::Test
|
||||
|
||||
bla
|
||||
',
|
||||
error_mode: :warn,
|
||||
line_numbers: true)
|
||||
error_mode: :warn,
|
||||
line_numbers: true)
|
||||
|
||||
assert_equal ['Liquid syntax error (line 4): Unexpected character = in "1 =! 2"'],
|
||||
template.warnings.map(&:message)
|
||||
assert_equal(['Liquid syntax error (line 4): Unexpected character = in "1 =! 2"'],
|
||||
template.warnings.map(&:message))
|
||||
end
|
||||
|
||||
def test_parsing_strict_with_line_numbers_adds_numbers_to_lexer_errors
|
||||
@@ -137,11 +137,11 @@ class ErrorHandlingTest < Minitest::Test
|
||||
|
||||
bla
|
||||
',
|
||||
error_mode: :strict,
|
||||
line_numbers: true)
|
||||
error_mode: :strict,
|
||||
line_numbers: true)
|
||||
end
|
||||
|
||||
assert_equal 'Liquid syntax error (line 4): Unexpected character = in "1 =! 2"', err.message
|
||||
assert_equal('Liquid syntax error (line 4): Unexpected character = in "1 =! 2"', err.message)
|
||||
end
|
||||
|
||||
def test_syntax_errors_in_nested_blocks_have_correct_line_number
|
||||
@@ -155,45 +155,45 @@ class ErrorHandlingTest < Minitest::Test
|
||||
|
||||
bla
|
||||
',
|
||||
line_numbers: true)
|
||||
line_numbers: true)
|
||||
end
|
||||
|
||||
assert_equal "Liquid syntax error (line 5): Unknown tag 'foo'", err.message
|
||||
assert_equal("Liquid syntax error (line 5): Unknown tag 'foo'", err.message)
|
||||
end
|
||||
|
||||
def test_strict_error_messages
|
||||
err = assert_raises(SyntaxError) do
|
||||
Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', error_mode: :strict)
|
||||
end
|
||||
assert_equal 'Liquid syntax error: Unexpected character = in "1 =! 2"', err.message
|
||||
assert_equal('Liquid syntax error: Unexpected character = in "1 =! 2"', err.message)
|
||||
|
||||
err = assert_raises(SyntaxError) do
|
||||
Liquid::Template.parse('{{%%%}}', error_mode: :strict)
|
||||
end
|
||||
assert_equal 'Liquid syntax error: Unexpected character % in "{{%%%}}"', err.message
|
||||
assert_equal('Liquid syntax error: Unexpected character % in "{{%%%}}"', err.message)
|
||||
end
|
||||
|
||||
def test_warnings
|
||||
template = Liquid::Template.parse('{% if ~~~ %}{{%%%}}{% else %}{{ hello. }}{% endif %}', error_mode: :warn)
|
||||
assert_equal 3, template.warnings.size
|
||||
assert_equal 'Unexpected character ~ in "~~~"', template.warnings[0].to_s(false)
|
||||
assert_equal 'Unexpected character % in "{{%%%}}"', template.warnings[1].to_s(false)
|
||||
assert_equal 'Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].to_s(false)
|
||||
assert_equal '', template.render
|
||||
assert_equal(3, template.warnings.size)
|
||||
assert_equal('Unexpected character ~ in "~~~"', template.warnings[0].to_s(false))
|
||||
assert_equal('Unexpected character % in "{{%%%}}"', template.warnings[1].to_s(false))
|
||||
assert_equal('Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].to_s(false))
|
||||
assert_equal('', template.render)
|
||||
end
|
||||
|
||||
def test_warning_line_numbers
|
||||
template = Liquid::Template.parse("{% if ~~~ %}\n{{%%%}}{% else %}\n{{ hello. }}{% endif %}", error_mode: :warn, line_numbers: true)
|
||||
assert_equal 'Liquid syntax error (line 1): Unexpected character ~ in "~~~"', template.warnings[0].message
|
||||
assert_equal 'Liquid syntax error (line 2): Unexpected character % in "{{%%%}}"', template.warnings[1].message
|
||||
assert_equal 'Liquid syntax error (line 3): Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].message
|
||||
assert_equal 3, template.warnings.size
|
||||
assert_equal [1, 2, 3], template.warnings.map(&:line_number)
|
||||
assert_equal('Liquid syntax error (line 1): Unexpected character ~ in "~~~"', template.warnings[0].message)
|
||||
assert_equal('Liquid syntax error (line 2): Unexpected character % in "{{%%%}}"', template.warnings[1].message)
|
||||
assert_equal('Liquid syntax error (line 3): Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].message)
|
||||
assert_equal(3, template.warnings.size)
|
||||
assert_equal([1, 2, 3], template.warnings.map(&:line_number))
|
||||
end
|
||||
|
||||
# Liquid should not catch Exceptions that are not subclasses of StandardError, like Interrupt and NoMemoryError
|
||||
def test_exceptions_propagate
|
||||
assert_raises Exception do
|
||||
assert_raises(Exception) do
|
||||
template = Liquid::Template.parse('{{ errors.exception }}')
|
||||
template.render('errors' => ErrorDrop.new)
|
||||
end
|
||||
@@ -204,14 +204,14 @@ class ErrorHandlingTest < Minitest::Test
|
||||
|
||||
output = template.render('errors' => ErrorDrop.new)
|
||||
|
||||
assert_equal 'This is a runtime error: Liquid error (line 1): internal', output
|
||||
assert_equal [Liquid::InternalError], template.errors.map(&:class)
|
||||
assert_equal('This is a runtime error: Liquid error (line 1): internal', output)
|
||||
assert_equal([Liquid::InternalError], template.errors.map(&:class))
|
||||
end
|
||||
|
||||
def test_setting_default_exception_renderer
|
||||
old_exception_renderer = Liquid::Template.default_exception_renderer
|
||||
exceptions = []
|
||||
Liquid::Template.default_exception_renderer = lambda { |e|
|
||||
Liquid::Template.default_exception_renderer = ->(e) {
|
||||
exceptions << e
|
||||
''
|
||||
}
|
||||
@@ -219,31 +219,31 @@ class ErrorHandlingTest < Minitest::Test
|
||||
|
||||
output = template.render('errors' => ErrorDrop.new)
|
||||
|
||||
assert_equal 'This is a runtime error: ', output
|
||||
assert_equal [Liquid::ArgumentError], template.errors.map(&:class)
|
||||
assert_equal('This is a runtime error: ', output)
|
||||
assert_equal([Liquid::ArgumentError], template.errors.map(&:class))
|
||||
ensure
|
||||
Liquid::Template.default_exception_renderer = old_exception_renderer if old_exception_renderer
|
||||
end
|
||||
|
||||
def test_exception_renderer_exposing_non_liquid_error
|
||||
template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true)
|
||||
template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true)
|
||||
exceptions = []
|
||||
handler = lambda { |e|
|
||||
handler = ->(e) {
|
||||
exceptions << e
|
||||
e.cause
|
||||
}
|
||||
|
||||
output = template.render({ 'errors' => ErrorDrop.new }, exception_renderer: handler)
|
||||
|
||||
assert_equal 'This is a runtime error: runtime error', output
|
||||
assert_equal [Liquid::InternalError], exceptions.map(&:class)
|
||||
assert_equal exceptions, template.errors
|
||||
assert_equal '#<RuntimeError: runtime error>', exceptions.first.cause.inspect
|
||||
assert_equal('This is a runtime error: runtime error', output)
|
||||
assert_equal([Liquid::InternalError], exceptions.map(&:class))
|
||||
assert_equal(exceptions, template.errors)
|
||||
assert_equal('#<RuntimeError: runtime error>', exceptions.first.cause.inspect)
|
||||
end
|
||||
|
||||
class TestFileSystem
|
||||
def read_template_file(_template_path)
|
||||
'{{ errors.argument_error }}'
|
||||
"{{ errors.argument_error }}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -252,12 +252,21 @@ class ErrorHandlingTest < Minitest::Test
|
||||
|
||||
begin
|
||||
Liquid::Template.file_system = TestFileSystem.new
|
||||
|
||||
template = Liquid::Template.parse("Argument error:\n{% include 'product' %}", line_numbers: true)
|
||||
page = template.render('errors' => ErrorDrop.new)
|
||||
page = template.render('errors' => ErrorDrop.new)
|
||||
ensure
|
||||
Liquid::Template.file_system = old_file_system
|
||||
end
|
||||
assert_equal "Argument error:\nLiquid error (product line 1): argument error", page
|
||||
assert_equal 'product', template.errors.first.template_name
|
||||
assert_equal("Argument error:\nLiquid error (product line 1): argument error", page)
|
||||
assert_equal("product", template.errors.first.template_name)
|
||||
end
|
||||
|
||||
def test_bug_compatible_silencing_of_errors_in_blank_nodes
|
||||
output = Liquid::Template.parse("{% assign x = 0 %}{% if 1 < '2' %}not blank{% assign x = 3 %}{% endif %}{{ x }}").render
|
||||
assert_equal("Liquid error: comparison of Integer with String failed0", output)
|
||||
|
||||
output = Liquid::Template.parse("{% assign x = 0 %}{% if 1 < '2' %}{% assign x = 3 %}{% endif %}{{ x }}").render
|
||||
assert_equal("0", output)
|
||||
end
|
||||
end
|
||||
|
||||
46
test/integration/expression_test.rb
Normal file
46
test/integration/expression_test.rb
Normal file
@@ -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
|
||||
@@ -29,7 +29,7 @@ class FiltersTest < Minitest::Test
|
||||
|
||||
module OverrideObjectMethodFilter
|
||||
def tap(_input)
|
||||
'tap overridden'
|
||||
"tap overridden"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -41,13 +41,13 @@ class FiltersTest < Minitest::Test
|
||||
@context['var'] = 1000
|
||||
@context.add_filters(MoneyFilter)
|
||||
|
||||
assert_equal ' 1000$ ', Template.parse('{{var | money}}').render(@context)
|
||||
assert_equal(' 1000$ ', Template.parse("{{var | money}}").render(@context))
|
||||
end
|
||||
|
||||
def test_underscore_in_filter_name
|
||||
@context['var'] = 1000
|
||||
@context.add_filters(MoneyFilter)
|
||||
assert_equal ' 1000$ ', Template.parse('{{var | money_with_underscore}}').render(@context)
|
||||
assert_equal(' 1000$ ', Template.parse("{{var | money_with_underscore}}").render(@context))
|
||||
end
|
||||
|
||||
def test_second_filter_overwrites_first
|
||||
@@ -55,103 +55,112 @@ class FiltersTest < Minitest::Test
|
||||
@context.add_filters(MoneyFilter)
|
||||
@context.add_filters(CanadianMoneyFilter)
|
||||
|
||||
assert_equal ' 1000$ CAD ', Template.parse('{{var | money}}').render(@context)
|
||||
assert_equal(' 1000$ CAD ', Template.parse("{{var | money}}").render(@context))
|
||||
end
|
||||
|
||||
def test_size
|
||||
@context['var'] = 'abcd'
|
||||
@context.add_filters(MoneyFilter)
|
||||
|
||||
assert_equal '4', Template.parse('{{var | size}}').render(@context)
|
||||
assert_equal('4', Template.parse("{{var | size}}").render(@context))
|
||||
end
|
||||
|
||||
def test_join
|
||||
@context['var'] = [1, 2, 3, 4]
|
||||
|
||||
assert_equal '1 2 3 4', Template.parse('{{var | join}}').render(@context)
|
||||
assert_equal("1 2 3 4", Template.parse("{{var | join}}").render(@context))
|
||||
end
|
||||
|
||||
def test_sort
|
||||
@context['value'] = 3
|
||||
@context['value'] = 3
|
||||
@context['numbers'] = [2, 1, 4, 3]
|
||||
@context['words'] = %w[expected as alphabetic]
|
||||
@context['arrays'] = %w[flower are]
|
||||
@context['case_sensitive'] = %w[sensitive Expected case]
|
||||
@context['words'] = ['expected', 'as', 'alphabetic']
|
||||
@context['arrays'] = ['flower', 'are']
|
||||
@context['case_sensitive'] = ['sensitive', 'Expected', 'case']
|
||||
|
||||
assert_equal '1 2 3 4', Template.parse('{{numbers | sort | join}}').render(@context)
|
||||
assert_equal 'alphabetic as expected', Template.parse('{{words | sort | join}}').render(@context)
|
||||
assert_equal '3', Template.parse('{{value | sort}}').render(@context)
|
||||
assert_equal 'are flower', Template.parse('{{arrays | sort | join}}').render(@context)
|
||||
assert_equal 'Expected case sensitive', Template.parse('{{case_sensitive | sort | join}}').render(@context)
|
||||
assert_equal('1 2 3 4', Template.parse("{{numbers | sort | join}}").render(@context))
|
||||
assert_equal('alphabetic as expected', Template.parse("{{words | sort | join}}").render(@context))
|
||||
assert_equal('3', Template.parse("{{value | sort}}").render(@context))
|
||||
assert_equal('are flower', Template.parse("{{arrays | sort | join}}").render(@context))
|
||||
assert_equal('Expected case sensitive', Template.parse("{{case_sensitive | sort | join}}").render(@context))
|
||||
end
|
||||
|
||||
def test_sort_natural
|
||||
@context['words'] = %w[case Assert Insensitive]
|
||||
@context['hashes'] = [{ 'a' => 'A' }, { 'a' => 'b' }, { 'a' => 'C' }]
|
||||
@context['words'] = ['case', 'Assert', 'Insensitive']
|
||||
@context['hashes'] = [{ 'a' => 'A' }, { 'a' => 'b' }, { 'a' => 'C' }]
|
||||
@context['objects'] = [TestObject.new('A'), TestObject.new('b'), TestObject.new('C')]
|
||||
|
||||
# Test strings
|
||||
assert_equal 'Assert case Insensitive', Template.parse('{{words | sort_natural | join}}').render(@context)
|
||||
assert_equal('Assert case Insensitive', Template.parse("{{words | sort_natural | join}}").render(@context))
|
||||
|
||||
# Test hashes
|
||||
assert_equal 'A b C', Template.parse("{{hashes | sort_natural: 'a' | map: 'a' | join}}").render(@context)
|
||||
assert_equal('A b C', Template.parse("{{hashes | sort_natural: 'a' | map: 'a' | join}}").render(@context))
|
||||
|
||||
# Test objects
|
||||
assert_equal 'A b C', Template.parse("{{objects | sort_natural: 'a' | map: 'a' | join}}").render(@context)
|
||||
assert_equal('A b C', Template.parse("{{objects | sort_natural: 'a' | map: 'a' | join}}").render(@context))
|
||||
end
|
||||
|
||||
def test_compact
|
||||
@context['words'] = ['a', nil, 'b', nil, 'c']
|
||||
@context['hashes'] = [{ 'a' => 'A' }, { 'a' => nil }, { 'a' => 'C' }]
|
||||
@context['words'] = ['a', nil, 'b', nil, 'c']
|
||||
@context['hashes'] = [{ 'a' => 'A' }, { 'a' => nil }, { 'a' => 'C' }]
|
||||
@context['objects'] = [TestObject.new('A'), TestObject.new(nil), TestObject.new('C')]
|
||||
|
||||
# Test strings
|
||||
assert_equal 'a b c', Template.parse('{{words | compact | join}}').render(@context)
|
||||
assert_equal('a b c', Template.parse("{{words | compact | join}}").render(@context))
|
||||
|
||||
# Test hashes
|
||||
assert_equal 'A C', Template.parse("{{hashes | compact: 'a' | map: 'a' | join}}").render(@context)
|
||||
assert_equal('A C', Template.parse("{{hashes | compact: 'a' | map: 'a' | join}}").render(@context))
|
||||
|
||||
# Test objects
|
||||
assert_equal 'A C', Template.parse("{{objects | compact: 'a' | map: 'a' | join}}").render(@context)
|
||||
assert_equal('A C', Template.parse("{{objects | compact: 'a' | map: 'a' | join}}").render(@context))
|
||||
end
|
||||
|
||||
def test_strip_html
|
||||
@context['var'] = '<b>bla blub</a>'
|
||||
@context['var'] = "<b>bla blub</a>"
|
||||
|
||||
assert_equal 'bla blub', Template.parse('{{ var | strip_html }}').render(@context)
|
||||
assert_equal("bla blub", Template.parse("{{ var | strip_html }}").render(@context))
|
||||
end
|
||||
|
||||
def test_strip_html_ignore_comments_with_html
|
||||
@context['var'] = '<!-- split and some <ul> tag --><b>bla blub</a>'
|
||||
@context['var'] = "<!-- split and some <ul> tag --><b>bla blub</a>"
|
||||
|
||||
assert_equal 'bla blub', Template.parse('{{ var | strip_html }}').render(@context)
|
||||
assert_equal("bla blub", Template.parse("{{ var | strip_html }}").render(@context))
|
||||
end
|
||||
|
||||
def test_capitalize
|
||||
@context['var'] = 'blub'
|
||||
@context['var'] = "blub"
|
||||
|
||||
assert_equal 'Blub', Template.parse('{{ var | capitalize }}').render(@context)
|
||||
assert_equal("Blub", Template.parse("{{ var | capitalize }}").render(@context))
|
||||
end
|
||||
|
||||
def test_nonexistent_filter_is_ignored
|
||||
@context['var'] = 1000
|
||||
|
||||
assert_equal '1000', Template.parse('{{ var | xyzzy }}').render(@context)
|
||||
assert_equal('1000', Template.parse("{{ var | xyzzy }}").render(@context))
|
||||
end
|
||||
|
||||
def test_filter_with_keyword_arguments
|
||||
@context['surname'] = 'john'
|
||||
@context['input'] = 'hello %{first_name}, %{last_name}'
|
||||
@context['input'] = 'hello %{first_name}, %{last_name}'
|
||||
@context.add_filters(SubstituteFilter)
|
||||
output = Template.parse(%({{ input | substitute: first_name: surname, last_name: 'doe' }})).render(@context)
|
||||
assert_equal 'hello john, doe', output
|
||||
output = Template.parse(%({{ input | substitute: first_name: surname, last_name: 'doe' }})).render(@context)
|
||||
assert_equal('hello john, doe', output)
|
||||
end
|
||||
|
||||
def test_override_object_method_in_filter
|
||||
assert_equal 'tap overridden', Template.parse('{{var | tap}}').render!({ 'var' => 1000 }, filters: [OverrideObjectMethodFilter])
|
||||
assert_equal("tap overridden", Template.parse("{{var | tap}}").render!({ 'var' => 1000 }, filters: [OverrideObjectMethodFilter]))
|
||||
|
||||
# tap still treated as a non-existent filter
|
||||
assert_equal '1000', Template.parse('{{var | tap}}').render!('var' => 1000)
|
||||
assert_equal("1000", Template.parse("{{var | tap}}").render!('var' => 1000))
|
||||
end
|
||||
|
||||
def test_liquid_argument_error
|
||||
source = "{{ '' | size: 'too many args' }}"
|
||||
exc = assert_raises(Liquid::ArgumentError) do
|
||||
Template.parse(source).render!
|
||||
end
|
||||
assert_match(/\ALiquid error: wrong number of arguments /, exc.message)
|
||||
assert_equal(exc.message, Template.parse(source).render)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -160,15 +169,15 @@ class FiltersInTemplate < Minitest::Test
|
||||
|
||||
def test_local_global
|
||||
with_global_filter(MoneyFilter) do
|
||||
assert_equal ' 1000$ ', Template.parse('{{1000 | money}}').render!(nil, nil)
|
||||
assert_equal ' 1000$ CAD ', Template.parse('{{1000 | money}}').render!(nil, filters: CanadianMoneyFilter)
|
||||
assert_equal ' 1000$ CAD ', Template.parse('{{1000 | money}}').render!(nil, filters: [CanadianMoneyFilter])
|
||||
assert_equal(" 1000$ ", Template.parse("{{1000 | money}}").render!(nil, nil))
|
||||
assert_equal(" 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, filters: CanadianMoneyFilter))
|
||||
assert_equal(" 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, filters: [CanadianMoneyFilter]))
|
||||
end
|
||||
end
|
||||
|
||||
def test_local_filter_with_deprecated_syntax
|
||||
assert_equal ' 1000$ CAD ', Template.parse('{{1000 | money}}').render!(nil, CanadianMoneyFilter)
|
||||
assert_equal ' 1000$ CAD ', Template.parse('{{1000 | money}}').render!(nil, [CanadianMoneyFilter])
|
||||
assert_equal(" 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, CanadianMoneyFilter))
|
||||
assert_equal(" 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, [CanadianMoneyFilter]))
|
||||
end
|
||||
end # FiltersTest
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ class HashOrderingTest < Minitest::Test
|
||||
|
||||
def test_global_register_order
|
||||
with_global_filter(MoneyFilter, CanadianMoneyFilter) do
|
||||
assert_equal ' 1000$ CAD ', Template.parse('{{1000 | money}}').render(nil, nil)
|
||||
assert_equal(" 1000$ CAD ", Template.parse("{{1000 | money}}").render(nil, nil))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
require 'liquid/legacy'
|
||||
|
||||
class FiltersTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def test_constants
|
||||
assert_equal Liquid::FilterSeparator, Liquid::FILTER_SEPARATOR
|
||||
assert_equal Liquid::BlockBody::ContentOfVariable, Liquid::BlockBody::CONTENT_OF_VARIABLE
|
||||
end
|
||||
end
|
||||
@@ -11,11 +11,11 @@ module FunnyFilter
|
||||
"LOL: #{input}"
|
||||
end
|
||||
|
||||
def add_smiley(input, smiley = ':-)')
|
||||
def add_smiley(input, smiley = ":-)")
|
||||
"#{input} #{smiley}"
|
||||
end
|
||||
|
||||
def add_tag(input, tag = 'p', id = 'foo')
|
||||
def add_tag(input, tag = "p", id = "foo")
|
||||
%(<#{tag} id="#{id}">#{input}</#{tag}>)
|
||||
end
|
||||
|
||||
@@ -42,84 +42,84 @@ class OutputTest < Minitest::Test
|
||||
text = %( {{best_cars}} )
|
||||
|
||||
expected = %( bmw )
|
||||
assert_equal expected, Template.parse(text).render!(@assigns)
|
||||
assert_equal(expected, Template.parse(text).render!(@assigns))
|
||||
end
|
||||
|
||||
def test_variable_traversing_with_two_brackets
|
||||
text = %({{ site.data.menu[include.menu][include.locale] }})
|
||||
assert_equal 'it works!', Template.parse(text).render!(
|
||||
'site' => { 'data' => { 'menu' => { 'foo' => { 'bar' => 'it works!' } } } },
|
||||
'include' => { 'menu' => 'foo', 'locale' => 'bar' }
|
||||
)
|
||||
assert_equal("it works!", Template.parse(text).render!(
|
||||
"site" => { "data" => { "menu" => { "foo" => { "bar" => "it works!" } } } },
|
||||
"include" => { "menu" => "foo", "locale" => "bar" }
|
||||
))
|
||||
end
|
||||
|
||||
def test_variable_traversing
|
||||
text = %( {{car.bmw}} {{car.gm}} {{car.bmw}} )
|
||||
|
||||
expected = %( good bad good )
|
||||
assert_equal expected, Template.parse(text).render!(@assigns)
|
||||
assert_equal(expected, Template.parse(text).render!(@assigns))
|
||||
end
|
||||
|
||||
def test_variable_piping
|
||||
text = %( {{ car.gm | make_funny }} )
|
||||
text = %( {{ car.gm | make_funny }} )
|
||||
expected = %( LOL )
|
||||
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
|
||||
assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
|
||||
end
|
||||
|
||||
def test_variable_piping_with_input
|
||||
text = %( {{ car.gm | cite_funny }} )
|
||||
text = %( {{ car.gm | cite_funny }} )
|
||||
expected = %( LOL: bad )
|
||||
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
|
||||
assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
|
||||
end
|
||||
|
||||
def test_variable_piping_with_args
|
||||
text = %! {{ car.gm | add_smiley : ':-(' }} !
|
||||
text = %! {{ car.gm | add_smiley : ':-(' }} !
|
||||
expected = %| bad :-( |
|
||||
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
|
||||
assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
|
||||
end
|
||||
|
||||
def test_variable_piping_with_no_args
|
||||
text = %( {{ car.gm | add_smiley }} )
|
||||
text = %( {{ car.gm | add_smiley }} )
|
||||
expected = %| bad :-) |
|
||||
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
|
||||
assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
|
||||
end
|
||||
|
||||
def test_multiple_variable_piping_with_args
|
||||
text = %! {{ car.gm | add_smiley : ':-(' | add_smiley : ':-('}} !
|
||||
text = %! {{ car.gm | add_smiley : ':-(' | add_smiley : ':-('}} !
|
||||
expected = %| bad :-( :-( |
|
||||
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
|
||||
assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
|
||||
end
|
||||
|
||||
def test_variable_piping_with_multiple_args
|
||||
text = %( {{ car.gm | add_tag : 'span', 'bar'}} )
|
||||
text = %( {{ car.gm | add_tag : 'span', 'bar'}} )
|
||||
expected = %( <span id="bar">bad</span> )
|
||||
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
|
||||
assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
|
||||
end
|
||||
|
||||
def test_variable_piping_with_variable_args
|
||||
text = %( {{ car.gm | add_tag : 'span', car.bmw}} )
|
||||
text = %( {{ car.gm | add_tag : 'span', car.bmw}} )
|
||||
expected = %( <span id="good">bad</span> )
|
||||
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
|
||||
assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
|
||||
end
|
||||
|
||||
def test_multiple_pipings
|
||||
text = %( {{ best_cars | cite_funny | paragraph }} )
|
||||
text = %( {{ best_cars | cite_funny | paragraph }} )
|
||||
expected = %( <p>LOL: bmw</p> )
|
||||
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
|
||||
assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
|
||||
end
|
||||
|
||||
def test_link_to
|
||||
text = %( {{ 'Typo' | link_to: 'http://typo.leetsoft.com' }} )
|
||||
text = %( {{ 'Typo' | link_to: 'http://typo.leetsoft.com' }} )
|
||||
expected = %( <a href="http://typo.leetsoft.com">Typo</a> )
|
||||
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
|
||||
assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
|
||||
end
|
||||
end # OutputTest
|
||||
|
||||
@@ -6,38 +6,38 @@ class ParsingQuirksTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def test_parsing_css
|
||||
text = ' div { font-weight: bold; } '
|
||||
assert_equal text, Template.parse(text).render!
|
||||
text = " div { font-weight: bold; } "
|
||||
assert_equal(text, Template.parse(text).render!)
|
||||
end
|
||||
|
||||
def test_raise_on_single_close_bracet
|
||||
assert_raises(SyntaxError) do
|
||||
Template.parse('text {{method} oh nos!')
|
||||
Template.parse("text {{method} oh nos!")
|
||||
end
|
||||
end
|
||||
|
||||
def test_raise_on_label_and_no_close_bracets
|
||||
assert_raises(SyntaxError) do
|
||||
Template.parse('TEST {{ ')
|
||||
Template.parse("TEST {{ ")
|
||||
end
|
||||
end
|
||||
|
||||
def test_raise_on_label_and_no_close_bracets_percent
|
||||
assert_raises(SyntaxError) do
|
||||
Template.parse('TEST {% ')
|
||||
Template.parse("TEST {% ")
|
||||
end
|
||||
end
|
||||
|
||||
def test_error_on_empty_filter
|
||||
assert Template.parse('{{test}}')
|
||||
assert(Template.parse("{{test}}"))
|
||||
|
||||
with_error_mode(:lax) do
|
||||
assert Template.parse('{{|test}}')
|
||||
assert(Template.parse("{{|test}}"))
|
||||
end
|
||||
|
||||
with_error_mode(:strict) do
|
||||
assert_raises(SyntaxError) { Template.parse('{{|test}}') }
|
||||
assert_raises(SyntaxError) { Template.parse('{{test |a|b|}}') }
|
||||
assert_raises(SyntaxError) { Template.parse("{{|test}}") }
|
||||
assert_raises(SyntaxError) { Template.parse("{{test |a|b|}}") }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -53,35 +53,35 @@ class ParsingQuirksTest < Minitest::Test
|
||||
def test_unexpected_characters_syntax_error
|
||||
with_error_mode(:strict) do
|
||||
assert_raises(SyntaxError) do
|
||||
markup = 'true && false'
|
||||
markup = "true && false"
|
||||
Template.parse("{% if #{markup} %} YES {% endif %}")
|
||||
end
|
||||
assert_raises(SyntaxError) do
|
||||
markup = 'false || true'
|
||||
markup = "false || true"
|
||||
Template.parse("{% if #{markup} %} YES {% endif %}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_no_error_on_lax_empty_filter
|
||||
assert Template.parse('{{test |a|b|}}', error_mode: :lax)
|
||||
assert Template.parse('{{test}}', error_mode: :lax)
|
||||
assert Template.parse('{{|test|}}', error_mode: :lax)
|
||||
assert(Template.parse("{{test |a|b|}}", error_mode: :lax))
|
||||
assert(Template.parse("{{test}}", error_mode: :lax))
|
||||
assert(Template.parse("{{|test|}}", error_mode: :lax))
|
||||
end
|
||||
|
||||
def test_meaningless_parens_lax
|
||||
with_error_mode(:lax) do
|
||||
assigns = { 'b' => 'bar', 'c' => 'baz' }
|
||||
markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false"
|
||||
markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false"
|
||||
assert_template_result(' YES ', "{% if #{markup} %} YES {% endif %}", assigns)
|
||||
end
|
||||
end
|
||||
|
||||
def test_unexpected_characters_silently_eat_logic_lax
|
||||
with_error_mode(:lax) do
|
||||
markup = 'true && false'
|
||||
markup = "true && false"
|
||||
assert_template_result(' YES ', "{% if #{markup} %} YES {% endif %}")
|
||||
markup = 'false || true'
|
||||
markup = "false || true"
|
||||
assert_template_result('', "{% if #{markup} %} YES {% endif %}")
|
||||
end
|
||||
end
|
||||
@@ -114,10 +114,20 @@ class ParsingQuirksTest < Minitest::Test
|
||||
|
||||
def test_extra_dots_in_ranges
|
||||
with_error_mode(:lax) do
|
||||
assert_template_result('12345', '{% for i in (1...5) %}{{ i }}{% endfor %}')
|
||||
assert_template_result('12345', "{% for i in (1...5) %}{{ i }}{% endfor %}")
|
||||
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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user