mirror of
https://github.com/kemko/liquid.git
synced 2026-01-02 00:05:42 +03:00
Compare commits
208 Commits
force-to_l
...
rubocop-up
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d0917493d | ||
|
|
ef8a6dcda1 | ||
|
|
0dd22af7d6 | ||
|
|
da9051eb18 | ||
|
|
831355dfbd | ||
|
|
00702d8e63 | ||
|
|
197c058208 | ||
|
|
98dfe198e1 | ||
|
|
c2c1497ca8 | ||
|
|
d19967a79d | ||
|
|
248c54a386 | ||
|
|
2c42447659 | ||
|
|
9ef6f9b642 | ||
|
|
4684478e94 | ||
|
|
b3b63a683f | ||
|
|
1c577c5b62 | ||
|
|
755d2821f3 | ||
|
|
495b3d312f | ||
|
|
9640e77805 | ||
|
|
453f6348c2 | ||
|
|
70ed1fc86d | ||
|
|
2a1ca3152d | ||
|
|
c2ef247be5 | ||
|
|
1518d3f6f9 | ||
|
|
c67b77709d | ||
|
|
c89ce9c2ed | ||
|
|
b0629f17f7 | ||
|
|
274f078806 | ||
|
|
d7171aa084 | ||
|
|
06c4789dc5 | ||
|
|
f2f467bdbc | ||
|
|
ff99d92c18 | ||
|
|
39fecd06db | ||
|
|
8013df8ca2 | ||
|
|
14cd011cb5 | ||
|
|
e2d9907df2 | ||
|
|
23d669f5e6 | ||
|
|
ed73794f82 | ||
|
|
f59f6dea83 | ||
|
|
7a81fb821a | ||
|
|
cec27ea326 | ||
|
|
14999e8f7c | ||
|
|
b41fc10d8e | ||
|
|
2b3c81cfd0 | ||
|
|
2a2376bfd9 | ||
|
|
ca9e75db53 | ||
|
|
407c8abf30 | ||
|
|
43f181e211 | ||
|
|
7c613e87cb | ||
|
|
fe4034ccf9 | ||
|
|
52ee303a36 | ||
|
|
8217a8d86c | ||
|
|
7d13d88258 | ||
|
|
ff727016ef | ||
|
|
c11fc656cf | ||
|
|
d789ec4175 | ||
|
|
fd09f049b0 | ||
|
|
842986a972 | ||
|
|
4661700a97 | ||
|
|
cd5a6dd225 | ||
|
|
89c1ba2b0e | ||
|
|
479d8fb4a4 | ||
|
|
53b8babf52 | ||
|
|
76b4920d3e | ||
|
|
8dcc319128 | ||
|
|
0b36461d80 | ||
|
|
70e75719de | ||
|
|
b037b19688 | ||
|
|
d0f77f6cf4 | ||
|
|
0be260bc97 | ||
|
|
5f0b64cebc | ||
|
|
c086017bc9 | ||
|
|
4369fe6c85 | ||
|
|
c118e6b435 | ||
|
|
0fbaf873d9 | ||
|
|
5980ddbfae | ||
|
|
193fc0fb7a | ||
|
|
e4da4d49d2 | ||
|
|
a0bec1f873 | ||
|
|
4aa3261518 | ||
|
|
04d552fabb | ||
|
|
5106466a2d | ||
|
|
5d6c1ed7c6 | ||
|
|
a594653a0c | ||
|
|
0c802aba17 | ||
|
|
147d7ae24d | ||
|
|
282d42f98d | ||
|
|
e6ba6ee87b | ||
|
|
2ad7a37d44 | ||
|
|
4bdaaf069f | ||
|
|
85b1e91aed | ||
|
|
a7c5e247c8 | ||
|
|
6c117fd7dd | ||
|
|
7d2d90d715 | ||
|
|
f761d21215 | ||
|
|
a796c17f8b | ||
|
|
deb10ebc7a | ||
|
|
cfe1844de9 | ||
|
|
59950bff87 | ||
|
|
27c91203ab | ||
|
|
44eaa4b9d8 | ||
|
|
a979b3ec95 | ||
|
|
bf3e759da3 | ||
|
|
59162f7a0e | ||
|
|
c582b86f16 | ||
|
|
e340803d12 | ||
|
|
48a6d86ac2 | ||
|
|
3bb29d5456 | ||
|
|
9c72ccb82f | ||
|
|
62d4625468 | ||
|
|
8928454e29 | ||
|
|
1370a102c9 | ||
|
|
c9bac9befe | ||
|
|
210a0616f3 | ||
|
|
5149cde5c3 | ||
|
|
22f2cec5de | ||
|
|
4318240ae0 | ||
|
|
aa79c33dda | ||
|
|
b1ef28566e | ||
|
|
41bcc48222 | ||
|
|
27d5106dc9 | ||
|
|
7334073be2 | ||
|
|
5dcefd7d77 | ||
|
|
25c7b05916 | ||
|
|
d17f86ba4d | ||
|
|
384e4313ff | ||
|
|
23f2af8ff5 | ||
|
|
a93eac0268 | ||
|
|
2cc7493cb0 | ||
|
|
85463e1753 | ||
|
|
52ff9b0e84 | ||
|
|
0c58328a40 | ||
|
|
2bb3552033 | ||
|
|
8b751ddf46 | ||
|
|
e5cbdb2b27 | ||
|
|
ffb0ace303 | ||
|
|
ad00998ef8 | ||
|
|
869dbc7ebf | ||
|
|
fae3a2de7b | ||
|
|
f27bd619b9 | ||
|
|
a9b84b7806 | ||
|
|
6cc2c567c5 | ||
|
|
812e3c51b9 | ||
|
|
9dd0801f5c | ||
|
|
b146b49f46 | ||
|
|
86944fe7b7 | ||
|
|
a549d289d7 | ||
|
|
b2feeacbce | ||
|
|
143ba39a08 | ||
|
|
43e59796f6 | ||
|
|
bb3624b799 | ||
|
|
64fca66ef5 | ||
|
|
e9d7486758 | ||
|
|
2bb98c1431 | ||
|
|
95d5c24bfc | ||
|
|
b7ee1a2176 | ||
|
|
0eca61a977 | ||
|
|
9bfd04da2d | ||
|
|
302185a7fc | ||
|
|
6ed6e7e12f | ||
|
|
f41ed78378 | ||
|
|
50c85afc35 | ||
|
|
5876dff326 | ||
|
|
f25185631d | ||
|
|
283f1bad18 | ||
|
|
e1d40c7d89 | ||
|
|
19c6eb426a | ||
|
|
f87b06095d | ||
|
|
b81d54e789 | ||
|
|
00f53b16e8 | ||
|
|
e4cf55b112 | ||
|
|
5bb211d933 | ||
|
|
6adc431a19 | ||
|
|
23d2beed41 | ||
|
|
a80ecb7678 | ||
|
|
361c695264 | ||
|
|
f93243cc1a | ||
|
|
1e533a52e7 | ||
|
|
3ea84f095f | ||
|
|
4239c899a4 | ||
|
|
1597f8859f | ||
|
|
b3dda384c9 | ||
|
|
6828670bfe | ||
|
|
d2f16d92d6 | ||
|
|
d233acb483 | ||
|
|
8920e2a2a2 | ||
|
|
bfee507005 | ||
|
|
929c89789f | ||
|
|
d03c4ae8e8 | ||
|
|
021bafd260 | ||
|
|
04c393ab07 | ||
|
|
9a7778e52c | ||
|
|
dde00253f9 | ||
|
|
18d1644980 | ||
|
|
c424d47274 | ||
|
|
8e6b9d503d | ||
|
|
8be38d1795 | ||
|
|
3146d5c3f2 | ||
|
|
0cc8b68a97 | ||
|
|
5a50c12953 | ||
|
|
a6fa4c5c38 | ||
|
|
dadd9b4dd2 | ||
|
|
6434b8d2bb | ||
|
|
2d891ddd8f | ||
|
|
60b508b151 | ||
|
|
3891f14a1a | ||
|
|
198f0aa366 | ||
|
|
f2e6adf566 |
2
.github/probots.yml
vendored
Normal file
2
.github/probots.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
enabled:
|
||||
- cla
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -4,6 +4,8 @@
|
||||
pkg
|
||||
*.rbc
|
||||
.rvmrc
|
||||
.idea
|
||||
.ruby-version
|
||||
Gemfile.lock
|
||||
.bundle
|
||||
.byebug_history
|
||||
|
||||
1027
.rubocop.shopify.yml
Normal file
1027
.rubocop.shopify.yml
Normal file
File diff suppressed because it is too large
Load Diff
129
.rubocop.yml
129
.rubocop.yml
@@ -1,125 +1,10 @@
|
||||
inherit_from: ./.rubocop_todo.yml
|
||||
inherit_from:
|
||||
- .rubocop.shopify.yml
|
||||
|
||||
AllCops:
|
||||
Exclude:
|
||||
- 'performance/shopify/*'
|
||||
- 'pkg/**'
|
||||
require: rubocop-performance
|
||||
|
||||
Metrics/BlockNesting:
|
||||
Max: 3
|
||||
Exclude:
|
||||
- 'lib/liquid/block_body.rb'
|
||||
Performance:
|
||||
Enabled: true
|
||||
|
||||
Metrics/ModuleLength:
|
||||
Enabled: false
|
||||
|
||||
Metrics/ClassLength:
|
||||
Enabled: false
|
||||
|
||||
Lint/AssignmentInCondition:
|
||||
Enabled: false
|
||||
|
||||
Lint/AmbiguousOperator:
|
||||
Enabled: false
|
||||
|
||||
Lint/AmbiguousRegexpLiteral:
|
||||
Enabled: false
|
||||
|
||||
Lint/ParenthesesAsGroupedExpression:
|
||||
Enabled: false
|
||||
|
||||
Lint/UnusedBlockArgument:
|
||||
Enabled: false
|
||||
|
||||
Lint/EndAlignment:
|
||||
AlignWith: variable
|
||||
|
||||
Lint/UnusedMethodArgument:
|
||||
Enabled: false
|
||||
|
||||
Style/SingleLineBlockParams:
|
||||
Enabled: false
|
||||
|
||||
Style/DoubleNegation:
|
||||
Enabled: false
|
||||
|
||||
Style/StringLiteralsInInterpolation:
|
||||
Enabled: false
|
||||
|
||||
Style/AndOr:
|
||||
Enabled: false
|
||||
|
||||
Style/SignalException:
|
||||
Enabled: false
|
||||
|
||||
Style/StringLiterals:
|
||||
Enabled: false
|
||||
|
||||
Style/BracesAroundHashParameters:
|
||||
Enabled: false
|
||||
|
||||
Style/NumericLiterals:
|
||||
Enabled: false
|
||||
|
||||
Style/SpaceInsideBrackets:
|
||||
Enabled: false
|
||||
|
||||
Style/SpaceBeforeBlockBraces:
|
||||
Enabled: false
|
||||
|
||||
Style/Documentation:
|
||||
Enabled: false
|
||||
|
||||
Style/ClassAndModuleChildren:
|
||||
Enabled: false
|
||||
|
||||
Style/TrailingComma:
|
||||
Enabled: false
|
||||
|
||||
Style/IndentHash:
|
||||
EnforcedStyle: consistent
|
||||
|
||||
Style/FormatString:
|
||||
Enabled: false
|
||||
|
||||
Style/AlignParameters:
|
||||
EnforcedStyle: with_fixed_indentation
|
||||
|
||||
Style/MultilineOperationIndentation:
|
||||
EnforcedStyle: indented
|
||||
|
||||
Style/IfUnlessModifier:
|
||||
Enabled: false
|
||||
|
||||
Style/RaiseArgs:
|
||||
Enabled: false
|
||||
|
||||
Style/DeprecatedHashMethods:
|
||||
Enabled: false
|
||||
|
||||
Style/RegexpLiteral:
|
||||
Enabled: false
|
||||
|
||||
Style/SymbolLiteral:
|
||||
Enabled: false
|
||||
|
||||
Performance/Count:
|
||||
Enabled: false
|
||||
|
||||
Style/ConstantName:
|
||||
Enabled: false
|
||||
|
||||
Style/CaseIndentation:
|
||||
Enabled: false
|
||||
|
||||
Style/ClassVars:
|
||||
Enabled: false
|
||||
|
||||
Style/PerlBackrefs:
|
||||
Enabled: false
|
||||
|
||||
Style/TrivialAccessors:
|
||||
AllowPredicates: true
|
||||
|
||||
Style/WordArray:
|
||||
Enabled: false
|
||||
Metrics/LineLength:
|
||||
Enabled: false
|
||||
@@ -1,72 +0,0 @@
|
||||
# This configuration was generated by `rubocop --auto-gen-config`
|
||||
# on 2015-06-08 18:16:16 +0000 using RuboCop version 0.32.0.
|
||||
# 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: 5
|
||||
Lint/NestedMethodDefinition:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 53
|
||||
Metrics/AbcSize:
|
||||
Max: 58
|
||||
|
||||
# Offense count: 12
|
||||
Metrics/CyclomaticComplexity:
|
||||
Max: 15
|
||||
|
||||
# Offense count: 553
|
||||
# Configuration parameters: AllowURI, URISchemes.
|
||||
Metrics/LineLength:
|
||||
Max: 294
|
||||
|
||||
# Offense count: 77
|
||||
# Configuration parameters: CountComments.
|
||||
Metrics/MethodLength:
|
||||
Max: 46
|
||||
|
||||
# Offense count: 6
|
||||
Metrics/PerceivedComplexity:
|
||||
Max: 13
|
||||
|
||||
# Offense count: 1
|
||||
Style/AccessorMethodName:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 1
|
||||
# Cop supports --auto-correct.
|
||||
Style/ClosingParenthesisIndentation:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 3
|
||||
# Configuration parameters: MinBodyLength.
|
||||
Style/GuardClause:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 4
|
||||
# Configuration parameters: EnforcedStyle, SupportedStyles.
|
||||
Style/MethodName:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 1
|
||||
Style/MultilineBlockChain:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 2
|
||||
# Configuration parameters: EnforcedStyle, MinBodyLength, SupportedStyles.
|
||||
Style/Next:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 7
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: AllowAsExpressionSeparator.
|
||||
Style/Semicolon:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 1
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: MaxLineLength.
|
||||
Style/WhileUntilModifier:
|
||||
Enabled: false
|
||||
15
.travis.yml
15
.travis.yml
@@ -1,12 +1,13 @@
|
||||
language: ruby
|
||||
|
||||
rvm:
|
||||
- 2.0
|
||||
- 2.1
|
||||
- 2.2
|
||||
- 2.4
|
||||
- 2.5
|
||||
- 2.6
|
||||
- 2.7
|
||||
- ruby-head
|
||||
- jruby-head
|
||||
# - rbx-2
|
||||
- truffleruby
|
||||
|
||||
sudo: false
|
||||
|
||||
@@ -17,9 +18,13 @@ addons:
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- rvm: ruby-head
|
||||
- rvm: jruby-head
|
||||
|
||||
script: "bundle exec rake"
|
||||
install:
|
||||
- bundle install
|
||||
|
||||
script: bundle exec rake
|
||||
|
||||
notifications:
|
||||
disable: true
|
||||
|
||||
@@ -4,23 +4,22 @@
|
||||
|
||||
* Bugfixes
|
||||
* Performance improvements
|
||||
* Features which are likely to be useful to the majority of Liquid users
|
||||
* Features that are likely to be useful to the majority of Liquid users
|
||||
|
||||
## Things we won't merge
|
||||
|
||||
* Code which introduces considerable performance degrations
|
||||
* Code which touches performance critical parts of Liquid and comes without benchmarks
|
||||
* Features which are not important for most people (we want to keep the core Liquid code small and tidy)
|
||||
* Features which can easily be implemented on top of Liquid (for example as a custom filter or custom filesystem)
|
||||
* Code which comes without tests
|
||||
* Code which breaks existing tests
|
||||
* Code that introduces considerable performance degrations
|
||||
* Code that touches performance-critical parts of Liquid and comes without benchmarks
|
||||
* Features that are not important for most people (we want to keep the core Liquid code small and tidy)
|
||||
* 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
|
||||
|
||||
## Workflow
|
||||
|
||||
* Fork the Liquid repository
|
||||
* Create a new branch in your fork
|
||||
* If it makes sense, add tests for your code and run a performance benchmark
|
||||
* Make sure all tests pass
|
||||
* 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
|
||||
* In the description, ping one of [@boourns](https://github.com/boourns), [@fw42](https://github.com/fw42), [@camilo](https://github.com/camilo), [@dylanahsmith](https://github.com/dylanahsmith), or [@arthurnn](https://github.com/arthurnn) and ask for a code review.
|
||||
|
||||
|
||||
24
Gemfile
24
Gemfile
@@ -1,14 +1,26 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
source 'https://rubygems.org'
|
||||
git_source(:github) do |repo_name|
|
||||
"https://github.com/#{repo_name}.git"
|
||||
end
|
||||
|
||||
gemspec
|
||||
gem 'stackprof', platforms: :mri_21
|
||||
|
||||
group :test do
|
||||
gem 'spy', '0.4.1'
|
||||
group :benchmark, :test do
|
||||
gem 'benchmark-ips'
|
||||
gem 'rubocop', '0.34.2'
|
||||
gem 'memory_profiler'
|
||||
|
||||
platform :mri do
|
||||
gem 'liquid-c', github: 'Shopify/liquid-c', ref: '2570693d8d03faa0df9160ec74348a7149436df3'
|
||||
install_if -> { RUBY_PLATFORM !~ /mingw|mswin|java/ && RUBY_ENGINE != 'truffleruby' } do
|
||||
gem 'stackprof'
|
||||
end
|
||||
end
|
||||
|
||||
group :test do
|
||||
gem 'rubocop', '~> 0.74.0', require: false
|
||||
gem 'rubocop-performance', require: false
|
||||
|
||||
platform :mri, :truffleruby do
|
||||
gem 'liquid-c', github: 'Shopify/liquid-c', ref: '7ba926791ef8411984d0f3e41c6353fd716041c6'
|
||||
end
|
||||
end
|
||||
|
||||
67
History.md
67
History.md
@@ -1,8 +1,65 @@
|
||||
# Liquid Change Log
|
||||
|
||||
## 4.0.0 / not yet released / branch "master"
|
||||
## 4.0.3 / 2019-03-12
|
||||
|
||||
### Fixed
|
||||
* Fix break and continue tags inside included templates in loops (#1072) [Justin Li]
|
||||
|
||||
## 4.0.2 / 2019-03-08
|
||||
|
||||
### Changed
|
||||
* Add `where` filter (#1026) [Samuel Doiron]
|
||||
* Add `ParseTreeVisitor` to iterate the Liquid AST (#1025) [Stephen Paul Weber]
|
||||
* Improve `strip_html` performance (#1032) [printercu]
|
||||
|
||||
### Fixed
|
||||
* Add error checking for invalid combinations of inputs to sort, sort_natural, where, uniq, map, compact filters (#1059) [Garland Zhang]
|
||||
* Validate the character encoding in url_decode (#1070) [Clayton Smith]
|
||||
|
||||
## 4.0.1 / 2018-10-09
|
||||
|
||||
### Changed
|
||||
* Add benchmark group in Gemfile (#855) [Jerry Liu]
|
||||
* Allow benchmarks to benchmark render by itself (#851) [Jerry Liu]
|
||||
* Avoid calling `line_number` on String node when rescuing a render error. (#860) [Dylan Thacker-Smith]
|
||||
* Avoid duck typing to detect whether to call render on a node. [Dylan Thacker-Smith]
|
||||
* Clarify spelling of `reversed` on `for` block tag (#843) [Mark Crossfield]
|
||||
* Replace recursion with loop to avoid potential stack overflow from malicious input (#891, #892) [Dylan Thacker-Smith]
|
||||
* Limit block tag nesting to 100 (#894) [Dylan Thacker-Smith]
|
||||
* Replace `assert_equal nil` with `assert_nil` (#895) [Dylan Thacker-Smith]
|
||||
* Remove Spy Gem (#896) [Dylan Thacker-Smith]
|
||||
* Add `collection_name` and `variable_name` reader to `For` block (#909)
|
||||
* Symbols render as strings (#920) [Justin Li]
|
||||
* Remove default value from Hash objects (#932) [Maxime Bedard]
|
||||
* Remove one level of nesting (#944) [Dylan Thacker-Smith]
|
||||
* Update Rubocop version (#952) [Justin Li]
|
||||
* Add `at_least` and `at_most` filters (#954, #958) [Nithin Bekal]
|
||||
* Add a regression test for a liquid-c trim mode bug (#972) [Dylan Thacker-Smith]
|
||||
* Use https rather than git protocol to fetch liquid-c [Dylan Thacker-Smith]
|
||||
* Add tests against Ruby 2.4 (#963) and 2.5 (#981)
|
||||
* Replace RegExp literals with constants (#988) [Ashwin Maroli]
|
||||
* Replace unnecessary `#each_with_index` with `#each` (#992) [Ashwin Maroli]
|
||||
* Improve the unexpected end delimiter message for block tags. (#1003) [Dylan Thacker-Smith]
|
||||
* Refactor and optimize rendering (#1005) [Christopher Aue]
|
||||
* Add installation instruction (#1006) [Ben Gift]
|
||||
* Remove Circle CI (#1010)
|
||||
* Rename deprecated `BigDecimal.new` to `BigDecimal` (#1024) [Koichi ITO]
|
||||
* Rename deprecated Rubocop name (#1027) [Justin Li]
|
||||
|
||||
### Fixed
|
||||
* Handle `join` filter on non String joiners (#857) [Richard Monette]
|
||||
* Fix duplicate inclusion condition logic error of `Liquid::Strainer.add_filter` method (#861)
|
||||
* Fix `escape`, `url_encode`, `url_decode` not handling non-string values (#898) [Thierry Joyal]
|
||||
* Fix raise when variable is defined but nil when using `strict_variables` [Pascal Betz]
|
||||
* Fix `sort` and `sort_natural` to handle arrays with nils (#930) [Eric Chan]
|
||||
|
||||
## 4.0.0 / 2016-12-14 / branch "4-0-stable"
|
||||
|
||||
### Changed
|
||||
* Render an opaque internal error by default for non-Liquid::Error (#835) [Dylan Thacker-Smith]
|
||||
* Ruby 2.0 support dropped (#832) [Dylan Thacker-Smith]
|
||||
* Add to_number Drop method to allow custom drops to work with number filters (#731)
|
||||
* Add strict_variables and strict_filters options to detect undefined references (#691)
|
||||
* Improve loop performance (#681) [Florian Weingarten]
|
||||
* Rename Drop method `before_method` to `liquid_method_missing` (#661) [Thierry Joyal]
|
||||
* Add url_decode filter to invert url_encode (#645) [Larry Archer]
|
||||
@@ -16,9 +73,13 @@
|
||||
* Add concat filter to concatenate arrays (#429) [Diogo Beato]
|
||||
* Ruby 1.9 support dropped (#491) [Justin Li]
|
||||
* Liquid::Template.file_system's read_template_file method is no longer passed the context. (#441) [James Reid-Smith]
|
||||
* Remove support for `liquid_methods`
|
||||
* Remove `liquid_methods` (See https://github.com/Shopify/liquid/pull/568 for replacement)
|
||||
* Liquid::Template.register_filter raises when the module overrides registered public methods as private or protected (#705) [Gaurav Chande]
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fix variable names being detected as an operator when starting with contains (#788) [Michael Angell]
|
||||
* Fix include tag used with strict_variables (#828) [QuickPay]
|
||||
* Fix map filter when value is a Proc (#672) [Guillaume Malette]
|
||||
* Fix truncate filter when value is not a string (#672) [Guillaume Malette]
|
||||
* Fix behaviour of escape filter when input is nil (#665) [Tanel Jakobsoo]
|
||||
@@ -213,7 +274,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::ArgumentSeparator: , 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::ARGUMENT_SEPARATOR: , 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]
|
||||
|
||||
|
||||
33
README.md
33
README.md
@@ -42,6 +42,8 @@ Liquid is a template engine which was written with very specific requirements:
|
||||
|
||||
## How to use Liquid
|
||||
|
||||
Install Liquid by adding `gem 'liquid'` to your gemfile.
|
||||
|
||||
Liquid supports a very simple API based around the Liquid::Template class.
|
||||
For standard use you can just pass it the content of a file and call render with a parameters hash.
|
||||
|
||||
@@ -73,3 +75,34 @@ This is useful for doing things like enabling strict mode only in the theme edit
|
||||
|
||||
It is recommended that you enable `:strict` or `:warn` mode on new apps to stop invalid templates from being created.
|
||||
It is also recommended that you use it in the template editors of existing apps to give editors better error messages.
|
||||
|
||||
### Undefined variables and filters
|
||||
|
||||
By default, the renderer doesn't raise or in any other way notify you if some variables or filters are missing, i.e. not passed to the `render` method.
|
||||
You can improve this situation by passing `strict_variables: true` and/or `strict_filters: true` options to the `render` method.
|
||||
When one of these options is set to true, all errors about undefined variables and undefined filters will be stored in `errors` array of a `Liquid::Template` instance.
|
||||
Here are some examples:
|
||||
|
||||
```ruby
|
||||
template = Liquid::Template.parse("{{x}} {{y}} {{z.a}} {{z.b}}")
|
||||
template.render({ 'x' => 1, 'z' => { 'a' => 2 } }, { strict_variables: true })
|
||||
#=> '1 2 ' # when a variable is undefined, it's rendered as nil
|
||||
template.errors
|
||||
#=> [#<Liquid::UndefinedVariable: Liquid error: undefined variable y>, #<Liquid::UndefinedVariable: Liquid error: undefined variable b>]
|
||||
```
|
||||
|
||||
```ruby
|
||||
template = Liquid::Template.parse("{{x | filter1 | upcase}}")
|
||||
template.render({ 'x' => 'foo' }, { strict_filters: true })
|
||||
#=> '' # when at least one filter in the filter chain is undefined, a whole expression is rendered as nil
|
||||
template.errors
|
||||
#=> [#<Liquid::UndefinedFilter: Liquid error: undefined filter filter1>]
|
||||
```
|
||||
|
||||
If you want to raise on a first exception instead of pushing all of them in `errors`, you can use `render!` method:
|
||||
|
||||
```ruby
|
||||
template = Liquid::Template.parse("{{x}} {{y}}")
|
||||
template.render!({ 'x' => 1}, { strict_variables: true })
|
||||
#=> Liquid::UndefinedVariable: Liquid error: undefined variable y
|
||||
```
|
||||
|
||||
58
Rakefile
58
Rakefile
@@ -1,29 +1,33 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rake'
|
||||
require 'rake/testtask'
|
||||
$LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
|
||||
require "liquid/version"
|
||||
$LOAD_PATH.unshift(File.expand_path('lib', __dir__))
|
||||
require 'liquid/version'
|
||||
|
||||
task default: [:rubocop, :test]
|
||||
task(default: %i[test rubocop])
|
||||
|
||||
desc 'run test suite with default parser'
|
||||
desc('run test suite with default parser')
|
||||
Rake::TestTask.new(:base_test) do |t|
|
||||
t.libs << '.' << 'lib' << 'test'
|
||||
t.test_files = FileList['test/{integration,unit}/**/*_test.rb']
|
||||
t.verbose = false
|
||||
end
|
||||
|
||||
desc 'run test suite with warn error mode'
|
||||
desc('run test suite with warn error mode')
|
||||
task :warn_test do
|
||||
ENV['LIQUID_PARSER_MODE'] = 'warn'
|
||||
Rake::Task['base_test'].invoke
|
||||
end
|
||||
|
||||
task :rubocop do
|
||||
require 'rubocop/rake_task'
|
||||
RuboCop::RakeTask.new
|
||||
if RUBY_ENGINE == 'ruby'
|
||||
require 'rubocop/rake_task'
|
||||
RuboCop::RakeTask.new
|
||||
end
|
||||
end
|
||||
|
||||
desc 'runs test suite with both strict and lax parsers'
|
||||
desc('runs test suite with both strict and lax parsers')
|
||||
task :test do
|
||||
ENV['LIQUID_PARSER_MODE'] = 'lax'
|
||||
Rake::Task['base_test'].invoke
|
||||
@@ -32,8 +36,9 @@ task :test do
|
||||
Rake::Task['base_test'].reenable
|
||||
Rake::Task['base_test'].invoke
|
||||
|
||||
if RUBY_ENGINE == 'ruby'
|
||||
ENV['LIQUID-C'] = '1'
|
||||
if RUBY_ENGINE == 'ruby' || RUBY_ENGINE == 'truffleruby'
|
||||
|
||||
ENV['LIQUID_C'] = '1'
|
||||
|
||||
ENV['LIQUID_PARSER_MODE'] = 'lax'
|
||||
Rake::Task['base_test'].reenable
|
||||
@@ -45,9 +50,9 @@ task :test do
|
||||
end
|
||||
end
|
||||
|
||||
task gem: :build
|
||||
task(gem: :build)
|
||||
task :build do
|
||||
system "gem build liquid.gemspec"
|
||||
system 'gem build liquid.gemspec'
|
||||
end
|
||||
|
||||
task install: :build do
|
||||
@@ -56,38 +61,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
|
||||
|
||||
desc "Run example"
|
||||
namespace :memory_profile do
|
||||
desc 'Run memory profiler'
|
||||
task :run do
|
||||
ruby './performance/memory_profile.rb'
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
machine:
|
||||
ruby:
|
||||
version: ruby-2.0
|
||||
@@ -1,6 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ProductsFilter
|
||||
def price(integer)
|
||||
sprintf("$%.2d USD", integer / 100.0)
|
||||
format('$%.2d USD', integer / 100.0)
|
||||
end
|
||||
|
||||
def prettyprint(text)
|
||||
@@ -28,17 +30,17 @@ class Servlet < LiquidServlet
|
||||
private
|
||||
|
||||
def products_list
|
||||
[{ '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' }]
|
||||
[{ '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' }]
|
||||
end
|
||||
|
||||
def more_products_list
|
||||
[{ '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' }]
|
||||
[{ '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' }]
|
||||
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,23 +1,28 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet
|
||||
def do_GET(req, res)
|
||||
def get(req, res)
|
||||
handle(:get, req, res)
|
||||
end
|
||||
|
||||
def do_POST(req, res)
|
||||
def post(req, res)
|
||||
handle(:post, req, res)
|
||||
end
|
||||
|
||||
alias_method :do_GET, :get
|
||||
alias_method :do_POST, :post
|
||||
|
||||
private
|
||||
|
||||
def handle(type, req, res)
|
||||
def handle(_type, req, res)
|
||||
@request = req
|
||||
@response = res
|
||||
|
||||
@request.path_info =~ /(\w+)\z/
|
||||
@action = $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])
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'webrick'
|
||||
require 'rexml/document'
|
||||
|
||||
@@ -8,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
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Copyright (c) 2005 Tobias Luetke
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
@@ -20,30 +22,32 @@
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
module Liquid
|
||||
FilterSeparator = /\|/
|
||||
ArgumentSeparator = ','.freeze
|
||||
FilterArgumentSeparator = ':'.freeze
|
||||
VariableAttributeSeparator = '.'.freeze
|
||||
TagStart = /\{\%/
|
||||
TagEnd = /\%\}/
|
||||
VariableSignature = /\(?[\w\-\.\[\]]\)?/
|
||||
VariableSegment = /[\w\-]/
|
||||
VariableStart = /\{\{/
|
||||
VariableEnd = /\}\}/
|
||||
VariableIncompleteEnd = /\}\}?/
|
||||
QuotedString = /"[^"]*"|'[^']*'/
|
||||
QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o
|
||||
TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/o
|
||||
AnyStartingTag = /\{\{|\{\%/
|
||||
PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om
|
||||
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om
|
||||
VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o
|
||||
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
|
||||
|
||||
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'
|
||||
require 'liquid/i18n'
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Block < Tag
|
||||
MAX_DEPTH = 100
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
@blank = true
|
||||
@@ -11,6 +15,7 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
# For backwards compatibility
|
||||
def render(context)
|
||||
@body.render(context)
|
||||
end
|
||||
@@ -24,16 +29,16 @@ module Liquid
|
||||
end
|
||||
|
||||
def unknown_tag(tag, _params, _tokens)
|
||||
case tag
|
||||
when 'else'.freeze
|
||||
raise SyntaxError.new(parse_context.locale.t("errors.syntax.unexpected_else".freeze,
|
||||
block_name: block_name))
|
||||
when 'end'.freeze
|
||||
raise SyntaxError.new(parse_context.locale.t("errors.syntax.invalid_delimiter".freeze,
|
||||
block_name: block_name,
|
||||
block_delimiter: block_delimiter))
|
||||
if tag == 'else'
|
||||
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)
|
||||
else
|
||||
raise SyntaxError.new(parse_context.locale.t("errors.syntax.unknown_tag".freeze, tag: tag))
|
||||
raise SyntaxError, parse_context.locale.t('errors.syntax.unknown_tag', tag: tag)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -48,17 +53,22 @@ module Liquid
|
||||
protected
|
||||
|
||||
def parse_body(body, tokens)
|
||||
body.parse(tokens, parse_context) do |end_tag_name, end_tag_params|
|
||||
@blank &&= body.blank?
|
||||
raise StackLevelError, 'Nesting too deep' if parse_context.depth >= MAX_DEPTH
|
||||
|
||||
return false if end_tag_name == block_delimiter
|
||||
unless end_tag_name
|
||||
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name))
|
||||
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
|
||||
|
||||
# this tag is not registered with the system
|
||||
# pass it to the current block for special handling or error reporting
|
||||
unknown_tag(end_tag_name, end_tag_params, tokens)
|
||||
end
|
||||
|
||||
# this tag is not registered with the system
|
||||
# pass it to the current block for special handling or error reporting
|
||||
unknown_tag(end_tag_name, end_tag_params, tokens)
|
||||
ensure
|
||||
parse_context.depth -= 1
|
||||
end
|
||||
|
||||
true
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class BlockBody
|
||||
FullToken = /\A#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
|
||||
ContentOfVariable = /\A#{VariableStart}(.*)#{VariableEnd}\z/om
|
||||
TAGSTART = "{%".freeze
|
||||
VARSTART = "{{".freeze
|
||||
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 = '{{'
|
||||
|
||||
attr_reader :nodelist
|
||||
|
||||
@@ -14,33 +17,34 @@ module Liquid
|
||||
|
||||
def parse(tokenizer, parse_context)
|
||||
parse_context.line_number = tokenizer.line_number
|
||||
while token = tokenizer.shift
|
||||
unless token.empty?
|
||||
case
|
||||
when token.start_with?(TAGSTART)
|
||||
if token =~ FullToken
|
||||
tag_name = $1
|
||||
markup = $2
|
||||
# fetch the tag from registered blocks
|
||||
if tag = registered_tags[tag_name]
|
||||
new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
|
||||
@blank &&= new_tag.blank?
|
||||
@nodelist << new_tag
|
||||
else
|
||||
# end parsing if we reach an unknown tag and let the caller decide
|
||||
# determine how to proceed
|
||||
return yield tag_name, markup
|
||||
end
|
||||
else
|
||||
raise_missing_tag_terminator(token, parse_context)
|
||||
end
|
||||
when token.start_with?(VARSTART)
|
||||
@nodelist << create_variable(token, parse_context)
|
||||
@blank = false
|
||||
else
|
||||
@nodelist << token
|
||||
@blank &&= !!(token =~ /\A\s*\z/)
|
||||
while (token = tokenizer.shift)
|
||||
next if token.empty?
|
||||
|
||||
case
|
||||
when token.start_with?(TAG_START)
|
||||
whitespace_handler(token, parse_context)
|
||||
raise_missing_tag_terminator(token, parse_context) unless token =~ FULL_TOKEN
|
||||
tag_name = Regexp.last_match(1)
|
||||
markup = Regexp.last_match(2)
|
||||
# fetch the tag from registered blocks
|
||||
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)
|
||||
whitespace_handler(token, parse_context)
|
||||
@nodelist << create_variable(token, parse_context)
|
||||
@blank = false
|
||||
else
|
||||
token.lstrip! if parse_context.trim_whitespace
|
||||
parse_context.trim_whitespace = false
|
||||
@nodelist << token
|
||||
@blank &&= token.match?(WHITESPACE_OR_NOTHING)
|
||||
end
|
||||
parse_context.line_number = tokenizer.line_number
|
||||
end
|
||||
@@ -48,57 +52,75 @@ module Liquid
|
||||
yield nil, nil
|
||||
end
|
||||
|
||||
def whitespace_handler(token, parse_context)
|
||||
if token[2] == WHITESPACE_CONTROL
|
||||
previous_token = @nodelist.last
|
||||
previous_token.rstrip! if previous_token.is_a?(String)
|
||||
end
|
||||
parse_context.trim_whitespace = (token[-3] == WHITESPACE_CONTROL)
|
||||
end
|
||||
|
||||
def blank?
|
||||
@blank
|
||||
end
|
||||
|
||||
def render(context)
|
||||
output = []
|
||||
render_to_output_buffer(context, +'')
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
context.resource_limits.render_score += @nodelist.length
|
||||
|
||||
@nodelist.each do |token|
|
||||
# Break out if we have any unhanded interrupts.
|
||||
break if context.interrupt?
|
||||
idx = 0
|
||||
while (node = @nodelist[idx])
|
||||
previous_output_size = output.bytesize
|
||||
|
||||
begin
|
||||
case node
|
||||
when String
|
||||
output << node
|
||||
when Variable
|
||||
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 %}
|
||||
if token.is_a?(Continue) || token.is_a?(Break)
|
||||
context.push_interrupt(token.interrupt)
|
||||
break
|
||||
end
|
||||
|
||||
node_output = render_node(token, context)
|
||||
|
||||
unless token.is_a?(Block) && token.blank?
|
||||
output << node_output
|
||||
end
|
||||
rescue MemoryError => e
|
||||
raise e
|
||||
rescue ::StandardError => e
|
||||
output << context.handle_error(e, token.line_number)
|
||||
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
|
||||
end
|
||||
idx += 1
|
||||
|
||||
raise_if_resource_limits_reached(context, output.bytesize - previous_output_size)
|
||||
end
|
||||
|
||||
output.join
|
||||
output
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def render_node(node, context)
|
||||
node_output = (node.respond_to?(:render) ? node.render(context) : node)
|
||||
node_output = node_output.is_a?(Array) ? node_output.join : node_output.to_s
|
||||
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
|
||||
|
||||
context.resource_limits.render_length += node_output.length
|
||||
if context.resource_limits.reached?
|
||||
raise MemoryError.new("Memory limits exceeded".freeze)
|
||||
end
|
||||
node_output
|
||||
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'
|
||||
end
|
||||
|
||||
def create_variable(token, parse_context)
|
||||
token.scan(ContentOfVariable) do |content|
|
||||
token.scan(CONTENT_OF_VARIABLE) do |content|
|
||||
markup = content.first
|
||||
return Variable.new(markup, parse_context)
|
||||
end
|
||||
@@ -106,11 +128,11 @@ module Liquid
|
||||
end
|
||||
|
||||
def raise_missing_tag_terminator(token, parse_context)
|
||||
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_termination".freeze, token: token, tag_end: TagEnd.inspect))
|
||||
raise SyntaxError, parse_context.locale.t('errors.syntax.tag_termination', token: token, tag_end: TAG_END.inspect)
|
||||
end
|
||||
|
||||
def raise_missing_variable_terminator(token, parse_context)
|
||||
raise SyntaxError.new(parse_context.locale.t("errors.syntax.variable_termination".freeze, token: token, tag_end: VariableEnd.inspect))
|
||||
raise SyntaxError, parse_context.locale.t('errors.syntax.variable_termination', token: token, tag_end: VARIABLE_END.inspect)
|
||||
end
|
||||
|
||||
def registered_tags
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# Container for liquid nodes which conveniently wraps decision making logic
|
||||
#
|
||||
@@ -7,50 +9,56 @@ module Liquid
|
||||
# c.evaluate #=> true
|
||||
#
|
||||
class Condition #:nodoc:
|
||||
@@operators = {
|
||||
'=='.freeze => ->(cond, left, right) { cond.send(:equal_variables, left, right) },
|
||||
'!='.freeze => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
|
||||
'<>'.freeze => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
|
||||
'<'.freeze => :<,
|
||||
'>'.freeze => :>,
|
||||
'>='.freeze => :>=,
|
||||
'<='.freeze => :<=,
|
||||
'contains'.freeze => lambda do |cond, left, right|
|
||||
@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) },
|
||||
'<' => :<,
|
||||
'>' => :>,
|
||||
'>=' => :>=,
|
||||
'<=' => :<=,
|
||||
'contains' => lambda do |_cond, left, right|
|
||||
if left && right && left.respond_to?(:include?)
|
||||
right = right.to_s if left.is_a?(String)
|
||||
left.include?(right)
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
def self.operators
|
||||
@@operators
|
||||
class << self
|
||||
attr_accessor :operators
|
||||
end
|
||||
|
||||
attr_reader :attachment
|
||||
attr_reader :attachment, :child_condition
|
||||
attr_accessor :left, :operator, :right
|
||||
|
||||
def initialize(left = nil, operator = nil, right = nil)
|
||||
@left = left
|
||||
@operator = operator
|
||||
@right = right
|
||||
@child_relation = nil
|
||||
@child_relation = nil
|
||||
@child_condition = nil
|
||||
end
|
||||
|
||||
def evaluate(context = Context.new)
|
||||
result = interpret_condition(left, right, operator, context)
|
||||
condition = self
|
||||
result = nil
|
||||
loop do
|
||||
result = interpret_condition(condition.left, condition.right, condition.operator, context)
|
||||
|
||||
case @child_relation
|
||||
when :or
|
||||
result || @child_condition.evaluate(context)
|
||||
when :and
|
||||
result && @child_condition.evaluate(context)
|
||||
else
|
||||
result
|
||||
case condition.child_relation
|
||||
when :or
|
||||
break if result
|
||||
when :and
|
||||
break unless result
|
||||
else
|
||||
break
|
||||
end
|
||||
condition = condition.child_condition
|
||||
end
|
||||
result
|
||||
end
|
||||
|
||||
def or(condition)
|
||||
@@ -72,9 +80,13 @@ module Liquid
|
||||
end
|
||||
|
||||
def inspect
|
||||
"#<Condition #{[@left, @operator, @right].compact.join(' '.freeze)}>"
|
||||
"#<Condition #{[@left, @operator, @right].compact.join(' ')}>"
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
attr_reader :child_relation
|
||||
|
||||
private
|
||||
|
||||
def equal_variables(left, right)
|
||||
@@ -106,18 +118,27 @@ module Liquid
|
||||
left = context.evaluate(left)
|
||||
right = context.evaluate(right)
|
||||
|
||||
operation = self.class.operators[op] || raise(Liquid::ArgumentError.new("Unknown operator #{op}"))
|
||||
operation = self.class.operators[op] || raise(Liquid::ArgumentError, "Unknown operator #{op}")
|
||||
|
||||
if operation.respond_to?(:call)
|
||||
operation.call(self, left, right)
|
||||
elsif left.respond_to?(operation) && right.respond_to?(operation)
|
||||
elsif left.respond_to?(operation) && right.respond_to?(operation) && !left.is_a?(Hash) && !right.is_a?(Hash)
|
||||
begin
|
||||
left.send(operation, right)
|
||||
rescue ::ArgumentError => e
|
||||
raise Liquid::ArgumentError.new(e.message)
|
||||
raise Liquid::ArgumentError, e.message
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
[
|
||||
@node.left, @node.right,
|
||||
@node.child_condition, @node.attachment
|
||||
].compact
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class ElseCondition < Condition
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# Context keeps the variable stack and resolves variables, as well as keywords
|
||||
#
|
||||
@@ -13,7 +15,7 @@ module Liquid
|
||||
# context['bob'] #=> nil class Context
|
||||
class Context
|
||||
attr_reader :scopes, :errors, :registers, :environments, :resource_limits
|
||||
attr_accessor :exception_handler, :template_name, :partial, :global_filter
|
||||
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
|
||||
@@ -21,14 +23,14 @@ module Liquid
|
||||
@registers = registers
|
||||
@errors = []
|
||||
@partial = false
|
||||
@strict_variables = false
|
||||
@resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
|
||||
squash_instance_assigns_with_environments
|
||||
|
||||
@this_stack_used = false
|
||||
|
||||
if rethrow_errors
|
||||
self.exception_handler = ->(e) { raise }
|
||||
end
|
||||
self.exception_renderer = Template.default_exception_renderer
|
||||
self.exception_renderer = ->(_e) { raise } if rethrow_errors
|
||||
|
||||
@interrupts = []
|
||||
@filters = []
|
||||
@@ -73,30 +75,11 @@ module Liquid
|
||||
end
|
||||
|
||||
def handle_error(e, line_number = nil)
|
||||
if e.is_a?(Liquid::Error)
|
||||
e.template_name ||= template_name
|
||||
e.line_number ||= line_number
|
||||
end
|
||||
|
||||
output = nil
|
||||
|
||||
if exception_handler
|
||||
result = exception_handler.call(e)
|
||||
case result
|
||||
when Exception
|
||||
e = result
|
||||
if e.is_a?(Liquid::Error)
|
||||
e.template_name ||= template_name
|
||||
e.line_number ||= line_number
|
||||
end
|
||||
when String
|
||||
output = result
|
||||
else
|
||||
raise if result
|
||||
end
|
||||
end
|
||||
e = internal_error unless e.is_a?(Liquid::Error)
|
||||
e.template_name ||= template_name
|
||||
e.line_number ||= line_number
|
||||
errors.push(e)
|
||||
output || Liquid::Error.render(e)
|
||||
exception_renderer.call(e).to_s
|
||||
end
|
||||
|
||||
def invoke(method, *args)
|
||||
@@ -106,7 +89,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".freeze if @scopes.length > 100
|
||||
raise StackLevelError, 'Nesting too deep' if @scopes.length > Block::MAX_DEPTH
|
||||
end
|
||||
|
||||
# Merge a hash of variables in the current local scope
|
||||
@@ -117,6 +100,7 @@ module Liquid
|
||||
# Pop from the stack. use <tt>Context#stack</tt> instead
|
||||
def pop
|
||||
raise ContextError if @scopes.size == 1
|
||||
|
||||
@scopes.shift
|
||||
end
|
||||
|
||||
@@ -177,7 +161,7 @@ module Liquid
|
||||
end
|
||||
|
||||
# Fetches an object starting at the local scope and then moving up the hierachy
|
||||
def find_variable(key)
|
||||
def find_variable(key, raise_on_not_found: true)
|
||||
# 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) }
|
||||
@@ -187,8 +171,10 @@ module Liquid
|
||||
|
||||
if scope.nil?
|
||||
@environments.each do |e|
|
||||
variable = lookup_and_evaluate(e, key)
|
||||
unless variable.nil?
|
||||
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
|
||||
@@ -196,7 +182,7 @@ module Liquid
|
||||
end
|
||||
|
||||
scope ||= @environments.last || @scopes.last
|
||||
variable ||= lookup_and_evaluate(scope, key)
|
||||
variable ||= lookup_and_evaluate(scope, key, raise_on_not_found: raise_on_not_found)
|
||||
|
||||
variable = variable.to_liquid
|
||||
variable.context = self if variable.respond_to?(:context=)
|
||||
@@ -204,9 +190,13 @@ module Liquid
|
||||
variable
|
||||
end
|
||||
|
||||
def lookup_and_evaluate(obj, key)
|
||||
if (value = obj[key]).is_a?(Proc) && obj.respond_to?(:[]=)
|
||||
obj[key] = (value.arity == 0) ? value.call : value.call(self)
|
||||
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)
|
||||
|
||||
value = obj[key]
|
||||
|
||||
if value.is_a?(Proc) && obj.respond_to?(:[]=)
|
||||
obj[key] = value.arity == 0 ? value.call : value.call(self)
|
||||
else
|
||||
value
|
||||
end
|
||||
@@ -214,6 +204,13 @@ module Liquid
|
||||
|
||||
private
|
||||
|
||||
def internal_error
|
||||
# raise and catch to set backtrace and cause on exception
|
||||
raise Liquid::InternalError, 'internal'
|
||||
rescue Liquid::InternalError => e
|
||||
e
|
||||
end
|
||||
|
||||
def squash_instance_assigns_with_environments
|
||||
@scopes.last.each_key do |k|
|
||||
@environments.each do |env|
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Document < BlockBody
|
||||
def self.parse(tokens, parse_context)
|
||||
@@ -7,7 +9,7 @@ module Liquid
|
||||
end
|
||||
|
||||
def parse(tokens, parse_context)
|
||||
super do |end_tag_name, end_tag_params|
|
||||
super do |end_tag_name, _end_tag_params|
|
||||
unknown_tag(end_tag_name, parse_context) if end_tag_name
|
||||
end
|
||||
rescue SyntaxError => e
|
||||
@@ -17,10 +19,10 @@ module Liquid
|
||||
|
||||
def unknown_tag(tag, parse_context)
|
||||
case tag
|
||||
when 'else'.freeze, 'end'.freeze
|
||||
raise SyntaxError.new(parse_context.locale.t("errors.syntax.unexpected_outer_tag".freeze, tag: tag))
|
||||
when 'else', 'end'
|
||||
raise SyntaxError, parse_context.locale.t('errors.syntax.unexpected_outer_tag', tag: tag)
|
||||
else
|
||||
raise SyntaxError.new(parse_context.locale.t("errors.syntax.unknown_tag".freeze, tag: tag))
|
||||
raise SyntaxError, parse_context.locale.t('errors.syntax.unknown_tag', tag: tag)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'set'
|
||||
|
||||
module Liquid
|
||||
@@ -24,8 +26,10 @@ module Liquid
|
||||
attr_writer :context
|
||||
|
||||
# Catch all for the method
|
||||
def liquid_method_missing(_method)
|
||||
nil
|
||||
def liquid_method_missing(method)
|
||||
return nil unless @context&.strict_variables
|
||||
|
||||
raise Liquid::UndefinedDropMethod, "undefined method #{method}"
|
||||
end
|
||||
|
||||
# called by liquid to invoke a drop
|
||||
@@ -61,16 +65,17 @@ module Liquid
|
||||
end
|
||||
|
||||
def self.invokable_methods
|
||||
unless @invokable_methods
|
||||
@invokable_methods ||= begin
|
||||
blacklist = Liquid::Drop.public_instance_methods + [:each]
|
||||
|
||||
if include?(Enumerable)
|
||||
blacklist += Enumerable.public_instance_methods
|
||||
blacklist -= [:sort, :count, :first, :min, :max, :include?]
|
||||
blacklist -= %i[sort count first min max include?]
|
||||
end
|
||||
|
||||
whitelist = [:to_liquid] + (public_instance_methods - blacklist)
|
||||
@invokable_methods = Set.new(whitelist.map(&:to_s))
|
||||
Set.new(whitelist.map(&:to_s))
|
||||
end
|
||||
@invokable_methods
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Error < ::StandardError
|
||||
attr_accessor :line_number
|
||||
@@ -5,43 +7,35 @@ 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
|
||||
|
||||
str
|
||||
end
|
||||
|
||||
def self.render(e)
|
||||
if e.is_a?(Liquid::Error)
|
||||
e.to_s
|
||||
else
|
||||
"Liquid error: #{e}"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def message_prefix
|
||||
str = ""
|
||||
if is_a?(SyntaxError)
|
||||
str << "Liquid syntax error"
|
||||
str = +''
|
||||
str << if is_a?(SyntaxError)
|
||||
'Liquid syntax error'
|
||||
else
|
||||
str << "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
|
||||
@@ -56,4 +50,9 @@ module Liquid
|
||||
MemoryError = Class.new(Error)
|
||||
ZeroDivisionError = Class.new(Error)
|
||||
FloatDomainError = Class.new(Error)
|
||||
UndefinedVariable = Class.new(Error)
|
||||
UndefinedDropMethod = Class.new(Error)
|
||||
UndefinedFilter = Class.new(Error)
|
||||
MethodOverrideError = Class.new(Error)
|
||||
InternalError = Class.new(Error)
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Expression
|
||||
class MethodLiteral
|
||||
@@ -14,28 +16,32 @@ module Liquid
|
||||
end
|
||||
|
||||
LITERALS = {
|
||||
nil => nil, 'nil'.freeze => nil, 'null'.freeze => nil, ''.freeze => nil,
|
||||
'true'.freeze => true,
|
||||
'false'.freeze => false,
|
||||
'blank'.freeze => MethodLiteral.new(:blank?, '').freeze,
|
||||
'empty'.freeze => MethodLiteral.new(:empty?, '').freeze
|
||||
}
|
||||
nil => nil, 'nil' => nil, 'null' => nil, '' => nil,
|
||||
'true' => true,
|
||||
'false' => false,
|
||||
'blank' => MethodLiteral.new(:blank?, '').freeze,
|
||||
'empty' => MethodLiteral.new(:empty?, '').freeze
|
||||
}.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
|
||||
|
||||
def self.parse(markup)
|
||||
if LITERALS.key?(markup)
|
||||
LITERALS[markup]
|
||||
else
|
||||
case markup
|
||||
when /\A'(.*)'\z/m # Single quoted strings
|
||||
$1
|
||||
when /\A"(.*)"\z/m # Double quoted strings
|
||||
$1
|
||||
when /\A(-?\d+)\z/ # Integer and floats
|
||||
$1.to_i
|
||||
when /\A\((\S+)\.\.(\S+)\)\z/ # Ranges
|
||||
RangeLookup.parse($1, $2)
|
||||
when /\A(-?\d[\d\.]+)\z/ # Floats
|
||||
$1.to_f
|
||||
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
|
||||
VariableLookup.parse(markup)
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'time'
|
||||
require 'date'
|
||||
|
||||
@@ -7,6 +9,12 @@ class String # :nodoc:
|
||||
end
|
||||
end
|
||||
|
||||
class Symbol # :nodoc:
|
||||
def to_liquid
|
||||
to_s
|
||||
end
|
||||
end
|
||||
|
||||
class Array # :nodoc:
|
||||
def to_liquid
|
||||
self
|
||||
@@ -25,6 +33,12 @@ class Numeric # :nodoc:
|
||||
end
|
||||
end
|
||||
|
||||
class Range # :nodoc:
|
||||
def to_liquid
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
class Time # :nodoc:
|
||||
def to_liquid
|
||||
self
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# A Liquid file system is a way to let your templates retrieve other templates for use with the include tag.
|
||||
#
|
||||
@@ -8,14 +10,14 @@ module Liquid
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# Liquid::Template.file_system = Liquid::LocalFileSystem.new(template_path)
|
||||
# liquid = Liquid::Template.parse(template)
|
||||
# Liquid::Template.file_system = Liquid::LocalFileSystem.new(template_path)
|
||||
# liquid = Liquid::Template.parse(template)
|
||||
#
|
||||
# This will parse the template with a LocalFileSystem implementation rooted at 'template_path'.
|
||||
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
|
||||
|
||||
@@ -26,10 +28,10 @@ module Liquid
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# file_system = Liquid::LocalFileSystem.new("/some/path")
|
||||
# file_system = Liquid::LocalFileSystem.new("/some/path")
|
||||
#
|
||||
# file_system.full_path("mypartial") # => "/some/path/_mypartial.liquid"
|
||||
# file_system.full_path("dir/mypartial") # => "/some/path/dir/_mypartial.liquid"
|
||||
# file_system.full_path("mypartial") # => "/some/path/_mypartial.liquid"
|
||||
# file_system.full_path("dir/mypartial") # => "/some/path/dir/_mypartial.liquid"
|
||||
#
|
||||
# Optionally in the second argument you can specify a custom pattern for template filenames.
|
||||
# The Kernel::sprintf format specification is used.
|
||||
@@ -37,14 +39,14 @@ module Liquid
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# file_system = Liquid::LocalFileSystem.new("/some/path", "%s.html")
|
||||
# file_system = Liquid::LocalFileSystem.new("/some/path", "%s.html")
|
||||
#
|
||||
# file_system.full_path("index") # => "/some/path/index.html"
|
||||
# file_system.full_path("index") # => "/some/path/index.html"
|
||||
#
|
||||
class LocalFileSystem
|
||||
attr_accessor :root
|
||||
|
||||
def initialize(root, pattern = "_%s.liquid".freeze)
|
||||
def initialize(root, pattern = '_%s.liquid')
|
||||
@root = root
|
||||
@pattern = pattern
|
||||
end
|
||||
@@ -57,15 +59,15 @@ module Liquid
|
||||
end
|
||||
|
||||
def full_path(template_path)
|
||||
raise FileSystemError, "Illegal template name '#{template_path}'" unless template_path =~ /\A[^.\/][a-zA-Z0-9_\/]+\z/
|
||||
raise FileSystemError, "Illegal template name '#{template_path}'" unless template_path =~ %r{\A[^./][a-zA-Z0-9_/]+\z}
|
||||
|
||||
full_path = if template_path.include?('/'.freeze)
|
||||
full_path = if template_path.include?('/')
|
||||
File.join(root, File.dirname(template_path), @pattern % File.basename(template_path))
|
||||
else
|
||||
File.join(root, @pattern % template_path)
|
||||
end
|
||||
|
||||
raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path) =~ /\A#{File.expand_path(root)}/
|
||||
raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path).start_with?(File.expand_path(root))
|
||||
|
||||
full_path
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class ForloopDrop < Drop
|
||||
def initialize(name, length, parentloop)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
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)
|
||||
|
||||
@@ -26,13 +28,13 @@ module Liquid
|
||||
def interpolate(name, vars)
|
||||
name.gsub(/%\{(\w+)\}/) do
|
||||
# raise TranslationError, "Undefined key #{$1} for interpolation in translation #{name}" unless vars[$1.to_sym]
|
||||
"#{vars[$1.to_sym]}"
|
||||
(vars[Regexp.last_match(1).to_sym]).to_s
|
||||
end
|
||||
end
|
||||
|
||||
def deep_fetch_translation(name)
|
||||
name.split('.'.freeze).reduce(locale) do |level, cur|
|
||||
level[cur] or raise TranslationError, "Translation for #{name} does not exist in locale #{path}"
|
||||
name.split('.').reduce(locale) do |level, cur|
|
||||
level[cur] || raise(TranslationError, "Translation for #{name} does not exist in locale #{path}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# An interrupt is any command that breaks processing of a block (ex: a for loop).
|
||||
class Interrupt
|
||||
attr_reader :message
|
||||
|
||||
def initialize(message = nil)
|
||||
@message = message || "interrupt".freeze
|
||||
@message = message || 'interrupt'
|
||||
end
|
||||
end
|
||||
|
||||
# Interrupt that is thrown whenever a {% break %} is called.
|
||||
class BreakInterrupt < Interrupt; end
|
||||
class BreakInterrupt < RuntimeError; end
|
||||
|
||||
# Interrupt that is thrown whenever a {% continue %} is called.
|
||||
class ContinueInterrupt < Interrupt; end
|
||||
class ContinueInterrupt < RuntimeError; end
|
||||
end
|
||||
|
||||
79
lib/liquid/legacy.rb
Normal file
79
lib/liquid/legacy.rb
Normal file
@@ -0,0 +1,79 @@
|
||||
# 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,44 +1,48 @@
|
||||
require "strscan"
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'strscan'
|
||||
module Liquid
|
||||
class Lexer
|
||||
SPECIALS = {
|
||||
'|'.freeze => :pipe,
|
||||
'.'.freeze => :dot,
|
||||
':'.freeze => :colon,
|
||||
','.freeze => :comma,
|
||||
'['.freeze => :open_square,
|
||||
']'.freeze => :close_square,
|
||||
'('.freeze => :open_round,
|
||||
')'.freeze => :close_round,
|
||||
'?'.freeze => :question,
|
||||
'-'.freeze => :dash
|
||||
}
|
||||
IDENTIFIER = /[a-zA-Z_][\w-]*\??/
|
||||
SINGLE_STRING_LITERAL = /'[^\']*'/
|
||||
DOUBLE_STRING_LITERAL = /"[^\"]*"/
|
||||
NUMBER_LITERAL = /-?\d+(\.\d+)?/
|
||||
DOTDOT = /\.\./
|
||||
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains/
|
||||
'|' => :pipe,
|
||||
'.' => :dot,
|
||||
':' => :colon,
|
||||
',' => :comma,
|
||||
'[' => :open_square,
|
||||
']' => :close_square,
|
||||
'(' => :open_round,
|
||||
')' => :close_round,
|
||||
'?' => :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
|
||||
|
||||
def initialize(input)
|
||||
@ss = StringScanner.new(input.rstrip)
|
||||
@ss = StringScanner.new(input)
|
||||
end
|
||||
|
||||
def tokenize
|
||||
@output = []
|
||||
|
||||
until @ss.eos?
|
||||
@ss.skip(/\s*/)
|
||||
tok = case
|
||||
when t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t]
|
||||
when t = @ss.scan(SINGLE_STRING_LITERAL) then [:string, t]
|
||||
when t = @ss.scan(DOUBLE_STRING_LITERAL) then [:string, t]
|
||||
when t = @ss.scan(NUMBER_LITERAL) then [:number, t]
|
||||
when t = @ss.scan(IDENTIFIER) then [:id, t]
|
||||
when t = @ss.scan(DOTDOT) then [:dotdot, t]
|
||||
@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]
|
||||
else
|
||||
c = @ss.getch
|
||||
if s = SPECIALS[c]
|
||||
if (s = SPECIALS[c])
|
||||
[s, c]
|
||||
else
|
||||
raise SyntaxError, "Unexpected character #{c}"
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
if: "Syntax Error in tag 'if' - Valid syntax: if [expression]"
|
||||
include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]"
|
||||
unknown_tag: "Unknown tag '%{tag}'"
|
||||
invalid_delimiter: "'end' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}"
|
||||
invalid_delimiter: "'%{tag}' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}"
|
||||
unexpected_else: "%{block_name} tag does not expect 'else' tag"
|
||||
unexpected_outer_tag: "Unexpected outer '%{tag}' tag"
|
||||
tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"
|
||||
@@ -22,3 +22,5 @@
|
||||
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"
|
||||
argument:
|
||||
include: "Argument error in tag 'include' - Illegal template name"
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class ParseContext
|
||||
attr_accessor :partial, :locale, :line_number
|
||||
attr_reader :warnings, :error_mode
|
||||
attr_accessor :locale, :line_number, :trim_whitespace, :depth
|
||||
attr_reader :partial, :warnings, :error_mode
|
||||
|
||||
def initialize(options = {})
|
||||
@template_options = options ? options.dup : {}
|
||||
@locale = @template_options[:locale] ||= I18n.new
|
||||
@warnings = []
|
||||
self.depth = 0
|
||||
self.partial = false
|
||||
end
|
||||
|
||||
@@ -18,7 +21,6 @@ module Liquid
|
||||
@partial = value
|
||||
@options = value ? partial_options : @template_options
|
||||
@error_mode = @options[:error_mode] || Template.error_mode
|
||||
value
|
||||
end
|
||||
|
||||
def partial_options
|
||||
@@ -27,7 +29,7 @@ module Liquid
|
||||
if dont_pass == true
|
||||
{ locale: locale }
|
||||
elsif dont_pass.is_a?(Array)
|
||||
@template_options.reject { |k, v| dont_pass.include?(k) }
|
||||
@template_options.reject { |k, _v| dont_pass.include?(k) }
|
||||
else
|
||||
@template_options
|
||||
end
|
||||
|
||||
42
lib/liquid/parse_tree_visitor.rb
Normal file
42
lib/liquid/parse_tree_visitor.rb
Normal file
@@ -0,0 +1,42 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class ParseTreeVisitor
|
||||
def self.for(node, callbacks = Hash.new(proc {}))
|
||||
if defined?(node.class::ParseTreeVisitor)
|
||||
node.class::ParseTreeVisitor
|
||||
else
|
||||
self
|
||||
end.new(node, callbacks)
|
||||
end
|
||||
|
||||
def initialize(node, callbacks)
|
||||
@node = node
|
||||
@callbacks = callbacks
|
||||
end
|
||||
|
||||
def add_callback_for(*classes, &block)
|
||||
callback = block
|
||||
callback = ->(node, _) { yield node } if block.arity.abs == 1
|
||||
callback = ->(_, _) { yield } if block.arity.zero?
|
||||
classes.each { |klass| @callbacks[klass] = callback }
|
||||
self
|
||||
end
|
||||
|
||||
def visit(context = nil)
|
||||
children.map do |node|
|
||||
item, new_context = @callbacks[node.class].call(node, context)
|
||||
[
|
||||
item,
|
||||
ParseTreeVisitor.for(node, @callbacks).visit(new_context || context),
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def children
|
||||
@node.respond_to?(:nodelist) ? Array(@node.nodelist) : []
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Parser
|
||||
def initialize(input)
|
||||
@@ -12,9 +14,8 @@ module Liquid
|
||||
|
||||
def consume(type = nil)
|
||||
token = @tokens[@p]
|
||||
if type && token[0] != type
|
||||
raise SyntaxError, "Expected #{type} but found #{@tokens[@p].first}"
|
||||
end
|
||||
raise SyntaxError, "Expected #{type} but found #{@tokens[@p].first}" if type && token[0] != type
|
||||
|
||||
@p += 1
|
||||
token[1]
|
||||
end
|
||||
@@ -25,6 +26,7 @@ module Liquid
|
||||
def consume?(type)
|
||||
token = @tokens[@p]
|
||||
return false unless token && token[0] == type
|
||||
|
||||
@p += 1
|
||||
token[1]
|
||||
end
|
||||
@@ -34,6 +36,7 @@ module Liquid
|
||||
token = @tokens[@p]
|
||||
return false unless token && token[0] == :id
|
||||
return false unless token[1] == str
|
||||
|
||||
@p += 1
|
||||
token[1]
|
||||
end
|
||||
@@ -41,14 +44,18 @@ 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 [:string, :number].include? token[0]
|
||||
elsif SINGLE_TOKEN_EXPRESSION_TYPES.include?(token[0])
|
||||
consume
|
||||
elsif token.first == :open_round
|
||||
consume
|
||||
@@ -63,11 +70,9 @@ module Liquid
|
||||
end
|
||||
|
||||
def argument
|
||||
str = ""
|
||||
str = +''
|
||||
# might be a keyword argument (identifier: expression)
|
||||
if look(:id) && look(:colon, 1)
|
||||
str << consume << consume << ' '.freeze
|
||||
end
|
||||
str << consume << consume << ' ' if look(:id) && look(:colon, 1)
|
||||
|
||||
str << expression
|
||||
str
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
module ParserSwitching
|
||||
def parse_with_selected_parser(markup)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'liquid/profiler/hooks'
|
||||
|
||||
module Liquid
|
||||
@@ -97,9 +99,9 @@ module Liquid
|
||||
end
|
||||
|
||||
def initialize
|
||||
@partial_stack = ["<root>"]
|
||||
@partial_stack = ['<root>']
|
||||
|
||||
@root_timing = Timing.new("", current_partial)
|
||||
@root_timing = Timing.new('', current_partial)
|
||||
@timing_stack = [@root_timing]
|
||||
|
||||
@render_start_at = Time.now
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class BlockBody
|
||||
def render_node_with_profiling(node, context)
|
||||
def render_node_with_profiling(context, output, node)
|
||||
Profiler.profile_node_render(node) do
|
||||
render_node_without_profiling(node, context)
|
||||
render_node_without_profiling(context, output, node)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -11,13 +13,13 @@ module Liquid
|
||||
end
|
||||
|
||||
class Include < Tag
|
||||
def render_with_profiling(context)
|
||||
def render_to_output_buffer_with_profiling(context, output)
|
||||
Profiler.profile_children(context.evaluate(@template_name_expr).to_s) do
|
||||
render_without_profiling(context)
|
||||
render_to_output_buffer_without_profiling(context, output)
|
||||
end
|
||||
end
|
||||
|
||||
alias_method :render_without_profiling, :render
|
||||
alias_method :render, :render_with_profiling
|
||||
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
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class RangeLookup
|
||||
def self.parse(start_markup, end_markup)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class ResourceLimits
|
||||
attr_accessor :render_length, :render_score, :assign_score,
|
||||
:render_length_limit, :render_score_limit, :assign_score_limit
|
||||
:render_length_limit, :render_score_limit, :assign_score_limit
|
||||
|
||||
def initialize(limits)
|
||||
@render_length_limit = limits[:render_length_limit]
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'cgi'
|
||||
require 'bigdecimal'
|
||||
|
||||
module Liquid
|
||||
module StandardFilters
|
||||
HTML_ESCAPE = {
|
||||
'&'.freeze => '&'.freeze,
|
||||
'>'.freeze => '>'.freeze,
|
||||
'<'.freeze => '<'.freeze,
|
||||
'"'.freeze => '"'.freeze,
|
||||
"'".freeze => '''.freeze
|
||||
}
|
||||
HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/
|
||||
'&' => '&',
|
||||
'>' => '>',
|
||||
'<' => '<',
|
||||
'"' => '"',
|
||||
"'" => ''',
|
||||
}.freeze
|
||||
HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/.freeze
|
||||
STRIP_HTML_BLOCKS = Regexp.union(
|
||||
%r{<script.*?</script>}m,
|
||||
/<!--.*?-->/m,
|
||||
%r{<style.*?</style>}m
|
||||
)
|
||||
STRIP_HTML_TAGS = /<.*?>/m.freeze
|
||||
|
||||
# Return the size of an array or of an string
|
||||
def size(input)
|
||||
@@ -33,7 +41,7 @@ module Liquid
|
||||
end
|
||||
|
||||
def escape(input)
|
||||
CGI.escapeHTML(input).untaint unless input.nil?
|
||||
CGI.escapeHTML(input.to_s).untaint unless input.nil?
|
||||
end
|
||||
alias_method :h, :escape
|
||||
|
||||
@@ -42,11 +50,16 @@ module Liquid
|
||||
end
|
||||
|
||||
def url_encode(input)
|
||||
CGI.escape(input) unless input.nil?
|
||||
CGI.escape(input.to_s) unless input.nil?
|
||||
end
|
||||
|
||||
def url_decode(input)
|
||||
CGI.unescape(input) unless input.nil?
|
||||
return if input.nil?
|
||||
|
||||
result = CGI.unescape(input.to_s)
|
||||
raise Liquid::ArgumentError, "invalid byte sequence in #{result.encoding}" unless result.valid_encoding?
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
def slice(input, offset, length = nil)
|
||||
@@ -61,22 +74,25 @@ module Liquid
|
||||
end
|
||||
|
||||
# Truncate a string down to x characters
|
||||
def truncate(input, length = 50, truncate_string = "...".freeze)
|
||||
def truncate(input, length = 50, truncate_string = '...')
|
||||
return if input.nil?
|
||||
|
||||
input_str = input.to_s
|
||||
length = Utils.to_integer(length)
|
||||
l = length - truncate_string.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] + truncate_string : input_str
|
||||
input_str.length > length ? input_str[0...l].concat(truncate_string_str) : input_str
|
||||
end
|
||||
|
||||
def truncatewords(input, words = 15, truncate_string = "...".freeze)
|
||||
def truncatewords(input, words = 15, truncate_string = '...')
|
||||
return if input.nil?
|
||||
|
||||
wordlist = input.to_s.split
|
||||
words = Utils.to_integer(words)
|
||||
l = words - 1
|
||||
l = 0 if l < 0
|
||||
wordlist.length > l ? wordlist[0..l].join(" ".freeze) + truncate_string : input
|
||||
wordlist.length > l ? wordlist[0..l].join(' ').concat(truncate_string.to_s) : input
|
||||
end
|
||||
|
||||
# Split input string into an array of substrings separated by given pattern.
|
||||
@@ -85,7 +101,7 @@ module Liquid
|
||||
# <div class="summary">{{ post | split '//' | first }}</div>
|
||||
#
|
||||
def split(input, pattern)
|
||||
input.to_s.split(pattern)
|
||||
input.to_s.split(pattern.to_s)
|
||||
end
|
||||
|
||||
def strip(input)
|
||||
@@ -101,17 +117,19 @@ module Liquid
|
||||
end
|
||||
|
||||
def strip_html(input)
|
||||
empty = ''.freeze
|
||||
input.to_s.gsub(/<script.*?<\/script>/m, empty).gsub(/<!--.*?-->/m, empty).gsub(/<style.*?<\/style>/m, empty).gsub(/<.*?>/m, empty)
|
||||
empty = ''
|
||||
result = input.to_s.gsub(STRIP_HTML_BLOCKS, empty)
|
||||
result.gsub!(STRIP_HTML_TAGS, empty)
|
||||
result
|
||||
end
|
||||
|
||||
# Remove all newlines from the string
|
||||
def strip_newlines(input)
|
||||
input.to_s.gsub(/\r?\n/, ''.freeze)
|
||||
input.to_s.gsub(/\r?\n/, '')
|
||||
end
|
||||
|
||||
# Join elements of the array with certain character between them
|
||||
def join(input, glue = ' '.freeze)
|
||||
def join(input, glue = ' ')
|
||||
InputIterator.new(input).join(glue)
|
||||
end
|
||||
|
||||
@@ -119,14 +137,19 @@ module Liquid
|
||||
# provide optional property with which to sort an array of hashes or drops
|
||||
def sort(input, property = nil)
|
||||
ary = InputIterator.new(input)
|
||||
|
||||
return [] if ary.empty?
|
||||
|
||||
if property.nil?
|
||||
ary.sort
|
||||
elsif ary.empty? # The next two cases assume a non-empty array.
|
||||
[]
|
||||
elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
|
||||
ary.sort { |a, b| a[property] <=> b[property] }
|
||||
elsif ary.first.respond_to?(property)
|
||||
ary.sort { |a, b| a.send(property) <=> b.send(property) }
|
||||
ary.sort do |a, b|
|
||||
nil_safe_compare(a, b)
|
||||
end
|
||||
elsif ary.all? { |el| el.respond_to?(:[]) }
|
||||
begin
|
||||
ary.sort { |a, b| nil_safe_compare(a[property], b[property]) }
|
||||
rescue TypeError
|
||||
raise_property_error(property)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -135,14 +158,40 @@ module Liquid
|
||||
def sort_natural(input, property = nil)
|
||||
ary = InputIterator.new(input)
|
||||
|
||||
return [] if ary.empty?
|
||||
|
||||
if property.nil?
|
||||
ary.sort { |a, b| a.casecmp(b) }
|
||||
elsif ary.empty? # The next two cases assume a non-empty array.
|
||||
ary.sort do |a, b|
|
||||
nil_safe_casecmp(a, b)
|
||||
end
|
||||
elsif ary.all? { |el| el.respond_to?(:[]) }
|
||||
begin
|
||||
ary.sort { |a, b| nil_safe_casecmp(a[property], b[property]) }
|
||||
rescue TypeError
|
||||
raise_property_error(property)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# 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)
|
||||
|
||||
if ary.empty?
|
||||
[]
|
||||
elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
|
||||
ary.sort { |a, b| a[property].casecmp(b[property]) }
|
||||
elsif ary.first.respond_to?(property)
|
||||
ary.sort { |a, b| a.send(property).casecmp(b.send(property)) }
|
||||
elsif ary.first.respond_to?(:[]) && target_value.nil?
|
||||
begin
|
||||
ary.select { |item| item[property] }
|
||||
rescue TypeError
|
||||
raise_property_error(property)
|
||||
end
|
||||
elsif ary.first.respond_to?(:[])
|
||||
begin
|
||||
ary.select { |item| item[property] == target_value }
|
||||
rescue TypeError
|
||||
raise_property_error(property)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -156,7 +205,11 @@ module Liquid
|
||||
elsif ary.empty? # The next two cases assume a non-empty array.
|
||||
[]
|
||||
elsif ary.first.respond_to?(:[])
|
||||
ary.uniq{ |a| a[property] }
|
||||
begin
|
||||
ary.uniq { |a| a[property] }
|
||||
rescue TypeError
|
||||
raise_property_error(property)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -171,13 +224,15 @@ module Liquid
|
||||
InputIterator.new(input).map do |e|
|
||||
e = e.call if e.is_a?(Proc)
|
||||
|
||||
if property == "to_liquid".freeze
|
||||
if property == 'to_liquid'
|
||||
e
|
||||
elsif e.respond_to?(:[])
|
||||
r = e[property]
|
||||
r.is_a?(Proc) ? r.call : r
|
||||
end
|
||||
end
|
||||
rescue TypeError
|
||||
raise_property_error(property)
|
||||
end
|
||||
|
||||
# Remove nils within an array
|
||||
@@ -190,30 +245,32 @@ module Liquid
|
||||
elsif ary.empty? # The next two cases assume a non-empty array.
|
||||
[]
|
||||
elsif ary.first.respond_to?(:[])
|
||||
ary.reject{ |a| a[property].nil? }
|
||||
elsif ary.first.respond_to?(property)
|
||||
ary.reject { |a| a.send(property).nil? }
|
||||
begin
|
||||
ary.reject { |a| a[property].nil? }
|
||||
rescue TypeError
|
||||
raise_property_error(property)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Replace occurrences of a string with another
|
||||
def replace(input, string, replacement = ''.freeze)
|
||||
def replace(input, string, replacement = '')
|
||||
input.to_s.gsub(string.to_s, replacement.to_s)
|
||||
end
|
||||
|
||||
# Replace the first occurrences of a string with another
|
||||
def replace_first(input, string, replacement = ''.freeze)
|
||||
def replace_first(input, string, replacement = '')
|
||||
input.to_s.sub(string.to_s, replacement.to_s)
|
||||
end
|
||||
|
||||
# remove a substring
|
||||
def remove(input, string)
|
||||
input.to_s.gsub(string.to_s, ''.freeze)
|
||||
input.to_s.gsub(string.to_s, '')
|
||||
end
|
||||
|
||||
# remove the first occurrences of a substring
|
||||
def remove_first(input, string)
|
||||
input.to_s.sub(string.to_s, ''.freeze)
|
||||
input.to_s.sub(string.to_s, '')
|
||||
end
|
||||
|
||||
# add one string to another
|
||||
@@ -222,6 +279,8 @@ 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)
|
||||
end
|
||||
|
||||
@@ -232,7 +291,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".freeze)
|
||||
input.to_s.gsub(/\n/, "<br />\n")
|
||||
end
|
||||
|
||||
# Reformat a date using Ruby's core Time#strftime( string ) -> string
|
||||
@@ -269,7 +328,7 @@ module Liquid
|
||||
def date(input, format)
|
||||
return input if format.to_s.empty?
|
||||
|
||||
return input unless date = Utils.to_date(input)
|
||||
return input unless (date = Utils.to_date(input))
|
||||
|
||||
date.strftime(format.to_s)
|
||||
end
|
||||
@@ -292,6 +351,12 @@ module Liquid
|
||||
array.last if array.respond_to?(:last)
|
||||
end
|
||||
|
||||
# absolute value
|
||||
def abs(input)
|
||||
result = Utils.to_number(input).abs
|
||||
result.is_a?(BigDecimal) ? result.to_f : result
|
||||
end
|
||||
|
||||
# addition
|
||||
def plus(input, operand)
|
||||
apply_operation(input, operand, :+)
|
||||
@@ -341,7 +406,23 @@ module Liquid
|
||||
raise Liquid::FloatDomainError, e.message
|
||||
end
|
||||
|
||||
def default(input, default_value = ''.freeze)
|
||||
def at_least(input, n)
|
||||
min_value = Utils.to_number(n)
|
||||
|
||||
result = Utils.to_number(input)
|
||||
result = min_value if min_value > result
|
||||
result.is_a?(BigDecimal) ? result.to_f : result
|
||||
end
|
||||
|
||||
def at_most(input, n)
|
||||
max_value = Utils.to_number(n)
|
||||
|
||||
result = Utils.to_number(input)
|
||||
result = max_value if max_value < result
|
||||
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
|
||||
@@ -351,11 +432,31 @@ module Liquid
|
||||
|
||||
private
|
||||
|
||||
def raise_property_error(property)
|
||||
raise Liquid::ArgumentError, "cannot select the property '#{property}'"
|
||||
end
|
||||
|
||||
def apply_operation(input, operand, operation)
|
||||
result = Utils.to_number(input).send(operation, Utils.to_number(operand))
|
||||
result.is_a?(BigDecimal) ? result.to_f : result
|
||||
end
|
||||
|
||||
def nil_safe_compare(a, b)
|
||||
if !a.nil? && !b.nil?
|
||||
a <=> b
|
||||
else
|
||||
a.nil? ? 1 : -1
|
||||
end
|
||||
end
|
||||
|
||||
def nil_safe_casecmp(a, b)
|
||||
if !a.nil? && !b.nil?
|
||||
a.to_s.casecmp(b.to_s)
|
||||
else
|
||||
a.nil? ? 1 : -1
|
||||
end
|
||||
end
|
||||
|
||||
class InputIterator
|
||||
include Enumerable
|
||||
|
||||
@@ -372,11 +473,11 @@ module Liquid
|
||||
end
|
||||
|
||||
def join(glue)
|
||||
to_a.join(glue)
|
||||
to_a.join(glue.to_s)
|
||||
end
|
||||
|
||||
def concat(args)
|
||||
to_a.concat args
|
||||
to_a.concat(args)
|
||||
end
|
||||
|
||||
def reverse
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'set'
|
||||
|
||||
module Liquid
|
||||
@@ -7,12 +9,12 @@ module Liquid
|
||||
# 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
|
||||
@global_strainer = Class.new(Strainer) do
|
||||
@filter_methods = Set.new
|
||||
end
|
||||
@@strainer_class_cache = Hash.new do |hash, filters|
|
||||
hash[filters] = Class.new(@@global_strainer) do
|
||||
@filter_methods = @@global_strainer.filter_methods.dup
|
||||
@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
|
||||
@@ -22,19 +24,27 @@ module Liquid
|
||||
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: #{f.class}" unless filter.is_a?(Module)
|
||||
unless self.class.include?(filter)
|
||||
send(:include, filter)
|
||||
@filter_methods.merge(filter.public_instance_methods.map(&:to_s))
|
||||
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)
|
||||
@@global_strainer.add_filter(filter)
|
||||
@strainer_class_cache.clear
|
||||
@global_strainer.add_filter(filter)
|
||||
end
|
||||
|
||||
def self.invokable?(method)
|
||||
@@ -42,12 +52,14 @@ module Liquid
|
||||
end
|
||||
|
||||
def self.create(context, filters = [])
|
||||
@@strainer_class_cache[filters].new(context)
|
||||
@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
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class TablerowloopDrop < Drop
|
||||
def initialize(length, cols)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Tag
|
||||
attr_reader :nodelist, :tag_name, :line_number, :parse_context
|
||||
@@ -21,8 +23,7 @@ module Liquid
|
||||
@line_number = parse_context.line_number
|
||||
end
|
||||
|
||||
def parse(_tokens)
|
||||
end
|
||||
def parse(_tokens); end
|
||||
|
||||
def raw
|
||||
"#{@tag_name} #{@markup}"
|
||||
@@ -33,7 +34,15 @@ module Liquid
|
||||
end
|
||||
|
||||
def render(_context)
|
||||
''.freeze
|
||||
''
|
||||
end
|
||||
|
||||
# For backwards compatibility with custom tags. In a future release, the semantics
|
||||
# of the `render_to_output_buffer` method will become the default and the `render`
|
||||
# method will be removed.
|
||||
def render_to_output_buffer(context, output)
|
||||
output << render(context)
|
||||
output
|
||||
end
|
||||
|
||||
def blank?
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# Assign sets a variable in your template.
|
||||
#
|
||||
@@ -8,32 +10,52 @@ module Liquid
|
||||
# {{ foo }}
|
||||
#
|
||||
class Assign < Tag
|
||||
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
|
||||
SYNTAX = /(#{VARIABLE_SIGNATURE}+)\s*=\s*(.*)\s*/om.freeze
|
||||
|
||||
attr_reader :to, :from
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
if markup =~ Syntax
|
||||
@to = $1
|
||||
@from = Variable.new($2, options)
|
||||
if markup =~ SYNTAX
|
||||
@to = Regexp.last_match(1)
|
||||
@from = Variable.new(Regexp.last_match(2), options)
|
||||
else
|
||||
raise SyntaxError.new options[:locale].t("errors.syntax.assign".freeze)
|
||||
raise SyntaxError, options[:locale].t('errors.syntax.assign')
|
||||
end
|
||||
end
|
||||
|
||||
def render(context)
|
||||
def render_to_output_buffer(context, output)
|
||||
val = @from.render(context)
|
||||
context.scopes.last[@to] = val
|
||||
|
||||
inc = val.instance_of?(String) || val.instance_of?(Array) || val.instance_of?(Hash) ? val.length : 1
|
||||
context.resource_limits.assign_score += inc
|
||||
|
||||
''.freeze
|
||||
context.resource_limits.assign_score += assign_score_of(val)
|
||||
output
|
||||
end
|
||||
|
||||
def blank?
|
||||
true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def assign_score_of(val)
|
||||
if val.instance_of?(String)
|
||||
val.bytesize
|
||||
elsif val.instance_of?(Array) || val.instance_of?(Hash)
|
||||
sum = 1
|
||||
# Uses #each to avoid extra allocations.
|
||||
val.each { |child| sum += assign_score_of(child) }
|
||||
sum
|
||||
else
|
||||
1
|
||||
end
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
[@node.from]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('assign'.freeze, Assign)
|
||||
Template.register_tag('assign', Assign)
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# Break tag to be used to break out of a for loop.
|
||||
#
|
||||
@@ -14,5 +16,5 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('break'.freeze, Break)
|
||||
Template.register_tag('break', Break)
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# Capture stores the result of a block into a variable without rendering it inplace.
|
||||
#
|
||||
@@ -11,22 +13,23 @@ module Liquid
|
||||
# in a sidebar or footer.
|
||||
#
|
||||
class Capture < Block
|
||||
Syntax = /(#{VariableSignature}+)/o
|
||||
SYNTAX = /(#{VARIABLE_SIGNATURE}+)/o.freeze
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
if markup =~ Syntax
|
||||
@to = $1
|
||||
if markup =~ SYNTAX
|
||||
@to = Regexp.last_match(1)
|
||||
else
|
||||
raise SyntaxError.new(options[:locale].t("errors.syntax.capture"))
|
||||
raise SyntaxError, options[:locale].t('errors.syntax.capture')
|
||||
end
|
||||
end
|
||||
|
||||
def render(context)
|
||||
output = super
|
||||
def render_to_output_buffer(context, output)
|
||||
previous_output_size = output.bytesize
|
||||
super
|
||||
context.scopes.last[@to] = output
|
||||
context.resource_limits.assign_score += output.length
|
||||
''.freeze
|
||||
context.resource_limits.assign_score += (output.bytesize - previous_output_size)
|
||||
output
|
||||
end
|
||||
|
||||
def blank?
|
||||
@@ -34,5 +37,5 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('capture'.freeze, Capture)
|
||||
Template.register_tag('capture', Capture)
|
||||
end
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Case < Block
|
||||
Syntax = /(#{QuotedFragment})/o
|
||||
WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om
|
||||
SYNTAX = /(#{QUOTED_FRAGMENT})/o.freeze
|
||||
WHEN_SYNTAX = /(#{QUOTED_FRAGMENT})(?:(?:\s+or\s+|\s*\,\s*)(#{QUOTED_FRAGMENT}.*))?/om.freeze
|
||||
|
||||
attr_reader :blocks, :left
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
@blocks = []
|
||||
|
||||
if markup =~ Syntax
|
||||
@left = Expression.parse($1)
|
||||
if markup =~ SYNTAX
|
||||
@left = Expression.parse(Regexp.last_match(1))
|
||||
else
|
||||
raise SyntaxError.new(options[:locale].t("errors.syntax.case".freeze))
|
||||
raise SyntaxError, options[:locale].t('errors.syntax.case')
|
||||
end
|
||||
end
|
||||
|
||||
def parse(tokens)
|
||||
body = BlockBody.new
|
||||
while parse_body(body, tokens)
|
||||
body = @blocks.last.attachment
|
||||
end
|
||||
body = @blocks.last.attachment while parse_body(body, tokens)
|
||||
end
|
||||
|
||||
def nodelist
|
||||
@@ -27,30 +29,30 @@ module Liquid
|
||||
|
||||
def unknown_tag(tag, markup, tokens)
|
||||
case tag
|
||||
when 'when'.freeze
|
||||
when 'when'
|
||||
record_when_condition(markup)
|
||||
when 'else'.freeze
|
||||
when 'else'
|
||||
record_else_condition(markup)
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def render(context)
|
||||
def render_to_output_buffer(context, output)
|
||||
context.stack do
|
||||
execute_else_block = true
|
||||
|
||||
output = ''
|
||||
@blocks.each do |block|
|
||||
if block.else?
|
||||
return block.attachment.render(context) if execute_else_block
|
||||
block.attachment.render_to_output_buffer(context, output) if execute_else_block
|
||||
elsif block.evaluate(context)
|
||||
execute_else_block = false
|
||||
output << block.attachment.render(context)
|
||||
block.attachment.render_to_output_buffer(context, output)
|
||||
end
|
||||
end
|
||||
output
|
||||
end
|
||||
|
||||
output
|
||||
end
|
||||
|
||||
private
|
||||
@@ -59,28 +61,30 @@ module Liquid
|
||||
body = BlockBody.new
|
||||
|
||||
while markup
|
||||
unless markup =~ WhenSyntax
|
||||
raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_when".freeze))
|
||||
end
|
||||
raise SyntaxError, options[:locale].t('errors.syntax.case_invalid_when') unless markup =~ WHEN_SYNTAX
|
||||
|
||||
markup = $2
|
||||
markup = Regexp.last_match(2)
|
||||
|
||||
block = Condition.new(@left, '=='.freeze, Expression.parse($1))
|
||||
block = Condition.new(@left, '==', Expression.parse(Regexp.last_match(1)))
|
||||
block.attach(body)
|
||||
@blocks << block
|
||||
end
|
||||
end
|
||||
|
||||
def record_else_condition(markup)
|
||||
unless markup.strip.empty?
|
||||
raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_else".freeze))
|
||||
end
|
||||
raise SyntaxError, options[:locale].t('errors.syntax.case_invalid_else') unless markup.strip.empty?
|
||||
|
||||
block = ElseCondition.new
|
||||
block.attach(BlockBody.new)
|
||||
@blocks << block
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
[@node.left] + @node.blocks
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('case'.freeze, Case)
|
||||
Template.register_tag('case', Case)
|
||||
end
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Comment < Block
|
||||
def render(_context)
|
||||
''.freeze
|
||||
def render_to_output_buffer(_context, output)
|
||||
output
|
||||
end
|
||||
|
||||
def unknown_tag(_tag, _markup, _tokens)
|
||||
end
|
||||
def unknown_tag(_tag, _markup, _tokens); end
|
||||
|
||||
def blank?
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('comment'.freeze, Comment)
|
||||
Template.register_tag('comment', Comment)
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# Continue tag to be used to break out of a for loop.
|
||||
#
|
||||
@@ -14,5 +16,5 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('continue'.freeze, Continue)
|
||||
Template.register_tag('continue', Continue)
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# Cycle is usually used within a loop to alternate between values, like colors or DOM classes.
|
||||
#
|
||||
@@ -12,45 +14,64 @@ module Liquid
|
||||
# <div class="green"> Item five</div>
|
||||
#
|
||||
class Cycle < Tag
|
||||
SimpleSyntax = /\A#{QuotedFragment}+/o
|
||||
NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om
|
||||
SIMPLE_SYNTAX = /\A#{QUOTED_FRAGMENT}+/o.freeze
|
||||
NAMED_SYNTAX = /\A(#{QUOTED_FRAGMENT})\s*\:\s*(.*)/om.freeze
|
||||
|
||||
attr_reader :variables
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
case markup
|
||||
when NamedSyntax
|
||||
@variables = variables_from_string($2)
|
||||
@name = Expression.parse($1)
|
||||
when SimpleSyntax
|
||||
when NAMED_SYNTAX
|
||||
@variables = variables_from_string(Regexp.last_match(2))
|
||||
@name = Expression.parse(Regexp.last_match(1))
|
||||
when SIMPLE_SYNTAX
|
||||
@variables = variables_from_string(markup)
|
||||
@name = @variables.to_s
|
||||
else
|
||||
raise SyntaxError.new(options[:locale].t("errors.syntax.cycle".freeze))
|
||||
raise SyntaxError, options[:locale].t('errors.syntax.cycle')
|
||||
end
|
||||
end
|
||||
|
||||
def render(context)
|
||||
context.registers[:cycle] ||= Hash.new(0)
|
||||
def render_to_output_buffer(context, output)
|
||||
context.registers[:cycle] ||= {}
|
||||
|
||||
context.stack do
|
||||
key = context.evaluate(@name)
|
||||
iteration = context.registers[:cycle][key]
|
||||
result = context.evaluate(@variables[iteration])
|
||||
iteration = context.registers[:cycle][key].to_i
|
||||
|
||||
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
|
||||
result
|
||||
end
|
||||
|
||||
output
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def variables_from_string(markup)
|
||||
markup.split(',').collect do |var|
|
||||
var =~ /\s*(#{QuotedFragment})\s*/o
|
||||
$1 ? Expression.parse($1) : nil
|
||||
var =~ /\s*(#{QUOTED_FRAGMENT})\s*/o
|
||||
Regexp.last_match(1) ? Expression.parse(Regexp.last_match(1)) : nil
|
||||
end.compact
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
Array(@node.variables)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('cycle', Cycle)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# decrement is used in a place where one needs to insert a counter
|
||||
# into a template, and needs the counter to survive across
|
||||
@@ -23,13 +25,14 @@ module Liquid
|
||||
@variable = markup.strip
|
||||
end
|
||||
|
||||
def render(context)
|
||||
def render_to_output_buffer(context, output)
|
||||
value = context.environments.first[@variable] ||= 0
|
||||
value -= 1
|
||||
context.environments.first[@variable] = value
|
||||
value.to_s
|
||||
output << value.to_s
|
||||
output
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('decrement'.freeze, Decrement)
|
||||
Template.register_tag('decrement', Decrement)
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# "For" iterates over an array or collection.
|
||||
# Several useful variables are available to you within the loop.
|
||||
@@ -23,7 +25,7 @@ module Liquid
|
||||
# {{ item.name }}
|
||||
# {% end %}
|
||||
#
|
||||
# To reverse the for loop simply use {% for item in collection reversed %}
|
||||
# To reverse the for loop simply use {% for item in collection reversed %} (note that the flag's spelling is different to the filter `reverse`)
|
||||
#
|
||||
# == Available variables:
|
||||
#
|
||||
@@ -44,16 +46,21 @@ module Liquid
|
||||
# forloop.parentloop:: Provides access to the parent loop, if present.
|
||||
#
|
||||
class For < Block
|
||||
Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
|
||||
SYNTAX = /\A(#{VARIABLE_SEGMENT}+)\s+in\s+(#{QUOTED_FRAGMENT}+)\s*(reversed)?/o.freeze
|
||||
|
||||
attr_reader :collection_name, :variable_name, :limit, :from
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
@from = @limit = nil
|
||||
parse_with_selected_parser(markup)
|
||||
@for_block = BlockBody.new
|
||||
@else_block = nil
|
||||
end
|
||||
|
||||
def parse(tokens)
|
||||
return unless parse_body(@for_block, tokens)
|
||||
|
||||
parse_body(@else_block, tokens)
|
||||
end
|
||||
|
||||
@@ -62,50 +69,55 @@ module Liquid
|
||||
end
|
||||
|
||||
def unknown_tag(tag, markup, tokens)
|
||||
return super unless tag == 'else'.freeze
|
||||
return super unless tag == 'else'
|
||||
|
||||
@else_block = BlockBody.new
|
||||
end
|
||||
|
||||
def render(context)
|
||||
def render_to_output_buffer(context, output)
|
||||
segment = collection_segment(context)
|
||||
|
||||
if segment.empty?
|
||||
render_else(context)
|
||||
render_else(context, output)
|
||||
else
|
||||
render_segment(context, segment)
|
||||
render_segment(context, output, segment)
|
||||
end
|
||||
|
||||
output
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def lax_parse(markup)
|
||||
if markup =~ Syntax
|
||||
@variable_name = $1
|
||||
collection_name = $2
|
||||
@reversed = !!$3
|
||||
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(TagAttributes) do |key, value|
|
||||
markup.scan(TAG_ATTRIBUTES) do |key, value|
|
||||
set_attribute(key, value)
|
||||
end
|
||||
else
|
||||
raise SyntaxError.new(options[:locale].t("errors.syntax.for".freeze))
|
||||
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.new(options[:locale].t("errors.syntax.for_invalid_in".freeze)) unless p.id?('in'.freeze)
|
||||
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)
|
||||
@reversed = p.id?('reversed'.freeze)
|
||||
@reversed = p.id?('reversed')
|
||||
|
||||
while p.look(:id) && p.look(:colon, 1)
|
||||
unless attribute = p.id?('limit'.freeze) || p.id?('offset'.freeze)
|
||||
raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_attribute".freeze))
|
||||
unless (attribute = p.id?('limit') || p.id?('offset'))
|
||||
raise SyntaxError, options[:locale].t('errors.syntax.for_invalid_attribute')
|
||||
end
|
||||
|
||||
p.consume
|
||||
set_attribute(attribute, p.expression)
|
||||
end
|
||||
@@ -115,19 +127,28 @@ module Liquid
|
||||
private
|
||||
|
||||
def collection_segment(context)
|
||||
offsets = context.registers[:for] ||= Hash.new(0)
|
||||
offsets = context.registers[:for] ||= {}
|
||||
|
||||
from = if @from == :continue
|
||||
offsets[@name].to_i
|
||||
else
|
||||
context.evaluate(@from).to_i
|
||||
from_value = context.evaluate(@from)
|
||||
if from_value.nil?
|
||||
0
|
||||
else
|
||||
Utils.to_integer(from_value)
|
||||
end
|
||||
end
|
||||
|
||||
collection = context.evaluate(@collection_name)
|
||||
collection = collection.to_a if collection.is_a?(Range)
|
||||
|
||||
limit = context.evaluate(@limit)
|
||||
to = limit ? limit.to_i + from : nil
|
||||
limit_value = context.evaluate(@limit)
|
||||
to = if limit_value.nil?
|
||||
nil
|
||||
else
|
||||
Utils.to_integer(limit_value) + from
|
||||
end
|
||||
|
||||
segment = Utils.slice_collection(collection, from, to)
|
||||
segment.reverse! if @reversed
|
||||
@@ -137,57 +158,65 @@ module Liquid
|
||||
segment
|
||||
end
|
||||
|
||||
def render_segment(context, segment)
|
||||
def render_segment(context, output, segment)
|
||||
for_stack = context.registers[:for_stack] ||= []
|
||||
length = segment.length
|
||||
|
||||
result = ''
|
||||
|
||||
context.stack do
|
||||
loop_vars = Liquid::ForloopDrop.new(@name, length, for_stack[-1])
|
||||
|
||||
for_stack.push(loop_vars)
|
||||
|
||||
begin
|
||||
context['forloop'.freeze] = loop_vars
|
||||
context['forloop'] = loop_vars
|
||||
|
||||
segment.each_with_index do |item, index|
|
||||
segment.each do |item|
|
||||
context[@variable_name] = item
|
||||
result << @for_block.render(context)
|
||||
@for_block.render_to_output_buffer(context, output)
|
||||
loop_vars.send(:increment!)
|
||||
|
||||
# Handle any interrupts if they exist.
|
||||
if context.interrupt?
|
||||
interrupt = context.pop_interrupt
|
||||
break if interrupt.is_a? BreakInterrupt
|
||||
next if interrupt.is_a? ContinueInterrupt
|
||||
end
|
||||
next unless context.interrupt?
|
||||
|
||||
interrupt = context.pop_interrupt
|
||||
break if interrupt.is_a?(BreakInterrupt)
|
||||
next if interrupt.is_a?(ContinueInterrupt)
|
||||
end
|
||||
ensure
|
||||
for_stack.pop
|
||||
end
|
||||
end
|
||||
|
||||
result
|
||||
output
|
||||
end
|
||||
|
||||
def set_attribute(key, expr)
|
||||
case key
|
||||
when 'offset'.freeze
|
||||
@from = if expr == 'continue'.freeze
|
||||
when 'offset'
|
||||
@from = if expr == 'continue'
|
||||
:continue
|
||||
else
|
||||
Expression.parse(expr)
|
||||
end
|
||||
when 'limit'.freeze
|
||||
when 'limit'
|
||||
@limit = Expression.parse(expr)
|
||||
end
|
||||
end
|
||||
|
||||
def render_else(context)
|
||||
@else_block ? @else_block.render(context) : ''.freeze
|
||||
def render_else(context, output)
|
||||
if @else_block
|
||||
@else_block.render_to_output_buffer(context, output)
|
||||
else
|
||||
output
|
||||
end
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
(super + [@node.limit, @node.from, @node.collection_name]).compact
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('for'.freeze, For)
|
||||
Template.register_tag('for', For)
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# If is the conditional block
|
||||
#
|
||||
@@ -10,14 +12,20 @@ module Liquid
|
||||
# There are {% if count < 5 %} less {% else %} more {% endif %} items than you need.
|
||||
#
|
||||
class If < Block
|
||||
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)
|
||||
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
|
||||
|
||||
attr_reader :blocks
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
@blocks = []
|
||||
push_block('if'.freeze, markup)
|
||||
push_block('if', markup)
|
||||
end
|
||||
|
||||
def nodelist
|
||||
@blocks.map(&:attachment)
|
||||
end
|
||||
|
||||
def parse(tokens)
|
||||
@@ -25,33 +33,28 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
def nodelist
|
||||
@blocks.map(&:attachment)
|
||||
end
|
||||
|
||||
def unknown_tag(tag, markup, tokens)
|
||||
if ['elsif'.freeze, 'else'.freeze].include?(tag)
|
||||
if %w[elsif else].include?(tag)
|
||||
push_block(tag, markup)
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def render(context)
|
||||
def render_to_output_buffer(context, output)
|
||||
context.stack do
|
||||
@blocks.each do |block|
|
||||
if block.evaluate(context)
|
||||
return block.attachment.render(context)
|
||||
end
|
||||
return block.attachment.render_to_output_buffer(context, output) if block.evaluate(context)
|
||||
end
|
||||
''.freeze
|
||||
end
|
||||
|
||||
output
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def push_block(tag, markup)
|
||||
block = if tag == 'else'.freeze
|
||||
block = if tag == 'else'
|
||||
ElseCondition.new
|
||||
else
|
||||
parse_with_selected_parser(markup)
|
||||
@@ -62,18 +65,19 @@ module Liquid
|
||||
end
|
||||
|
||||
def lax_parse(markup)
|
||||
expressions = markup.scan(ExpressionsAndOperators)
|
||||
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop =~ Syntax
|
||||
expressions = markup.scan(EXPRESSIONS_AND_OPERATORS)
|
||||
raise SyntaxError, options[:locale].t('errors.syntax.if') unless expressions.pop =~ SYNTAX
|
||||
|
||||
condition = Condition.new(Expression.parse($1), $2, Expression.parse($3))
|
||||
condition = Condition.new(Expression.parse(Regexp.last_match(1)), Regexp.last_match(2), Expression.parse(Regexp.last_match(3)))
|
||||
|
||||
until expressions.empty?
|
||||
operator = expressions.pop.to_s.strip
|
||||
|
||||
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop.to_s =~ Syntax
|
||||
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)
|
||||
|
||||
new_condition = Condition.new(Expression.parse($1), $2, Expression.parse($3))
|
||||
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless BOOLEAN_OPERATORS.include?(operator)
|
||||
new_condition.send(operator, condition)
|
||||
condition = new_condition
|
||||
end
|
||||
@@ -83,29 +87,38 @@ module Liquid
|
||||
|
||||
def strict_parse(markup)
|
||||
p = Parser.new(markup)
|
||||
condition = parse_binary_comparison(p)
|
||||
condition = parse_binary_comparisons(p)
|
||||
p.consume(:end_of_string)
|
||||
condition
|
||||
end
|
||||
|
||||
def parse_binary_comparison(p)
|
||||
def parse_binary_comparisons(p)
|
||||
condition = parse_comparison(p)
|
||||
if op = (p.id?('and'.freeze) || p.id?('or'.freeze))
|
||||
condition.send(op, parse_binary_comparison(p))
|
||||
first_condition = condition
|
||||
while (op = (p.id?('and') || p.id?('or')))
|
||||
child_condition = parse_comparison(p)
|
||||
condition.send(op, child_condition)
|
||||
condition = child_condition
|
||||
end
|
||||
condition
|
||||
first_condition
|
||||
end
|
||||
|
||||
def parse_comparison(p)
|
||||
a = Expression.parse(p.expression)
|
||||
if op = p.consume?(:comparison)
|
||||
if (op = p.consume?(:comparison))
|
||||
b = Expression.parse(p.expression)
|
||||
Condition.new(a, op, b)
|
||||
else
|
||||
Condition.new(a)
|
||||
end
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
@node.blocks
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('if'.freeze, If)
|
||||
Template.register_tag('if', If)
|
||||
end
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Ifchanged < Block
|
||||
def render(context)
|
||||
def render_to_output_buffer(context, output)
|
||||
context.stack do
|
||||
output = super
|
||||
block_output = +''
|
||||
super(context, block_output)
|
||||
|
||||
if output != context.registers[:ifchanged]
|
||||
context.registers[:ifchanged] = output
|
||||
output
|
||||
else
|
||||
''.freeze
|
||||
if block_output != context.registers[:ifchanged]
|
||||
context.registers[:ifchanged] = block_output
|
||||
output << block_output
|
||||
end
|
||||
end
|
||||
|
||||
output
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('ifchanged'.freeze, Ifchanged)
|
||||
Template.register_tag('ifchanged', Ifchanged)
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# Include allows templates to relate with other templates
|
||||
#
|
||||
@@ -14,42 +16,44 @@ module Liquid
|
||||
# {% include 'product' for products %}
|
||||
#
|
||||
class Include < Tag
|
||||
Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/o
|
||||
SYNTAX = /(#{QUOTED_FRAGMENT}+)(\s+(?:with|for)\s+(#{QUOTED_FRAGMENT}+))?/o.freeze
|
||||
|
||||
attr_reader :template_name_expr, :variable_name_expr, :attributes
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
|
||||
if markup =~ Syntax
|
||||
if markup =~ SYNTAX
|
||||
|
||||
template_name = $1
|
||||
variable_name = $3
|
||||
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 = {}
|
||||
|
||||
markup.scan(TagAttributes) do |key, value|
|
||||
markup.scan(TAG_ATTRIBUTES) do |key, value|
|
||||
@attributes[key] = Expression.parse(value)
|
||||
end
|
||||
|
||||
else
|
||||
raise SyntaxError.new(options[:locale].t("errors.syntax.include".freeze))
|
||||
raise SyntaxError, options[:locale].t('errors.syntax.include')
|
||||
end
|
||||
end
|
||||
|
||||
def parse(_tokens)
|
||||
end
|
||||
def parse(_tokens); end
|
||||
|
||||
def render(context)
|
||||
def render_to_output_buffer(context, output)
|
||||
template_name = context.evaluate(@template_name_expr)
|
||||
partial = load_cached_partial(template_name, context)
|
||||
raise ArgumentError, options[:locale].t('errors.argument.include') unless template_name
|
||||
|
||||
context_variable_name = template_name.split('/'.freeze).last
|
||||
partial = load_cached_partial(template_name, context)
|
||||
context_variable_name = template_name.split('/').last
|
||||
|
||||
variable = if @variable_name_expr
|
||||
context.evaluate(@variable_name_expr)
|
||||
else
|
||||
context.find_variable(template_name)
|
||||
context.find_variable(template_name, raise_on_not_found: false)
|
||||
end
|
||||
|
||||
old_template_name = context.template_name
|
||||
@@ -63,19 +67,21 @@ module Liquid
|
||||
end
|
||||
|
||||
if variable.is_a?(Array)
|
||||
variable.collect do |var|
|
||||
variable.each do |var|
|
||||
context[context_variable_name] = var
|
||||
partial.render(context)
|
||||
partial.render_to_output_buffer(context, output)
|
||||
end
|
||||
else
|
||||
context[context_variable_name] = variable
|
||||
partial.render(context)
|
||||
partial.render_to_output_buffer(context, output)
|
||||
end
|
||||
end
|
||||
ensure
|
||||
context.template_name = old_template_name
|
||||
context.partial = old_partial
|
||||
end
|
||||
|
||||
output
|
||||
end
|
||||
|
||||
private
|
||||
@@ -86,9 +92,10 @@ module Liquid
|
||||
def load_cached_partial(template_name, context)
|
||||
cached_partials = context.registers[:cached_partials] || {}
|
||||
|
||||
if cached = cached_partials[template_name]
|
||||
if (cached = cached_partials[template_name])
|
||||
return cached
|
||||
end
|
||||
|
||||
source = read_template_from_file_system(context)
|
||||
begin
|
||||
parse_context.partial = true
|
||||
@@ -106,7 +113,16 @@ module Liquid
|
||||
|
||||
file_system.read_template_file(context.evaluate(@template_name_expr))
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
[
|
||||
@node.template_name_expr,
|
||||
@node.variable_name_expr,
|
||||
] + @node.attributes.values
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('include'.freeze, Include)
|
||||
Template.register_tag('include', Include)
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# increment is used in a place where one needs to insert a counter
|
||||
# into a template, and needs the counter to survive across
|
||||
@@ -20,12 +22,13 @@ module Liquid
|
||||
@variable = markup.strip
|
||||
end
|
||||
|
||||
def render(context)
|
||||
def render_to_output_buffer(context, output)
|
||||
value = context.environments.first[@variable] ||= 0
|
||||
context.environments.first[@variable] = value + 1
|
||||
value.to_s
|
||||
output << value.to_s
|
||||
output
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('increment'.freeze, Increment)
|
||||
Template.register_tag('increment', Increment)
|
||||
end
|
||||
|
||||
@@ -1,31 +1,32 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Raw < Block
|
||||
Syntax = /\A\s*\z/
|
||||
FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
|
||||
SYNTAX = /\A\s*\z/.freeze
|
||||
FULL_TOKEN_POSSIBLY_INVALID = /\A(.*)#{TAG_START}\s*(\w+)\s*(.*)?#{TAG_END}\z/om.freeze
|
||||
|
||||
def initialize(tag_name, markup, parse_context)
|
||||
super
|
||||
|
||||
unless markup =~ Syntax
|
||||
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_unexpected_args".freeze, tag: tag_name))
|
||||
end
|
||||
ensure_valid_markup(tag_name, markup, parse_context)
|
||||
end
|
||||
|
||||
def parse(tokens)
|
||||
@body = ''
|
||||
while token = tokens.shift
|
||||
if token =~ FullTokenPossiblyInvalid
|
||||
@body << $1 if $1 != "".freeze
|
||||
return if block_delimiter == $2
|
||||
@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)
|
||||
end
|
||||
@body << token unless token.empty?
|
||||
end
|
||||
|
||||
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name))
|
||||
raise SyntaxError, parse_context.locale.t('errors.syntax.tag_never_closed', block_name: block_name)
|
||||
end
|
||||
|
||||
def render(_context)
|
||||
@body
|
||||
def render_to_output_buffer(_context, output)
|
||||
output << @body
|
||||
output
|
||||
end
|
||||
|
||||
def nodelist
|
||||
@@ -35,7 +36,13 @@ module Liquid
|
||||
def blank?
|
||||
@body.empty?
|
||||
end
|
||||
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('raw'.freeze, Raw)
|
||||
Template.register_tag('raw', Raw)
|
||||
end
|
||||
|
||||
@@ -1,54 +1,65 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class TableRow < Block
|
||||
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
|
||||
SYNTAX = /(\w+)\s+in\s+(#{QUOTED_FRAGMENT}+)/o.freeze
|
||||
|
||||
attr_reader :variable_name, :collection_name, :attributes
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
if markup =~ Syntax
|
||||
@variable_name = $1
|
||||
@collection_name = Expression.parse($2)
|
||||
if markup =~ SYNTAX
|
||||
@variable_name = Regexp.last_match(1)
|
||||
@collection_name = Expression.parse(Regexp.last_match(2))
|
||||
@attributes = {}
|
||||
markup.scan(TagAttributes) do |key, value|
|
||||
markup.scan(TAG_ATTRIBUTES) do |key, value|
|
||||
@attributes[key] = Expression.parse(value)
|
||||
end
|
||||
else
|
||||
raise SyntaxError.new(options[:locale].t("errors.syntax.table_row".freeze))
|
||||
raise SyntaxError, options[:locale].t('errors.syntax.table_row')
|
||||
end
|
||||
end
|
||||
|
||||
def render(context)
|
||||
collection = context.evaluate(@collection_name) or return ''.freeze
|
||||
def render_to_output_buffer(context, output)
|
||||
(collection = context.evaluate(@collection_name)) || (return '')
|
||||
|
||||
from = @attributes.key?('offset'.freeze) ? context.evaluate(@attributes['offset'.freeze]).to_i : 0
|
||||
to = @attributes.key?('limit'.freeze) ? from + context.evaluate(@attributes['limit'.freeze]).to_i : nil
|
||||
from = @attributes.key?('offset') ? context.evaluate(@attributes['offset']).to_i : 0
|
||||
to = @attributes.key?('limit') ? from + context.evaluate(@attributes['limit']).to_i : nil
|
||||
|
||||
collection = Utils.slice_collection(collection, from, to)
|
||||
|
||||
length = collection.length
|
||||
|
||||
cols = context.evaluate(@attributes['cols'.freeze]).to_i
|
||||
cols = context.evaluate(@attributes['cols']).to_i
|
||||
|
||||
result = "<tr class=\"row1\">\n"
|
||||
output << "<tr class=\"row1\">\n"
|
||||
context.stack do
|
||||
tablerowloop = Liquid::TablerowloopDrop.new(length, cols)
|
||||
context['tablerowloop'.freeze] = tablerowloop
|
||||
context['tablerowloop'] = tablerowloop
|
||||
|
||||
collection.each_with_index do |item, index|
|
||||
collection.each do |item|
|
||||
context[@variable_name] = item
|
||||
|
||||
result << "<td class=\"col#{tablerowloop.col}\">" << super << '</td>'
|
||||
output << "<td class=\"col#{tablerowloop.col}\">"
|
||||
super
|
||||
output << '</td>'
|
||||
|
||||
if tablerowloop.col_last && !tablerowloop.last
|
||||
result << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">"
|
||||
end
|
||||
output << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">" if tablerowloop.col_last && !tablerowloop.last
|
||||
|
||||
tablerowloop.send(:increment!)
|
||||
end
|
||||
end
|
||||
result << "</tr>\n"
|
||||
result
|
||||
|
||||
output << "</tr>\n"
|
||||
output
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
super + @node.attributes.values + [@node.collection_name]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('tablerow'.freeze, TableRow)
|
||||
Template.register_tag('tablerow', TableRow)
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'if'
|
||||
|
||||
module Liquid
|
||||
@@ -6,25 +8,21 @@ module Liquid
|
||||
# {% unless x < 0 %} x is greater than zero {% endunless %}
|
||||
#
|
||||
class Unless < If
|
||||
def render(context)
|
||||
def render_to_output_buffer(context, output)
|
||||
context.stack do
|
||||
# First condition is interpreted backwards ( if not )
|
||||
first_block = @blocks.first
|
||||
unless first_block.evaluate(context)
|
||||
return first_block.attachment.render(context)
|
||||
end
|
||||
return first_block.attachment.render_to_output_buffer(context, output) unless first_block.evaluate(context)
|
||||
|
||||
# After the first condition unless works just like if
|
||||
@blocks[1..-1].each do |block|
|
||||
if block.evaluate(context)
|
||||
return block.attachment.render(context)
|
||||
end
|
||||
return block.attachment.render_to_output_buffer(context, output) if block.evaluate(context)
|
||||
end
|
||||
|
||||
''.freeze
|
||||
end
|
||||
|
||||
output
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('unless'.freeze, Unless)
|
||||
Template.register_tag('unless', Unless)
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# Templates are central to liquid.
|
||||
# Interpretating templates is a two step process. First you compile the
|
||||
@@ -16,11 +18,17 @@ module Liquid
|
||||
attr_accessor :root
|
||||
attr_reader :resource_limits, :warnings
|
||||
|
||||
@@file_system = BlankFileSystem.new
|
||||
@file_system = BlankFileSystem.new
|
||||
|
||||
class << self
|
||||
attr_accessor :file_system
|
||||
end
|
||||
|
||||
class TagRegistry
|
||||
include Enumerable
|
||||
|
||||
def initialize
|
||||
@tags = {}
|
||||
@tags = {}
|
||||
@cache = {}
|
||||
end
|
||||
|
||||
@@ -41,10 +49,14 @@ module Liquid
|
||||
@cache.delete(tag_name)
|
||||
end
|
||||
|
||||
def each(&block)
|
||||
@tags.each(&block)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def lookup_class(name)
|
||||
name.split("::").reject(&:empty?).reduce(Object) { |scope, const| scope.const_get(const) }
|
||||
Object.const_get(name)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -63,12 +75,9 @@ module Liquid
|
||||
# :error raises an error when tainted output is used
|
||||
attr_writer :taint_mode
|
||||
|
||||
def file_system
|
||||
@@file_system
|
||||
end
|
||||
|
||||
def file_system=(obj)
|
||||
@@file_system = obj
|
||||
attr_accessor :default_exception_renderer
|
||||
Template.default_exception_renderer = lambda do |exception|
|
||||
exception
|
||||
end
|
||||
|
||||
def register_tag(name, klass)
|
||||
@@ -80,11 +89,11 @@ module Liquid
|
||||
end
|
||||
|
||||
def error_mode
|
||||
@error_mode || :lax
|
||||
@error_mode ||= :lax
|
||||
end
|
||||
|
||||
def taint_mode
|
||||
@taint_mode || :lax
|
||||
@taint_mode ||= :lax
|
||||
end
|
||||
|
||||
# Pass a module with filter methods which should be available
|
||||
@@ -107,6 +116,7 @@ module Liquid
|
||||
end
|
||||
|
||||
def initialize
|
||||
@rethrow_errors = false
|
||||
@resource_limits = ResourceLimits.new(self.class.default_resource_limits)
|
||||
end
|
||||
|
||||
@@ -153,15 +163,13 @@ module Liquid
|
||||
# filters and tags and might be useful to integrate liquid more with its host application
|
||||
#
|
||||
def render(*args)
|
||||
return ''.freeze if @root.nil?
|
||||
return '' if @root.nil?
|
||||
|
||||
context = case args.first
|
||||
when Liquid::Context
|
||||
c = args.shift
|
||||
|
||||
if @rethrow_errors
|
||||
c.exception_handler = ->(e) { raise }
|
||||
end
|
||||
c.exception_renderer = ->(_e) { raise } if @rethrow_errors
|
||||
|
||||
c
|
||||
when Liquid::Drop
|
||||
@@ -172,21 +180,19 @@ module Liquid
|
||||
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
|
||||
|
||||
case args.last
|
||||
when Hash
|
||||
options = args.pop
|
||||
output = options[:output] if options[:output]
|
||||
|
||||
registers.merge!(options[:registers]) if options[:registers].is_a?(Hash)
|
||||
|
||||
context.add_filters(options[:filters]) if options[:filters]
|
||||
|
||||
context.global_filter = options[:global_filter] if options[:global_filter]
|
||||
|
||||
context.exception_handler = options[:exception_handler] if options[:exception_handler]
|
||||
|
||||
apply_options_to_context(context, options)
|
||||
when Module, Array
|
||||
context.add_filters(args.pop)
|
||||
end
|
||||
@@ -197,10 +203,9 @@ module Liquid
|
||||
begin
|
||||
# render the nodelist.
|
||||
# for performance reasons we get an array back here. join will make a string out of it.
|
||||
result = with_profiling(context) do
|
||||
@root.render(context)
|
||||
with_profiling(context) do
|
||||
@root.render_to_output_buffer(context, output || +'')
|
||||
end
|
||||
result.respond_to?(:join) ? result.join : result
|
||||
rescue Liquid::MemoryError => e
|
||||
context.handle_error(e)
|
||||
ensure
|
||||
@@ -213,6 +218,10 @@ module Liquid
|
||||
render(*args)
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
render(context, output: output)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def tokenize(source)
|
||||
@@ -235,5 +244,13 @@ module Liquid
|
||||
yield
|
||||
end
|
||||
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.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]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Tokenizer
|
||||
attr_reader :line_number
|
||||
|
||||
def initialize(source, line_numbers = false)
|
||||
@source = source
|
||||
@line_number = 1 if line_numbers
|
||||
@line_number = line_numbers ? 1 : nil
|
||||
@tokens = tokenize
|
||||
end
|
||||
|
||||
@@ -20,10 +22,10 @@ module Liquid
|
||||
@source = @source.source if @source.respond_to?(:source)
|
||||
return [] if @source.to_s.empty?
|
||||
|
||||
tokens = @source.split(TemplateParser)
|
||||
tokens = @source.split(TEMPLATE_PARSER)
|
||||
|
||||
# removes the rogue empty element at the beginning of the array
|
||||
tokens.shift if tokens[0] && tokens[0].empty?
|
||||
tokens.shift if tokens[0]&.empty?
|
||||
|
||||
tokens
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
module Utils
|
||||
def self.slice_collection(collection, from, to)
|
||||
@@ -19,13 +21,9 @@ module Liquid
|
||||
return [] unless collection.respond_to?(:each)
|
||||
|
||||
collection.each do |item|
|
||||
if to && to <= index
|
||||
break
|
||||
end
|
||||
break if to && to <= index
|
||||
|
||||
if from <= index
|
||||
segments << item
|
||||
end
|
||||
segments << item if from <= index
|
||||
|
||||
index += 1
|
||||
end
|
||||
@@ -35,24 +33,29 @@ 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
|
||||
|
||||
def self.to_number(obj)
|
||||
case obj
|
||||
when Float
|
||||
BigDecimal.new(obj.to_s)
|
||||
BigDecimal(obj.to_s)
|
||||
when Numeric
|
||||
obj
|
||||
when String
|
||||
(obj.strip =~ /\A\d+\.\d+\z/) ? BigDecimal.new(obj) : obj.to_i
|
||||
obj.strip =~ /\A-?\d+\.\d+\z/ ? BigDecimal(obj) : obj.to_i
|
||||
else
|
||||
0
|
||||
if obj.respond_to?(:to_number)
|
||||
obj.to_number
|
||||
else
|
||||
0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -61,11 +64,12 @@ module Liquid
|
||||
|
||||
if obj.is_a?(String)
|
||||
return nil if obj.empty?
|
||||
|
||||
obj = obj.downcase
|
||||
end
|
||||
|
||||
case obj
|
||||
when 'now'.freeze, 'today'.freeze
|
||||
when 'now', 'today'
|
||||
Time.now
|
||||
when /\A\d+\z/, Integer
|
||||
Time.at(obj.to_i)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# Holds variables. Variables are only loaded "just in time"
|
||||
# and are not evaluated as part of the render stage
|
||||
@@ -10,10 +12,16 @@ module Liquid
|
||||
# {{ user | link }}
|
||||
#
|
||||
class Variable
|
||||
FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o
|
||||
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
|
||||
|
||||
attr_accessor :filters, :name, :line_number
|
||||
attr_reader :parse_context
|
||||
alias_method :options, :parse_context
|
||||
|
||||
include ParserSwitching
|
||||
|
||||
def initialize(markup, parse_context)
|
||||
@@ -35,17 +43,18 @@ module Liquid
|
||||
|
||||
def lax_parse(markup)
|
||||
@filters = []
|
||||
return unless markup =~ /(#{QuotedFragment})(.*)/om
|
||||
return unless markup =~ MARKUP_WITH_QUOTED_FRAGMENT
|
||||
|
||||
name_markup = $1
|
||||
filter_markup = $2
|
||||
name_markup = Regexp.last_match(1)
|
||||
filter_markup = Regexp.last_match(2)
|
||||
@name = Expression.parse(name_markup)
|
||||
if filter_markup =~ /#{FilterSeparator}\s*(.*)/om
|
||||
filters = $1.scan(FilterParser)
|
||||
if filter_markup =~ FILTER_MARKUP_REGEX
|
||||
filters = Regexp.last_match(1).scan(FILTER_PARSER)
|
||||
filters.each do |f|
|
||||
next unless f =~ /\w+/
|
||||
|
||||
filtername = Regexp.last_match(0)
|
||||
filterargs = f.scan(/(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o).flatten
|
||||
filterargs = f.scan(FILTER_ARGS_REGEX).flatten
|
||||
@filters << parse_filter_expressions(filtername, filterargs)
|
||||
end
|
||||
end
|
||||
@@ -79,31 +88,43 @@ module Liquid
|
||||
end
|
||||
|
||||
obj = context.apply_global_filter(obj)
|
||||
|
||||
taint_check(context, obj)
|
||||
|
||||
obj
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
obj = render(context)
|
||||
|
||||
if obj.is_a?(Array)
|
||||
output << obj.join
|
||||
elsif obj.nil?
|
||||
else
|
||||
output << obj.to_s
|
||||
end
|
||||
|
||||
output
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse_filter_expressions(filter_name, unparsed_args)
|
||||
filter_args = []
|
||||
keyword_args = {}
|
||||
keyword_args = nil
|
||||
unparsed_args.each do |a|
|
||||
if matches = a.match(/\A#{TagAttributes}\z/o)
|
||||
if (matches = a.match(JUST_TAG_ATTRIBUTES))
|
||||
keyword_args ||= {}
|
||||
keyword_args[matches[1]] = Expression.parse(matches[2])
|
||||
else
|
||||
filter_args << Expression.parse(a)
|
||||
end
|
||||
end
|
||||
result = [filter_name, filter_args]
|
||||
result << keyword_args unless keyword_args.empty?
|
||||
result << keyword_args if keyword_args
|
||||
result
|
||||
end
|
||||
|
||||
def evaluate_filter_expressions(context, filter_args, filter_kwargs)
|
||||
parsed_args = filter_args.map{ |expr| context.evaluate(expr) }
|
||||
parsed_args = filter_args.map { |expr| context.evaluate(expr) }
|
||||
if filter_kwargs
|
||||
parsed_kwargs = {}
|
||||
filter_kwargs.each do |key, expr|
|
||||
@@ -118,7 +139,7 @@ module Liquid
|
||||
return unless obj.tainted?
|
||||
return if Template.taint_mode == :lax
|
||||
|
||||
@markup =~ QuotedFragment
|
||||
@markup =~ QUOTED_FRAGMENT
|
||||
name = Regexp.last_match(0)
|
||||
|
||||
error = TaintedError.new("variable '#{name}' is tainted and was not escaped")
|
||||
@@ -132,5 +153,11 @@ module Liquid
|
||||
raise error
|
||||
end
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
[@node.name] + @node.filters.flatten
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class VariableLookup
|
||||
SQUARE_BRACKETED = /\A\[(.*)\]\z/m
|
||||
COMMAND_METHODS = ['size'.freeze, 'first'.freeze, 'last'.freeze]
|
||||
SQUARE_BRACKETED = /\A\[(.*)\]\z/m.freeze
|
||||
COMMAND_METHODS = %w[size first last].freeze
|
||||
|
||||
attr_reader :name, :lookups
|
||||
|
||||
@@ -10,12 +12,10 @@ module Liquid
|
||||
end
|
||||
|
||||
def initialize(markup)
|
||||
lookups = markup.scan(VariableParser)
|
||||
lookups = markup.scan(VARIABLE_PARSER)
|
||||
|
||||
name = lookups.shift
|
||||
if name =~ SQUARE_BRACKETED
|
||||
name = Expression.parse($1)
|
||||
end
|
||||
name = Expression.parse(Regexp.last_match(1)) if name =~ SQUARE_BRACKETED
|
||||
@name = name
|
||||
|
||||
@lookups = lookups
|
||||
@@ -24,7 +24,7 @@ module Liquid
|
||||
@lookups.each_index do |i|
|
||||
lookup = lookups[i]
|
||||
if lookup =~ SQUARE_BRACKETED
|
||||
lookups[i] = Expression.parse($1)
|
||||
lookups[i] = Expression.parse(Regexp.last_match(1))
|
||||
elsif COMMAND_METHODS.include?(lookup)
|
||||
@command_flags |= 1 << i
|
||||
end
|
||||
@@ -41,8 +41,8 @@ module Liquid
|
||||
# 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)
|
||||
@@ -55,9 +55,12 @@ module Liquid
|
||||
object = object.send(key).to_liquid
|
||||
|
||||
# No key was present with the desired value and it wasn't one of the directly supported
|
||||
# keywords either. The only thing we got left is to return nil
|
||||
# keywords either. The only thing we got left is to return nil or
|
||||
# raise an exception if `strict_variables` option is set to true
|
||||
else
|
||||
return nil
|
||||
return nil unless context.strict_variables
|
||||
|
||||
raise Liquid::UndefinedVariable, "undefined variable #{key}"
|
||||
end
|
||||
|
||||
# If we are dealing with a drop here we have to
|
||||
@@ -76,5 +79,11 @@ module Liquid
|
||||
def state
|
||||
[@name, @lookups, @command_flags]
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
@node.lookups
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# encoding: utf-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
VERSION = "4.0.0.rc1"
|
||||
VERSION = '4.0.3'
|
||||
end
|
||||
|
||||
@@ -1,29 +1,31 @@
|
||||
# encoding: utf-8
|
||||
lib = File.expand_path('../lib/', __FILE__)
|
||||
$LOAD_PATH.unshift lib unless $LOAD_PATH.include?(lib)
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "liquid/version"
|
||||
lib = File.expand_path('lib', __dir__)
|
||||
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
||||
|
||||
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_rubygems_version = ">= 1.3.7"
|
||||
s.required_ruby_version = '>= 2.4.0'
|
||||
s.required_rubygems_version = '>= 1.3.7'
|
||||
|
||||
s.test_files = Dir.glob("{test}/**/*")
|
||||
s.files = Dir.glob("{lib}/**/*") + %w(LICENSE README.md)
|
||||
s.test_files = Dir.glob('{test}/**/*')
|
||||
s.files = Dir.glob('{lib}/**/*') + %w[LICENSE README.md]
|
||||
|
||||
s.extra_rdoc_files = ["History.md", "README.md"]
|
||||
s.extra_rdoc_files = ['History.md', 'README.md']
|
||||
|
||||
s.require_path = "lib"
|
||||
s.require_path = 'lib'
|
||||
|
||||
s.add_development_dependency 'rake'
|
||||
s.add_development_dependency 'minitest'
|
||||
s.add_development_dependency('minitest')
|
||||
s.add_development_dependency('rake', '~> 11.3')
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'benchmark/ips'
|
||||
require_relative 'theme_runner'
|
||||
|
||||
@@ -5,13 +7,14 @@ Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
|
||||
profiler = ThemeRunner.new
|
||||
|
||||
Benchmark.ips do |x|
|
||||
x.time = 60
|
||||
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("parse & run:") { profiler.run }
|
||||
x.report('parse:') { profiler.compile }
|
||||
x.report('render:') { profiler.render }
|
||||
x.report('parse & render:') { profiler.run }
|
||||
end
|
||||
|
||||
26
performance/memory_profile.rb
Normal file
26
performance/memory_profile.rb
Normal file
@@ -0,0 +1,26 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'benchmark/ips'
|
||||
require 'memory_profiler'
|
||||
require_relative 'theme_runner'
|
||||
|
||||
def profile(phase, &block)
|
||||
puts
|
||||
puts "#{phase}:"
|
||||
puts
|
||||
|
||||
report = MemoryProfiler.report(&block)
|
||||
|
||||
report.pretty_print(
|
||||
color_output: true,
|
||||
scale_bytes: true,
|
||||
detailed_report: true
|
||||
)
|
||||
end
|
||||
|
||||
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
|
||||
|
||||
profiler = ThemeRunner.new
|
||||
|
||||
profile('Parsing') { profiler.compile }
|
||||
profile('Rendering') { profiler.render }
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'stackprof'
|
||||
require_relative 'theme_runner'
|
||||
|
||||
@@ -5,7 +7,7 @@ Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
|
||||
profiler = ThemeRunner.new
|
||||
profiler.run
|
||||
|
||||
[:cpu, :object].each do |profile_type|
|
||||
%i[cpu object].each do |profile_type|
|
||||
puts "Profiling in #{profile_type} mode..."
|
||||
results = StackProf.run(mode: profile_type) do
|
||||
200.times do
|
||||
@@ -13,12 +15,12 @@ profiler.run
|
||||
end
|
||||
end
|
||||
|
||||
if profile_type == :cpu && graph_filename = ENV['GRAPH_FILENAME']
|
||||
if profile_type == :cpu && (graph_filename = ENV['GRAPH_FILENAME'])
|
||||
File.open(graph_filename, 'w') do |f|
|
||||
StackProf::Report.new(results).print_graphviz(nil, f)
|
||||
end
|
||||
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,18 +1,20 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class CommentForm < Liquid::Block
|
||||
Syntax = /(#{Liquid::VariableSignature}+)/
|
||||
SYNTAX = /(#{Liquid::VARIABLE_SIGNATURE}+)/.freeze
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
|
||||
if markup =~ Syntax
|
||||
@variable_name = $1
|
||||
if markup =~ SYNTAX
|
||||
@variable_name = Regexp.last_match(1)
|
||||
@attributes = {}
|
||||
else
|
||||
raise SyntaxError.new("Syntax Error in 'comment_form' - Valid syntax: comment_form [article]")
|
||||
raise SyntaxError, "Syntax Error in 'comment_form' - Valid syntax: comment_form [article]"
|
||||
end
|
||||
end
|
||||
|
||||
def render(context)
|
||||
def render_to_output_buffer(context, output)
|
||||
article = context[@variable_name]
|
||||
|
||||
context.stack do
|
||||
@@ -20,10 +22,12 @@ class CommentForm < Liquid::Block
|
||||
'posted_successfully?' => context.registers[:posted_successfully],
|
||||
'errors' => context['comment.errors'],
|
||||
'author' => context['comment.author'],
|
||||
'email' => context['comment.email'],
|
||||
'body' => context['comment.body']
|
||||
'email' => context['comment.email'],
|
||||
'body' => context['comment.body'],
|
||||
}
|
||||
wrap_in_form(article, render_all(@nodelist, context))
|
||||
|
||||
output << wrap_in_form(article, render_all(@nodelist, context, output))
|
||||
output
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'yaml'
|
||||
|
||||
module Database
|
||||
@@ -16,8 +18,11 @@ module Database
|
||||
end
|
||||
|
||||
# key the tables by handles, as this is how liquid expects it.
|
||||
db = db.inject({}) do |assigns, (key, values)|
|
||||
assigns[key] = values.inject({}) { |h, v| h[v['handle']] = v; h; }
|
||||
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
|
||||
|
||||
@@ -29,9 +34,9 @@ module Database
|
||||
db['article'] = db['blog']['articles'].first
|
||||
|
||||
db['cart'] = {
|
||||
'total_price' => db['line_items'].values.inject(0) { |sum, item| sum += item['line_price'] * item['quantity'] },
|
||||
'item_count' => db['line_items'].values.inject(0) { |sum, item| sum += item['quantity'] },
|
||||
'items' => db['line_items'].values
|
||||
'total_price' => db['line_items'].values.inject(0) { |sum, item| sum + item['line_price'] * item['quantity'] },
|
||||
'item_count' => db['line_items'].values.inject(0) { |sum, item| sum + item['quantity'] },
|
||||
'items' => db['line_items'].values,
|
||||
}
|
||||
|
||||
db
|
||||
@@ -40,6 +45,6 @@ module Database
|
||||
end
|
||||
|
||||
if __FILE__ == $PROGRAM_NAME
|
||||
p Database.tables['collections']['frontpage'].keys
|
||||
p(Database.tables['collections']['frontpage'].keys)
|
||||
# p Database.tables['blog']['articles']
|
||||
end
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
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
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
$:.unshift __dir__ + '/../../lib'
|
||||
# frozen_string_literal: true
|
||||
|
||||
$LOAD_PATH.unshift(__dir__ + '/../../lib')
|
||||
require_relative '../../lib/liquid'
|
||||
|
||||
require_relative 'comment_form'
|
||||
@@ -9,11 +11,11 @@ require_relative 'shop_filter'
|
||||
require_relative 'tag_filter'
|
||||
require_relative 'weight_filter'
|
||||
|
||||
Liquid::Template.register_tag 'paginate', Paginate
|
||||
Liquid::Template.register_tag 'form', CommentForm
|
||||
Liquid::Template.register_tag('paginate', Paginate)
|
||||
Liquid::Template.register_tag('form', CommentForm)
|
||||
|
||||
Liquid::Template.register_filter JsonFilter
|
||||
Liquid::Template.register_filter MoneyFilter
|
||||
Liquid::Template.register_filter WeightFilter
|
||||
Liquid::Template.register_filter ShopFilter
|
||||
Liquid::Template.register_filter TagFilter
|
||||
Liquid::Template.register_filter(JsonFilter)
|
||||
Liquid::Template.register_filter(MoneyFilter)
|
||||
Liquid::Template.register_filter(WeightFilter)
|
||||
Liquid::Template.register_filter(ShopFilter)
|
||||
Liquid::Template.register_filter(TagFilter)
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module MoneyFilter
|
||||
def money_with_currency(money)
|
||||
return '' if money.nil?
|
||||
sprintf("$ %.2f USD", money / 100.0)
|
||||
|
||||
format('$ %.2f USD', money / 100.0)
|
||||
end
|
||||
|
||||
def money(money)
|
||||
return '' if money.nil?
|
||||
sprintf("$ %.2f", money / 100.0)
|
||||
|
||||
format('$ %.2f', money / 100.0)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -1,49 +1,51 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Paginate < Liquid::Block
|
||||
Syntax = /(#{Liquid::QuotedFragment})\s*(by\s*(\d+))?/
|
||||
SYNTAX = /(#{Liquid::QUOTED_FRAGMENT})\s*(by\s*(\d+))?/.freeze
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
|
||||
if markup =~ Syntax
|
||||
@collection_name = $1
|
||||
@page_size = if $2
|
||||
$3.to_i
|
||||
if markup =~ SYNTAX
|
||||
@collection_name = Regexp.last_match(1)
|
||||
@page_size = if Regexp.last_match(2)
|
||||
Regexp.last_match(3).to_i
|
||||
else
|
||||
20
|
||||
end
|
||||
|
||||
@attributes = { 'window_size' => 3 }
|
||||
markup.scan(Liquid::TagAttributes) do |key, value|
|
||||
markup.scan(Liquid::TAG_ATTRIBUTES) do |key, value|
|
||||
@attributes[key] = value
|
||||
end
|
||||
else
|
||||
raise SyntaxError.new("Syntax Error in tag 'paginate' - Valid syntax: paginate [collection] by number")
|
||||
raise SyntaxError, "Syntax Error in tag 'paginate' - Valid syntax: paginate [collection] by number"
|
||||
end
|
||||
end
|
||||
|
||||
def render(context)
|
||||
def render_to_output_buffer(context, output)
|
||||
@context = context
|
||||
|
||||
context.stack do
|
||||
current_page = context['current_page'].to_i
|
||||
current_page = context['current_page'].to_i
|
||||
|
||||
pagination = {
|
||||
'page_size' => @page_size,
|
||||
'current_page' => 5,
|
||||
'current_offset' => @page_size * 5
|
||||
'page_size' => @page_size,
|
||||
'current_page' => 5,
|
||||
'current_offset' => @page_size * 5,
|
||||
}
|
||||
|
||||
context['paginate'] = pagination
|
||||
|
||||
collection_size = context[@collection_name].size
|
||||
collection_size = context[@collection_name].size
|
||||
|
||||
raise ArgumentError.new("Cannot paginate array '#{@collection_name}'. Not found.") if collection_size.nil?
|
||||
raise ArgumentError, "Cannot paginate array '#{@collection_name}'. Not found." if collection_size.nil?
|
||||
|
||||
page_count = (collection_size.to_f / @page_size.to_f).to_f.ceil + 1
|
||||
|
||||
pagination['items'] = collection_size
|
||||
pagination['pages'] = page_count - 1
|
||||
pagination['previous'] = link('« Previous', current_page - 1) unless 1 >= current_page
|
||||
pagination['previous'] = link('« Previous', current_page - 1) unless current_page <= 1
|
||||
pagination['next'] = link('Next »', current_page + 1) unless page_count <= current_page + 1
|
||||
pagination['parts'] = []
|
||||
|
||||
@@ -59,6 +61,7 @@ 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
|
||||
@@ -85,6 +88,6 @@ class Paginate < Liquid::Block
|
||||
end
|
||||
|
||||
def current_url
|
||||
"/collections/frontpage"
|
||||
'/collections/frontpage'
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ShopFilter
|
||||
def asset_url(input)
|
||||
"/files/1/[shop_id]/[shop_id]/assets/#{input}"
|
||||
@@ -15,15 +17,15 @@ 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
|
||||
|
||||
@@ -52,15 +54,13 @@ module ShopFilter
|
||||
end
|
||||
|
||||
def product_img_url(url, style = 'small')
|
||||
unless url =~ /\Aproducts\/([\w\-\_]+)\.(\w{2,4})/
|
||||
raise ArgumentError, 'filter "size" can only be called on product images'
|
||||
end
|
||||
raise ArgumentError, 'filter "size" can only be called on product images' unless url =~ %r{\Aproducts/([\w\-\_]+)\.(\w{2,4})}
|
||||
|
||||
case style
|
||||
when 'original'
|
||||
return '/files/shops/random_number/' + url
|
||||
when 'grande', 'large', 'medium', 'compact', 'small', 'thumb', 'icon'
|
||||
"/files/shops/random_number/products/#{$1}_#{style}.#{$2}"
|
||||
"/files/shops/random_number/products/#{Regexp.last_match(1)}_#{style}.#{Regexp.last_match(2)}"
|
||||
else
|
||||
raise ArgumentError, 'valid parameters for filter "size" are: original, grande, large, medium, compact, small, thumb and icon '
|
||||
end
|
||||
@@ -70,16 +70,14 @@ module ShopFilter
|
||||
html = []
|
||||
html << %(<span class="prev">#{link_to(paginate['previous']['title'], paginate['previous']['url'])}</span>) if paginate['previous']
|
||||
|
||||
for part in paginate['parts']
|
||||
|
||||
if part['is_link']
|
||||
html << %(<span class="page">#{link_to(part['title'], part['url'])}</span>)
|
||||
paginate['parts'].each do |part|
|
||||
html << if part['is_link']
|
||||
%(<span class="page">#{link_to(part['title'], part['url'])}</span>)
|
||||
elsif part['title'].to_i == paginate['current_page'].to_i
|
||||
html << %(<span class="page current">#{part['title']}</span>)
|
||||
%(<span class="page current">#{part['title']}</span>)
|
||||
else
|
||||
html << %(<span class="deco">#{part['title']}</span>)
|
||||
%(<span class="deco">#{part['title']}</span>)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
html << %(<span class="next">#{link_to(paginate['next']['title'], paginate['next']['url'])}</span>) if paginate['next']
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module TagFilter
|
||||
def link_to_tag(label, tag)
|
||||
"<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tag}\">#{label}</a>"
|
||||
@@ -13,11 +15,11 @@ module TagFilter
|
||||
|
||||
def link_to_add_tag(label, tag)
|
||||
tags = (@context['current_tags'] + [tag]).uniq
|
||||
"<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tags.join("+")}\">#{label}</a>"
|
||||
"<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tags.join('+')}\">#{label}</a>"
|
||||
end
|
||||
|
||||
def link_to_remove_tag(label, tag)
|
||||
tags = (@context['current_tags'] - [tag]).uniq
|
||||
"<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tags.join("+")}\">#{label}</a>"
|
||||
"<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tags.join('+')}\">#{label}</a>"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WeightFilter
|
||||
def weight(grams)
|
||||
sprintf("%.2f", grams / 1000)
|
||||
format('%.2f', grams / 1000)
|
||||
end
|
||||
|
||||
def weight_with_unit(grams)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# This profiler run simulates Shopify.
|
||||
# We are looking in the tests directory for liquid files and render them within the designated layout file.
|
||||
# We will also export a substantial database to liquid which the templates can render values of.
|
||||
@@ -21,53 +23,100 @@ class ThemeRunner
|
||||
end
|
||||
end
|
||||
|
||||
# Load all templates into memory, do this now so that
|
||||
# we don't profile IO.
|
||||
# Initialize a new liquid ThemeRunner instance
|
||||
# Will load all templates into memory, do this now so that we don't profile IO.
|
||||
def initialize
|
||||
@tests = Dir[__dir__ + '/tests/**/*.liquid'].collect do |test|
|
||||
next if File.basename(test) == 'theme.liquid'
|
||||
|
||||
theme_path = File.dirname(test) + '/theme.liquid'
|
||||
|
||||
[File.read(test), (File.file?(theme_path) ? File.read(theme_path) : nil), test]
|
||||
{
|
||||
liquid: File.read(test),
|
||||
layout: (File.file?(theme_path) ? File.read(theme_path) : nil),
|
||||
template_name: test,
|
||||
}
|
||||
end.compact
|
||||
|
||||
compile_all_tests
|
||||
end
|
||||
|
||||
# `compile` will test just the compilation portion of liquid without any templates
|
||||
def compile
|
||||
# Dup assigns because will make some changes to them
|
||||
|
||||
@tests.each do |liquid, layout, template_name|
|
||||
tmpl = Liquid::Template.new
|
||||
tmpl.parse(liquid)
|
||||
tmpl = Liquid::Template.new
|
||||
tmpl.parse(layout)
|
||||
@tests.each do |test_hash|
|
||||
Liquid::Template.new.parse(test_hash[:liquid])
|
||||
Liquid::Template.new.parse(test_hash[:layout])
|
||||
end
|
||||
end
|
||||
|
||||
# `run` is called to benchmark rendering and compiling at the same time
|
||||
def run
|
||||
# Dup assigns because will make some changes to them
|
||||
assigns = Database.tables.dup
|
||||
|
||||
@tests.each do |liquid, layout, template_name|
|
||||
# Compute page_tempalte outside of profiler run, uninteresting to profiler
|
||||
page_template = File.basename(template_name, File.extname(template_name))
|
||||
each_test do |liquid, layout, assigns, page_template, template_name|
|
||||
compile_and_render(liquid, layout, assigns, page_template, template_name)
|
||||
end
|
||||
end
|
||||
|
||||
# `render` is called to benchmark just the render portion of liquid
|
||||
def render
|
||||
@compiled_tests.each do |test|
|
||||
tmpl = test[:tmpl]
|
||||
assigns = test[:assigns]
|
||||
layout = test[:layout]
|
||||
|
||||
if layout
|
||||
assigns['content_for_layout'] = tmpl.render!(assigns)
|
||||
layout.render!(assigns)
|
||||
else
|
||||
tmpl.render!(assigns)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
def compile_all_tests
|
||||
@compiled_tests = []
|
||||
each_test do |liquid, layout, assigns, page_template, template_name|
|
||||
@compiled_tests << compile_test(liquid, layout, assigns, page_template, template_name)
|
||||
end
|
||||
@compiled_tests
|
||||
end
|
||||
|
||||
def compile_test(template, layout, assigns, page_template, template_file)
|
||||
tmpl = init_template(page_template, template_file)
|
||||
parsed_template = tmpl.parse(template).dup
|
||||
|
||||
if layout
|
||||
parsed_layout = tmpl.parse(layout)
|
||||
{ tmpl: parsed_template, assigns: assigns, layout: parsed_layout }
|
||||
else
|
||||
{ tmpl: parsed_template, assigns: assigns }
|
||||
end
|
||||
end
|
||||
|
||||
# utility method with similar functionality needed in `compile_all_tests` and `run`
|
||||
def each_test
|
||||
# Dup assigns because will make some changes to them
|
||||
assigns = Database.tables.dup
|
||||
|
||||
@tests.each do |test_hash|
|
||||
# Compute page_template outside of profiler run, uninteresting to profiler
|
||||
page_template = File.basename(test_hash[:template_name], File.extname(test_hash[:template_name]))
|
||||
yield(test_hash[:liquid], test_hash[:layout], assigns, page_template, test_hash[:template_name])
|
||||
end
|
||||
end
|
||||
|
||||
# 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.registers[:file_system] = ThemeRunner::FileSystem.new(File.dirname(template_file))
|
||||
|
||||
content_for_layout = tmpl.parse(template).render!(assigns)
|
||||
|
||||
if layout
|
||||
assigns['content_for_layout'] = content_for_layout
|
||||
tmpl.parse(layout).render!(assigns)
|
||||
else
|
||||
content_for_layout
|
||||
end
|
||||
tmpl
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class AssignTest < Minitest::Test
|
||||
@@ -10,29 +12,29 @@ class AssignTest < Minitest::Test
|
||||
END_TEMPLATE
|
||||
template = Template.parse(template_source)
|
||||
rendered = template.render!
|
||||
assert_equal "Print this-thing", rendered.strip
|
||||
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
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class FoobarTag < Liquid::Tag
|
||||
def render(*args)
|
||||
" "
|
||||
def render_to_output_buffer(_context, output)
|
||||
output << ' '
|
||||
output
|
||||
end
|
||||
|
||||
Liquid::Template.register_tag('foobar', FoobarTag)
|
||||
end
|
||||
|
||||
class BlankTestFileSystem
|
||||
@@ -31,76 +32,78 @@ class BlankTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_new_tags_are_not_blank_by_default
|
||||
assert_template_result(" " * N, wrap_in_for("{% foobar %}"))
|
||||
with_custom_tag('foobar', FoobarTag) do
|
||||
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
|
||||
|
||||
14
test/integration/block_test.rb
Normal file
14
test/integration/block_test.rb
Normal file
@@ -0,0 +1,14 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class BlockTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def test_unexpected_end_tag
|
||||
exc = assert_raises(SyntaxError) do
|
||||
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
|
||||
end
|
||||
@@ -1,10 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
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,7 +16,7 @@ class CaptureTest < Minitest::Test
|
||||
END_TEMPLATE
|
||||
template = Template.parse(template_source)
|
||||
rendered = template.render!
|
||||
assert_equal "Print this-thing", rendered.strip
|
||||
assert_equal 'Print this-thing', rendered.strip
|
||||
end
|
||||
|
||||
def test_capture_to_variable_from_outer_scope_if_existing
|
||||
@@ -30,7 +32,7 @@ class CaptureTest < Minitest::Test
|
||||
END_TEMPLATE
|
||||
template = Template.parse(template_source)
|
||||
rendered = template.render!
|
||||
assert_equal "test-string", rendered.gsub(/\s/, '')
|
||||
assert_equal 'test-string', rendered.gsub(/\s/, '')
|
||||
end
|
||||
|
||||
def test_assigning_from_capture
|
||||
@@ -45,6 +47,6 @@ class CaptureTest < Minitest::Test
|
||||
END_TEMPLATE
|
||||
template = Template.parse(template_source)
|
||||
rendered = template.render!
|
||||
assert_equal "3-3", rendered.gsub(/\s/, '')
|
||||
assert_equal '3-3', rendered.gsub(/\s/, '')
|
||||
end
|
||||
end # CaptureTest
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class ContextTest < Minitest::Test
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class DocumentTest < Minitest::Test
|
||||
@@ -5,14 +7,14 @@ 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"
|
||||
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'"
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class ContextDrop < Liquid::Drop
|
||||
@@ -21,7 +23,7 @@ end
|
||||
class ProductDrop < Liquid::Drop
|
||||
class TextDrop < Liquid::Drop
|
||||
def array
|
||||
['text1', 'text2']
|
||||
%w[text1 text2]
|
||||
end
|
||||
|
||||
def text
|
||||
@@ -31,7 +33,7 @@ class ProductDrop < Liquid::Drop
|
||||
|
||||
class CatchallDrop < Liquid::Drop
|
||||
def liquid_method_missing(method)
|
||||
'catchall_method: ' << method.to_s
|
||||
+'catchall_method: ' << method.to_s
|
||||
end
|
||||
end
|
||||
|
||||
@@ -48,13 +50,13 @@ class ProductDrop < Liquid::Drop
|
||||
end
|
||||
|
||||
def user_input
|
||||
"foo".taint
|
||||
(+'foo').taint
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def callmenot
|
||||
"protected"
|
||||
'protected'
|
||||
end
|
||||
end
|
||||
|
||||
@@ -139,17 +141,17 @@ class DropsTest < Minitest::Test
|
||||
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
|
||||
@@ -173,12 +175,12 @@ class DropsTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_context_drop
|
||||
output = Liquid::Template.parse(' {{ context.bar }} ').render!('context' => ContextDrop.new, 'bar' => "carrot")
|
||||
output = Liquid::Template.parse(' {{ context.bar }} ').render!('context' => ContextDrop.new, 'bar' => 'carrot')
|
||||
assert_equal ' carrot ', output
|
||||
end
|
||||
|
||||
def test_nested_context_drop
|
||||
output = Liquid::Template.parse(' {{ product.context.foo }} ').render!('product' => ProductDrop.new, 'foo' => "monkey")
|
||||
output = Liquid::Template.parse(' {{ product.context.foo }} ').render!('product' => ProductDrop.new, 'foo' => 'monkey')
|
||||
assert_equal ' monkey ', output
|
||||
end
|
||||
|
||||
@@ -188,7 +190,7 @@ class DropsTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_object_methods_not_allowed
|
||||
[:dup, :clone, :singleton_class, :eval, :class_eval, :inspect].each do |method|
|
||||
%i[dup clone singleton_class eval class_eval inspect].each do |method|
|
||||
output = Liquid::Template.parse(" {{ product.#{method} }} ").render!('product' => ProductDrop.new)
|
||||
assert_equal ' ', output
|
||||
end
|
||||
@@ -201,9 +203,9 @@ class DropsTest < Minitest::Test
|
||||
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
|
||||
@@ -232,7 +234,7 @@ class DropsTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_enumerable_drop_will_invoke_liquid_method_missing_for_clashing_method_names
|
||||
["select", "each", "map", "cycle"].each do |method|
|
||||
%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)
|
||||
@@ -241,20 +243,20 @@ class DropsTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_some_enumerable_methods_still_get_invoked
|
||||
[ :count, :max ].each do |method|
|
||||
assert_equal "3", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new)
|
||||
assert_equal "3", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new)
|
||||
assert_equal "3", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new)
|
||||
assert_equal "3", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new)
|
||||
%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)
|
||||
end
|
||||
|
||||
assert_equal "yes", Liquid::Template.parse("{% if collection contains 3 %}yes{% endif %}").render!('collection' => RealEnumerableDrop.new)
|
||||
assert_equal 'yes', Liquid::Template.parse('{% if collection contains 3 %}yes{% endif %}').render!('collection' => RealEnumerableDrop.new)
|
||||
|
||||
[ :min, :first ].each do |method|
|
||||
assert_equal "1", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new)
|
||||
assert_equal "1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new)
|
||||
assert_equal "1", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new)
|
||||
assert_equal "1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new)
|
||||
%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)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -267,7 +269,7 @@ class DropsTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_default_to_s_on_drops
|
||||
assert_equal 'ProductDrop', Liquid::Template.parse("{{ product }}").render!('product' => ProductDrop.new)
|
||||
assert_equal 'ProductDrop', Liquid::Template.parse('{{ product }}').render!('product' => ProductDrop.new)
|
||||
assert_equal 'EnumerableDrop', Liquid::Template.parse('{{ collection }}').render!('collection' => EnumerableDrop.new)
|
||||
end
|
||||
end # DropsTest
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class ErrorHandlingTest < Minitest::Test
|
||||
@@ -83,18 +85,32 @@ class ErrorHandlingTest < Minitest::Test
|
||||
|
||||
def test_with_line_numbers_adds_numbers_to_parser_errors
|
||||
err = assert_raises(SyntaxError) do
|
||||
Liquid::Template.parse(%q(
|
||||
Liquid::Template.parse('
|
||||
foobar
|
||||
|
||||
{% "cat" | foobar %}
|
||||
|
||||
bla
|
||||
),
|
||||
line_numbers: true
|
||||
)
|
||||
',
|
||||
line_numbers: true)
|
||||
end
|
||||
|
||||
assert_match /Liquid syntax error \(line 4\)/, err.message
|
||||
assert_match(/Liquid syntax error \(line 4\)/, err.message)
|
||||
end
|
||||
|
||||
def test_with_line_numbers_adds_numbers_to_parser_errors_with_whitespace_trim
|
||||
err = assert_raises(SyntaxError) do
|
||||
Liquid::Template.parse('
|
||||
foobar
|
||||
|
||||
{%- "cat" | foobar -%}
|
||||
|
||||
bla
|
||||
',
|
||||
line_numbers: true)
|
||||
end
|
||||
|
||||
assert_match(/Liquid syntax error \(line 4\)/, err.message)
|
||||
end
|
||||
|
||||
def test_parsing_warn_with_line_numbers_adds_numbers_to_lexer_errors
|
||||
@@ -105,12 +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)
|
||||
template.warnings.map(&:message)
|
||||
end
|
||||
|
||||
def test_parsing_strict_with_line_numbers_adds_numbers_to_lexer_errors
|
||||
@@ -122,9 +137,8 @@ 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
|
||||
@@ -141,8 +155,7 @@ class ErrorHandlingTest < Minitest::Test
|
||||
|
||||
bla
|
||||
',
|
||||
line_numbers: true
|
||||
)
|
||||
line_numbers: true)
|
||||
end
|
||||
|
||||
assert_equal "Liquid syntax error (line 5): Unknown tag 'foo'", err.message
|
||||
@@ -186,27 +199,51 @@ class ErrorHandlingTest < Minitest::Test
|
||||
end
|
||||
end
|
||||
|
||||
def test_exception_handler_with_string_result
|
||||
template = Liquid::Template.parse('This is an argument error: {{ errors.argument_error }}')
|
||||
output = template.render({ 'errors' => ErrorDrop.new }, exception_handler: ->(e) { '' })
|
||||
assert_equal 'This is an argument error: ', output
|
||||
assert_equal [ArgumentError], template.errors.map(&:class)
|
||||
end
|
||||
|
||||
class InternalError < Liquid::Error
|
||||
end
|
||||
|
||||
def test_exception_handler_with_exception_result
|
||||
def test_default_exception_renderer_with_internal_error
|
||||
template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true)
|
||||
handler = ->(e) { e.is_a?(Liquid::Error) ? e : InternalError.new('internal') }
|
||||
output = template.render({ 'errors' => ErrorDrop.new }, exception_handler: handler)
|
||||
|
||||
output = template.render('errors' => ErrorDrop.new)
|
||||
|
||||
assert_equal 'This is a runtime error: Liquid error (line 1): internal', output
|
||||
assert_equal [InternalError], template.errors.map(&:class)
|
||||
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|
|
||||
exceptions << e
|
||||
''
|
||||
}
|
||||
template = Liquid::Template.parse('This is a runtime error: {{ errors.argument_error }}')
|
||||
|
||||
output = template.render('errors' => ErrorDrop.new)
|
||||
|
||||
assert_equal 'This is a runtime error: ', output
|
||||
assert_equal [Liquid::ArgumentError], template.errors.map(&:class)
|
||||
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)
|
||||
exceptions = []
|
||||
handler = lambda { |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
|
||||
end
|
||||
|
||||
class TestFileSystem
|
||||
def read_template_file(template_path)
|
||||
"{{ errors.argument_error }}"
|
||||
def read_template_file(_template_path)
|
||||
'{{ errors.argument_error }}'
|
||||
end
|
||||
end
|
||||
|
||||
@@ -221,6 +258,6 @@ class ErrorHandlingTest < Minitest::Test
|
||||
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 'product', template.errors.first.template_name
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
module MoneyFilter
|
||||
def money(input)
|
||||
sprintf(' %d$ ', input)
|
||||
format(' %d$ ', input)
|
||||
end
|
||||
|
||||
def money_with_underscore(input)
|
||||
sprintf(' %d$ ', input)
|
||||
format(' %d$ ', input)
|
||||
end
|
||||
end
|
||||
|
||||
module CanadianMoneyFilter
|
||||
def money(input)
|
||||
sprintf(' %d$ CAD ', input)
|
||||
format(' %d$ CAD ', input)
|
||||
end
|
||||
end
|
||||
|
||||
module SubstituteFilter
|
||||
def substitute(input, params = {})
|
||||
input.gsub(/%\{(\w+)\}/) { |match| params[$1] }
|
||||
input.gsub(/%\{(\w+)\}/) { |_match| params[Regexp.last_match(1)] }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -26,8 +28,8 @@ class FiltersTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
module OverrideObjectMethodFilter
|
||||
def tap(input)
|
||||
"tap overridden"
|
||||
def tap(_input)
|
||||
'tap overridden'
|
||||
end
|
||||
end
|
||||
|
||||
@@ -39,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
|
||||
@@ -53,43 +55,43 @@ 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['numbers'] = [2, 1, 4, 3]
|
||||
@context['words'] = ['expected', 'as', 'alphabetic']
|
||||
@context['arrays'] = ['flower', 'are']
|
||||
@context['case_sensitive'] = ['sensitive', 'Expected', 'case']
|
||||
@context['words'] = %w[expected as alphabetic]
|
||||
@context['arrays'] = %w[flower are]
|
||||
@context['case_sensitive'] = %w[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'] = ['case', 'Assert', 'Insensitive']
|
||||
@context['words'] = %w[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)
|
||||
@@ -104,7 +106,7 @@ class FiltersTest < Minitest::Test
|
||||
@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)
|
||||
@@ -114,27 +116,27 @@ class FiltersTest < Minitest::Test
|
||||
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
|
||||
@@ -146,10 +148,10 @@ class FiltersTest < Minitest::Test
|
||||
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
|
||||
end
|
||||
|
||||
@@ -158,15 +160,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
|
||||
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
module MoneyFilter
|
||||
def money(input)
|
||||
sprintf(' %d$ ', input)
|
||||
end
|
||||
end
|
||||
|
||||
module CanadianMoneyFilter
|
||||
def money(input)
|
||||
sprintf(' %d$ CAD ', input)
|
||||
end
|
||||
end
|
||||
|
||||
class HashOrderingTest < Minitest::Test
|
||||
module MoneyFilter
|
||||
def money(input)
|
||||
format(' %d$ ', input)
|
||||
end
|
||||
end
|
||||
|
||||
module CanadianMoneyFilter
|
||||
def money(input)
|
||||
format(' %d$ CAD ', input)
|
||||
end
|
||||
end
|
||||
|
||||
include Liquid
|
||||
|
||||
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
|
||||
|
||||
13
test/integration/legacy_test.rb
Normal file
13
test/integration/legacy_test.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
# 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
|
||||
@@ -1,7 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
module FunnyFilter
|
||||
def make_funny(input)
|
||||
def make_funny(_input)
|
||||
'LOL'
|
||||
end
|
||||
|
||||
@@ -9,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
|
||||
|
||||
@@ -32,7 +34,7 @@ class OutputTest < Minitest::Test
|
||||
def setup
|
||||
@assigns = {
|
||||
'best_cars' => 'bmw',
|
||||
'car' => { 'bmw' => 'good', 'gm' => 'bad' }
|
||||
'car' => { 'bmw' => 'good', 'gm' => 'bad' },
|
||||
}
|
||||
end
|
||||
|
||||
@@ -45,9 +47,9 @@ class OutputTest < Minitest::Test
|
||||
|
||||
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
|
||||
|
||||
|
||||
247
test/integration/parse_tree_visitor_test.rb
Normal file
247
test/integration/parse_tree_visitor_test.rb
Normal file
@@ -0,0 +1,247 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class ParseTreeVisitorTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def test_variable
|
||||
assert_equal(
|
||||
['test'],
|
||||
visit(%({{ test }}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_varible_with_filter
|
||||
assert_equal(
|
||||
%w[test infilter],
|
||||
visit(%({{ test | split: infilter }}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_dynamic_variable
|
||||
assert_equal(
|
||||
%w[test inlookup],
|
||||
visit(%({{ test[inlookup] }}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_if_condition
|
||||
assert_equal(
|
||||
['test'],
|
||||
visit(%({% if test %}{% endif %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_complex_if_condition
|
||||
assert_equal(
|
||||
['test'],
|
||||
visit(%({% if 1 == 1 and 2 == test %}{% endif %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_if_body
|
||||
assert_equal(
|
||||
['test'],
|
||||
visit(%({% if 1 == 1 %}{{ test }}{% endif %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_unless_condition
|
||||
assert_equal(
|
||||
['test'],
|
||||
visit(%({% unless test %}{% endunless %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_complex_unless_condition
|
||||
assert_equal(
|
||||
['test'],
|
||||
visit(%({% unless 1 == 1 and 2 == test %}{% endunless %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_unless_body
|
||||
assert_equal(
|
||||
['test'],
|
||||
visit(%({% unless 1 == 1 %}{{ test }}{% endunless %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_elsif_condition
|
||||
assert_equal(
|
||||
['test'],
|
||||
visit(%({% if 1 == 1 %}{% elsif test %}{% endif %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_complex_elsif_condition
|
||||
assert_equal(
|
||||
['test'],
|
||||
visit(%({% if 1 == 1 %}{% elsif 1 == 1 and 2 == test %}{% endif %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_elsif_body
|
||||
assert_equal(
|
||||
['test'],
|
||||
visit(%({% if 1 == 1 %}{% elsif 2 == 2 %}{{ test }}{% endif %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_else_body
|
||||
assert_equal(
|
||||
['test'],
|
||||
visit(%({% if 1 == 1 %}{% else %}{{ test }}{% endif %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_case_left
|
||||
assert_equal(
|
||||
['test'],
|
||||
visit(%({% case test %}{% endcase %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_case_condition
|
||||
assert_equal(
|
||||
['test'],
|
||||
visit(%({% case 1 %}{% when test %}{% endcase %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_case_when_body
|
||||
assert_equal(
|
||||
['test'],
|
||||
visit(%({% case 1 %}{% when 2 %}{{ test }}{% endcase %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_case_else_body
|
||||
assert_equal(
|
||||
['test'],
|
||||
visit(%({% case 1 %}{% else %}{{ test }}{% endcase %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_for_in
|
||||
assert_equal(
|
||||
['test'],
|
||||
visit(%({% for x in test %}{% endfor %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_for_limit
|
||||
assert_equal(
|
||||
['test'],
|
||||
visit(%({% for x in (1..5) limit: test %}{% endfor %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_for_offset
|
||||
assert_equal(
|
||||
['test'],
|
||||
visit(%({% for x in (1..5) offset: test %}{% endfor %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_for_body
|
||||
assert_equal(
|
||||
['test'],
|
||||
visit(%({% for x in (1..5) %}{{ test }}{% endfor %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_tablerow_in
|
||||
assert_equal(
|
||||
['test'],
|
||||
visit(%({% tablerow x in test %}{% endtablerow %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_tablerow_limit
|
||||
assert_equal(
|
||||
['test'],
|
||||
visit(%({% tablerow x in (1..5) limit: test %}{% endtablerow %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_tablerow_offset
|
||||
assert_equal(
|
||||
['test'],
|
||||
visit(%({% tablerow x in (1..5) offset: test %}{% endtablerow %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_tablerow_body
|
||||
assert_equal(
|
||||
['test'],
|
||||
visit(%({% tablerow x in (1..5) %}{{ test }}{% endtablerow %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_cycle
|
||||
assert_equal(
|
||||
['test'],
|
||||
visit(%({% cycle test %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_assign
|
||||
assert_equal(
|
||||
['test'],
|
||||
visit(%({% assign x = test %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_capture
|
||||
assert_equal(
|
||||
['test'],
|
||||
visit(%({% capture x %}{{ test }}{% endcapture %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_include
|
||||
assert_equal(
|
||||
['test'],
|
||||
visit(%({% include test %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_include_with
|
||||
assert_equal(
|
||||
['test'],
|
||||
visit(%({% include "hai" with test %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_include_for
|
||||
assert_equal(
|
||||
['test'],
|
||||
visit(%({% include "hai" for test %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_preserve_tree_structure
|
||||
assert_equal(
|
||||
[[nil, [
|
||||
[nil, [[nil, [['other', []]]]]],
|
||||
['test', []],
|
||||
['xs', []],
|
||||
]]],
|
||||
traversal(%({% for x in xs offset: test %}{{ other }}{% endfor %})).visit
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def traversal(template)
|
||||
ParseTreeVisitor
|
||||
.for(Template.parse(template).root)
|
||||
.add_callback_for(VariableLookup, &:name)
|
||||
end
|
||||
|
||||
def visit(template)
|
||||
traversal(template).visit.flatten.compact
|
||||
end
|
||||
end
|
||||
@@ -1,41 +1,43 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class ParsingQuirksTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def test_parsing_css
|
||||
text = " div { font-weight: bold; } "
|
||||
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
|
||||
|
||||
@@ -51,20 +53,20 @@ 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
|
||||
@@ -77,9 +79,9 @@ class ParsingQuirksTest < Minitest::Test
|
||||
|
||||
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
|
||||
@@ -99,7 +101,7 @@ class ParsingQuirksTest < Minitest::Test
|
||||
# After the messed up quotes a filter without parameters (reverse) should work
|
||||
# but one with parameters (remove) shouldn't be detected.
|
||||
assert_template_result('here', "{{ 'hi there' | split:\"t\"\" | reverse | first}}")
|
||||
assert_template_result('hi ', "{{ 'hi there' | split:\"t\"\" | remove:\"i\" | first}}")
|
||||
assert_template_result('hi ', "{{ 'hi there' | split:\"t\"\" | remove:\"i\" | first}}")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -112,7 +114,11 @@ 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_contains_in_id
|
||||
assert_template_result(' YES ', '{% if containsallshipments == true %} YES {% endif %}', 'containsallshipments' => true)
|
||||
end
|
||||
end # ParsingQuirksTest
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class RenderProfilingTest < Minitest::Test
|
||||
@@ -72,7 +74,7 @@ class RenderProfilingTest < Minitest::Test
|
||||
t = Template.parse("{% include 'a_template' %}", profile: true)
|
||||
t.render!
|
||||
|
||||
assert t.profiler.total_render_time >= 0, "Total render time was not calculated"
|
||||
assert t.profiler.total_render_time >= 0, 'Total render time was not calculated'
|
||||
end
|
||||
|
||||
def test_profiling_uses_include_to_mark_children
|
||||
@@ -89,7 +91,7 @@ class RenderProfilingTest < Minitest::Test
|
||||
|
||||
include_node = t.profiler[1]
|
||||
include_node.children.each do |child|
|
||||
assert_equal "a_template", child.partial
|
||||
assert_equal 'a_template', child.partial
|
||||
end
|
||||
end
|
||||
|
||||
@@ -99,12 +101,12 @@ class RenderProfilingTest < Minitest::Test
|
||||
|
||||
a_template = t.profiler[1]
|
||||
a_template.children.each do |child|
|
||||
assert_equal "a_template", child.partial
|
||||
assert_equal 'a_template', child.partial
|
||||
end
|
||||
|
||||
b_template = t.profiler[2]
|
||||
b_template.children.each do |child|
|
||||
assert_equal "b_template", child.partial
|
||||
assert_equal 'b_template', child.partial
|
||||
end
|
||||
end
|
||||
|
||||
@@ -114,12 +116,12 @@ class RenderProfilingTest < Minitest::Test
|
||||
|
||||
a_template1 = t.profiler[1]
|
||||
a_template1.children.each do |child|
|
||||
assert_equal "a_template", child.partial
|
||||
assert_equal 'a_template', child.partial
|
||||
end
|
||||
|
||||
a_template2 = t.profiler[2]
|
||||
a_template2.children.each do |child|
|
||||
assert_equal "a_template", child.partial
|
||||
assert_equal 'a_template', child.partial
|
||||
end
|
||||
end
|
||||
|
||||
@@ -128,7 +130,7 @@ class RenderProfilingTest < Minitest::Test
|
||||
t.render!
|
||||
|
||||
timing_count = 0
|
||||
t.profiler.each do |timing|
|
||||
t.profiler.each do |_timing|
|
||||
timing_count += 1
|
||||
end
|
||||
|
||||
@@ -136,7 +138,7 @@ class RenderProfilingTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_profiling_marks_children_of_if_blocks
|
||||
t = Template.parse("{% if true %} {% increment test %} {{ test }} {% endif %}", profile: true)
|
||||
t = Template.parse('{% if true %} {% increment test %} {{ test }} {% endif %}', profile: true)
|
||||
t.render!
|
||||
|
||||
assert_equal 1, t.profiler.length
|
||||
@@ -144,8 +146,8 @@ class RenderProfilingTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_profiling_marks_children_of_for_blocks
|
||||
t = Template.parse("{% for item in collection %} {{ item }} {% endfor %}", profile: true)
|
||||
t.render!({ "collection" => ["one", "two"] })
|
||||
t = Template.parse('{% for item in collection %} {{ item }} {% endfor %}', profile: true)
|
||||
t.render!('collection' => %w[one two])
|
||||
|
||||
assert_equal 1, t.profiler.length
|
||||
# Will profile each invocation of the for block
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
module SecurityFilter
|
||||
@@ -9,6 +11,10 @@ end
|
||||
class SecurityTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def setup
|
||||
@assigns = {}
|
||||
end
|
||||
|
||||
def test_no_instance_eval
|
||||
text = %( {{ '1+1' | instance_eval }} )
|
||||
expected = %( 1+1 )
|
||||
@@ -53,10 +59,24 @@ class SecurityTest < Minitest::Test
|
||||
current_symbols = Symbol.all_symbols
|
||||
|
||||
assigns = { 'drop' => Drop.new }
|
||||
assert_equal "", Template.parse("{{ drop.custom_method_1 }}", assigns).render!
|
||||
assert_equal "", Template.parse("{{ drop.custom_method_2 }}", assigns).render!
|
||||
assert_equal "", Template.parse("{{ drop.custom_method_3 }}", assigns).render!
|
||||
assert_equal '', Template.parse('{{ drop.custom_method_1 }}', assigns).render!
|
||||
assert_equal '', Template.parse('{{ drop.custom_method_2 }}', assigns).render!
|
||||
assert_equal '', Template.parse('{{ drop.custom_method_3 }}', assigns).render!
|
||||
|
||||
assert_equal [], (Symbol.all_symbols - current_symbols)
|
||||
end
|
||||
|
||||
def test_max_depth_nested_blocks_does_not_raise_exception
|
||||
depth = Liquid::Block::MAX_DEPTH
|
||||
code = '{% if true %}' * depth + 'rendered' + '{% endif %}' * depth
|
||||
assert_equal 'rendered', Template.parse(code).render!
|
||||
end
|
||||
|
||||
def test_more_than_max_depth_nested_blocks_raises_exception
|
||||
depth = Liquid::Block::MAX_DEPTH + 1
|
||||
code = '{% if true %}' * depth + 'rendered' + '{% endif %}' * depth
|
||||
assert_raises(Liquid::StackLevelError) do
|
||||
Template.parse(code).render!
|
||||
end
|
||||
end
|
||||
end # SecurityTest
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# encoding: utf-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
@@ -17,7 +17,7 @@ class TestThing
|
||||
"woot: #{@foo}"
|
||||
end
|
||||
|
||||
def [](whatever)
|
||||
def [](_whatever)
|
||||
to_s
|
||||
end
|
||||
|
||||
@@ -29,7 +29,7 @@ end
|
||||
|
||||
class TestDrop < Liquid::Drop
|
||||
def test
|
||||
"testfoo"
|
||||
'testfoo'
|
||||
end
|
||||
end
|
||||
|
||||
@@ -37,7 +37,17 @@ class TestEnumerable < Liquid::Drop
|
||||
include Enumerable
|
||||
|
||||
def each(&block)
|
||||
[ { "foo" => 1, "bar" => 2 }, { "foo" => 2, "bar" => 1 }, { "foo" => 3, "bar" => 3 } ].each(&block)
|
||||
[{ 'foo' => 1, 'bar' => 2 }, { 'foo' => 2, 'bar' => 1 }, { 'foo' => 3, 'bar' => 3 }].each(&block)
|
||||
end
|
||||
end
|
||||
|
||||
class NumberLikeThing < Liquid::Drop
|
||||
def initialize(amount)
|
||||
@amount = amount
|
||||
end
|
||||
|
||||
def to_number
|
||||
@amount
|
||||
end
|
||||
end
|
||||
|
||||
@@ -55,12 +65,12 @@ class StandardFiltersTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_downcase
|
||||
assert_equal 'testing', @filters.downcase("Testing")
|
||||
assert_equal 'testing', @filters.downcase('Testing')
|
||||
assert_equal '', @filters.downcase(nil)
|
||||
end
|
||||
|
||||
def test_upcase
|
||||
assert_equal 'TESTING', @filters.upcase("Testing")
|
||||
assert_equal 'TESTING', @filters.upcase('Testing')
|
||||
assert_equal '', @filters.upcase(nil)
|
||||
end
|
||||
|
||||
@@ -81,22 +91,22 @@ class StandardFiltersTest < Minitest::Test
|
||||
@filters.slice('foobar', nil)
|
||||
end
|
||||
assert_raises(Liquid::ArgumentError) do
|
||||
@filters.slice('foobar', 0, "")
|
||||
@filters.slice('foobar', 0, '')
|
||||
end
|
||||
end
|
||||
|
||||
def test_slice_on_arrays
|
||||
input = 'foobar'.split(//)
|
||||
assert_equal %w(o o b), @filters.slice(input, 1, 3)
|
||||
assert_equal %w(o o b a r), @filters.slice(input, 1, 1000)
|
||||
assert_equal %w(), @filters.slice(input, 1, 0)
|
||||
assert_equal %w(o), @filters.slice(input, 1, 1)
|
||||
assert_equal %w(b a r), @filters.slice(input, 3, 3)
|
||||
assert_equal %w(a r), @filters.slice(input, -2, 2)
|
||||
assert_equal %w(a r), @filters.slice(input, -2, 1000)
|
||||
assert_equal %w(r), @filters.slice(input, -1)
|
||||
assert_equal %w(), @filters.slice(input, 100, 10)
|
||||
assert_equal %w(), @filters.slice(input, -100, 10)
|
||||
assert_equal %w[o o b], @filters.slice(input, 1, 3)
|
||||
assert_equal %w[o o b a r], @filters.slice(input, 1, 1000)
|
||||
assert_equal %w[], @filters.slice(input, 1, 0)
|
||||
assert_equal %w[o], @filters.slice(input, 1, 1)
|
||||
assert_equal %w[b a r], @filters.slice(input, 3, 3)
|
||||
assert_equal %w[a r], @filters.slice(input, -2, 2)
|
||||
assert_equal %w[a r], @filters.slice(input, -2, 1000)
|
||||
assert_equal %w[r], @filters.slice(input, -1)
|
||||
assert_equal %w[], @filters.slice(input, 100, 10)
|
||||
assert_equal %w[], @filters.slice(input, -100, 10)
|
||||
end
|
||||
|
||||
def test_truncate
|
||||
@@ -104,22 +114,30 @@ class StandardFiltersTest < Minitest::Test
|
||||
assert_equal '1234567890', @filters.truncate('1234567890', 20)
|
||||
assert_equal '...', @filters.truncate('1234567890', 0)
|
||||
assert_equal '1234567890', @filters.truncate('1234567890')
|
||||
assert_equal "测试...", @filters.truncate("测试测试测试测试", 5)
|
||||
assert_equal '测试...', @filters.truncate('测试测试测试测试', 5)
|
||||
assert_equal '12341', @filters.truncate('1234567890', 5, 1)
|
||||
end
|
||||
|
||||
def test_split
|
||||
assert_equal ['12', '34'], @filters.split('12~34', '~')
|
||||
assert_equal %w[12 34], @filters.split('12~34', '~')
|
||||
assert_equal ['A? ', ' ,Z'], @filters.split('A? ~ ~ ~ ,Z', '~ ~ ~')
|
||||
assert_equal ['A?Z'], @filters.split('A?Z', '~')
|
||||
# Regexp works although Liquid does not support.
|
||||
assert_equal ['A', 'Z'], @filters.split('AxZ', /x/)
|
||||
assert_equal [], @filters.split(nil, ' ')
|
||||
assert_equal %w[A Z], @filters.split('A1Z', 1)
|
||||
end
|
||||
|
||||
def test_escape
|
||||
assert_equal '<strong>', @filters.escape('<strong>')
|
||||
assert_equal nil, @filters.escape(nil)
|
||||
assert_equal '1', @filters.escape(1)
|
||||
assert_equal '2001-02-03', @filters.escape(Date.new(2001, 2, 3))
|
||||
assert_nil @filters.escape(nil)
|
||||
end
|
||||
|
||||
def test_h
|
||||
assert_equal '<strong>', @filters.h('<strong>')
|
||||
assert_equal '1', @filters.h(1)
|
||||
assert_equal '2001-02-03', @filters.h(Date.new(2001, 2, 3))
|
||||
assert_nil @filters.h(nil)
|
||||
end
|
||||
|
||||
def test_escape_once
|
||||
@@ -128,14 +146,22 @@ class StandardFiltersTest < Minitest::Test
|
||||
|
||||
def test_url_encode
|
||||
assert_equal 'foo%2B1%40example.com', @filters.url_encode('foo+1@example.com')
|
||||
assert_equal nil, @filters.url_encode(nil)
|
||||
assert_equal '1', @filters.url_encode(1)
|
||||
assert_equal '2001-02-03', @filters.url_encode(Date.new(2001, 2, 3))
|
||||
assert_nil @filters.url_encode(nil)
|
||||
end
|
||||
|
||||
def test_url_decode
|
||||
assert_equal 'foo bar', @filters.url_decode('foo+bar')
|
||||
assert_equal 'foo bar', @filters.url_decode('foo%20bar')
|
||||
assert_equal 'foo+1@example.com', @filters.url_decode('foo%2B1%40example.com')
|
||||
assert_equal nil, @filters.url_decode(nil)
|
||||
assert_equal '1', @filters.url_decode(1)
|
||||
assert_equal '2001-02-03', @filters.url_decode(Date.new(2001, 2, 3))
|
||||
assert_nil @filters.url_decode(nil)
|
||||
exception = assert_raises Liquid::ArgumentError do
|
||||
@filters.url_decode('%ff')
|
||||
end
|
||||
assert_equal 'Liquid error: invalid byte sequence in UTF-8', exception.message
|
||||
end
|
||||
|
||||
def test_truncatewords
|
||||
@@ -143,62 +169,189 @@ class StandardFiltersTest < Minitest::Test
|
||||
assert_equal 'one two...', @filters.truncatewords('one two three', 2)
|
||||
assert_equal 'one two three', @filters.truncatewords('one two three')
|
||||
assert_equal 'Two small (13” x 5.5” x 10” high) baskets fit inside one large basket (13”...', @filters.truncatewords('Two small (13” x 5.5” x 10” high) baskets fit inside one large basket (13” x 16” x 10.5” high) with cover.', 15)
|
||||
assert_equal "测试测试测试测试", @filters.truncatewords('测试测试测试测试', 5)
|
||||
assert_equal '测试测试测试测试', @filters.truncatewords('测试测试测试测试', 5)
|
||||
assert_equal 'one two1', @filters.truncatewords('one two three', 2, 1)
|
||||
end
|
||||
|
||||
def test_strip_html
|
||||
assert_equal 'test', @filters.strip_html("<div>test</div>")
|
||||
assert_equal 'test', @filters.strip_html('<div>test</div>')
|
||||
assert_equal 'test', @filters.strip_html("<div id='test'>test</div>")
|
||||
assert_equal '', @filters.strip_html("<script type='text/javascript'>document.write('some stuff');</script>")
|
||||
assert_equal '', @filters.strip_html("<style type='text/css'>foo bar</style>")
|
||||
assert_equal 'test', @filters.strip_html("<div\nclass='multiline'>test</div>")
|
||||
assert_equal 'test', @filters.strip_html("<!-- foo bar \n test -->test")
|
||||
assert_equal '', @filters.strip_html(nil)
|
||||
|
||||
# Quirk of the existing implementation
|
||||
assert_equal 'foo;', @filters.strip_html('<<<script </script>script>foo;</script>')
|
||||
end
|
||||
|
||||
def test_join
|
||||
assert_equal '1 2 3 4', @filters.join([1, 2, 3, 4])
|
||||
assert_equal '1 - 2 - 3 - 4', @filters.join([1, 2, 3, 4], ' - ')
|
||||
assert_equal '1121314', @filters.join([1, 2, 3, 4], 1)
|
||||
end
|
||||
|
||||
def test_sort
|
||||
assert_equal [1, 2, 3, 4], @filters.sort([4, 3, 2, 1])
|
||||
assert_equal [{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }], @filters.sort([{ "a" => 4 }, { "a" => 3 }, { "a" => 1 }, { "a" => 2 }], "a")
|
||||
assert_equal [{ 'a' => 1 }, { 'a' => 2 }, { 'a' => 3 }, { 'a' => 4 }], @filters.sort([{ 'a' => 4 }, { 'a' => 3 }, { 'a' => 1 }, { 'a' => 2 }], 'a')
|
||||
end
|
||||
|
||||
def test_sort_with_nils
|
||||
assert_equal [1, 2, 3, 4, nil], @filters.sort([nil, 4, 3, 2, 1])
|
||||
assert_equal [{ 'a' => 1 }, { 'a' => 2 }, { 'a' => 3 }, { 'a' => 4 }, {}], @filters.sort([{ 'a' => 4 }, { 'a' => 3 }, {}, { 'a' => 1 }, { 'a' => 2 }], 'a')
|
||||
end
|
||||
|
||||
def test_sort_when_property_is_sometimes_missing_puts_nils_last
|
||||
input = [
|
||||
{ 'price' => 4, 'handle' => 'alpha' },
|
||||
{ 'handle' => 'beta' },
|
||||
{ 'price' => 1, 'handle' => 'gamma' },
|
||||
{ 'handle' => 'delta' },
|
||||
{ 'price' => 2, 'handle' => 'epsilon' },
|
||||
]
|
||||
expectation = [
|
||||
{ 'price' => 1, 'handle' => 'gamma' },
|
||||
{ 'price' => 2, 'handle' => 'epsilon' },
|
||||
{ 'price' => 4, 'handle' => 'alpha' },
|
||||
{ 'handle' => 'delta' },
|
||||
{ 'handle' => 'beta' },
|
||||
]
|
||||
assert_equal expectation, @filters.sort(input, 'price')
|
||||
end
|
||||
|
||||
def test_sort_natural
|
||||
assert_equal %w[a B c D], @filters.sort_natural(%w[c D a B])
|
||||
assert_equal [{ 'a' => 'a' }, { 'a' => 'B' }, { 'a' => 'c' }, { 'a' => 'D' }], @filters.sort_natural([{ 'a' => 'D' }, { 'a' => 'c' }, { 'a' => 'a' }, { 'a' => 'B' }], 'a')
|
||||
end
|
||||
|
||||
def test_sort_natural_with_nils
|
||||
assert_equal ['a', 'B', 'c', 'D', nil], @filters.sort_natural([nil, 'c', 'D', 'a', 'B'])
|
||||
assert_equal [{ 'a' => 'a' }, { 'a' => 'B' }, { 'a' => 'c' }, { 'a' => 'D' }, {}], @filters.sort_natural([{ 'a' => 'D' }, { 'a' => 'c' }, {}, { 'a' => 'a' }, { 'a' => 'B' }], 'a')
|
||||
end
|
||||
|
||||
def test_sort_natural_when_property_is_sometimes_missing_puts_nils_last
|
||||
input = [
|
||||
{ 'price' => '4', 'handle' => 'alpha' },
|
||||
{ 'handle' => 'beta' },
|
||||
{ 'price' => '1', 'handle' => 'gamma' },
|
||||
{ 'handle' => 'delta' },
|
||||
{ 'price' => 2, 'handle' => 'epsilon' },
|
||||
]
|
||||
expectation = [
|
||||
{ 'price' => '1', 'handle' => 'gamma' },
|
||||
{ 'price' => 2, 'handle' => 'epsilon' },
|
||||
{ 'price' => '4', 'handle' => 'alpha' },
|
||||
{ 'handle' => 'delta' },
|
||||
{ 'handle' => 'beta' },
|
||||
]
|
||||
assert_equal expectation, @filters.sort_natural(input, 'price')
|
||||
end
|
||||
|
||||
def test_sort_natural_case_check
|
||||
input = [
|
||||
{ 'key' => 'X' },
|
||||
{ 'key' => 'Y' },
|
||||
{ 'key' => 'Z' },
|
||||
{ 'fake' => 't' },
|
||||
{ 'key' => 'a' },
|
||||
{ 'key' => 'b' },
|
||||
{ 'key' => 'c' },
|
||||
]
|
||||
expectation = [
|
||||
{ 'key' => 'a' },
|
||||
{ 'key' => 'b' },
|
||||
{ 'key' => 'c' },
|
||||
{ 'key' => 'X' },
|
||||
{ 'key' => 'Y' },
|
||||
{ 'key' => 'Z' },
|
||||
{ 'fake' => 't' },
|
||||
]
|
||||
assert_equal expectation, @filters.sort_natural(input, 'key')
|
||||
assert_equal %w[a b c X Y Z], @filters.sort_natural(%w[X Y Z a b c])
|
||||
end
|
||||
|
||||
def test_sort_empty_array
|
||||
assert_equal [], @filters.sort([], "a")
|
||||
assert_equal [], @filters.sort([], 'a')
|
||||
end
|
||||
|
||||
def test_sort_invalid_property
|
||||
foo = [
|
||||
[1],
|
||||
[2],
|
||||
[3],
|
||||
]
|
||||
|
||||
assert_raises Liquid::ArgumentError do
|
||||
@filters.sort(foo, 'bar')
|
||||
end
|
||||
end
|
||||
|
||||
def test_sort_natural_empty_array
|
||||
assert_equal [], @filters.sort_natural([], "a")
|
||||
assert_equal [], @filters.sort_natural([], 'a')
|
||||
end
|
||||
|
||||
def test_sort_natural_invalid_property
|
||||
foo = [
|
||||
[1],
|
||||
[2],
|
||||
[3],
|
||||
]
|
||||
|
||||
assert_raises Liquid::ArgumentError do
|
||||
@filters.sort_natural(foo, 'bar')
|
||||
end
|
||||
end
|
||||
|
||||
def test_legacy_sort_hash
|
||||
assert_equal [{ a: 1, b: 2 }], @filters.sort({ a: 1, b: 2 })
|
||||
assert_equal [{ a: 1, b: 2 }], @filters.sort(a: 1, b: 2)
|
||||
end
|
||||
|
||||
def test_numerical_vs_lexicographical_sort
|
||||
assert_equal [2, 10], @filters.sort([10, 2])
|
||||
assert_equal [{ "a" => 2 }, { "a" => 10 }], @filters.sort([{ "a" => 10 }, { "a" => 2 }], "a")
|
||||
assert_equal ["10", "2"], @filters.sort(["10", "2"])
|
||||
assert_equal [{ "a" => "10" }, { "a" => "2" }], @filters.sort([{ "a" => "10" }, { "a" => "2" }], "a")
|
||||
assert_equal [{ 'a' => 2 }, { 'a' => 10 }], @filters.sort([{ 'a' => 10 }, { 'a' => 2 }], 'a')
|
||||
assert_equal %w[10 2], @filters.sort(%w[10 2])
|
||||
assert_equal [{ 'a' => '10' }, { 'a' => '2' }], @filters.sort([{ 'a' => '10' }, { 'a' => '2' }], 'a')
|
||||
end
|
||||
|
||||
def test_uniq
|
||||
assert_equal ["foo"], @filters.uniq("foo")
|
||||
assert_equal ['foo'], @filters.uniq('foo')
|
||||
assert_equal [1, 3, 2, 4], @filters.uniq([1, 1, 3, 2, 3, 1, 4, 3, 2, 1])
|
||||
assert_equal [{ "a" => 1 }, { "a" => 3 }, { "a" => 2 }], @filters.uniq([{ "a" => 1 }, { "a" => 3 }, { "a" => 1 }, { "a" => 2 }], "a")
|
||||
assert_equal [{ 'a' => 1 }, { 'a' => 3 }, { 'a' => 2 }], @filters.uniq([{ 'a' => 1 }, { 'a' => 3 }, { 'a' => 1 }, { 'a' => 2 }], 'a')
|
||||
testdrop = TestDrop.new
|
||||
assert_equal [testdrop], @filters.uniq([testdrop, TestDrop.new], 'test')
|
||||
end
|
||||
|
||||
def test_uniq_empty_array
|
||||
assert_equal [], @filters.uniq([], "a")
|
||||
assert_equal [], @filters.uniq([], 'a')
|
||||
end
|
||||
|
||||
def test_uniq_invalid_property
|
||||
foo = [
|
||||
[1],
|
||||
[2],
|
||||
[3],
|
||||
]
|
||||
|
||||
assert_raises Liquid::ArgumentError do
|
||||
@filters.uniq(foo, 'bar')
|
||||
end
|
||||
end
|
||||
|
||||
def test_compact_empty_array
|
||||
assert_equal [], @filters.compact([], "a")
|
||||
assert_equal [], @filters.compact([], 'a')
|
||||
end
|
||||
|
||||
def test_compact_invalid_property
|
||||
foo = [
|
||||
[1],
|
||||
[2],
|
||||
[3],
|
||||
]
|
||||
|
||||
assert_raises Liquid::ArgumentError do
|
||||
@filters.compact(foo, 'bar')
|
||||
end
|
||||
end
|
||||
|
||||
def test_reverse
|
||||
@@ -210,64 +363,87 @@ class StandardFiltersTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_map
|
||||
assert_equal [1, 2, 3, 4], @filters.map([{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }], 'a')
|
||||
assert_equal [1, 2, 3, 4], @filters.map([{ 'a' => 1 }, { 'a' => 2 }, { 'a' => 3 }, { 'a' => 4 }], 'a')
|
||||
assert_template_result 'abc', "{{ ary | map:'foo' | map:'bar' }}",
|
||||
'ary' => [{ 'foo' => { 'bar' => 'a' } }, { 'foo' => { 'bar' => 'b' } }, { 'foo' => { 'bar' => 'c' } }]
|
||||
'ary' => [{ 'foo' => { 'bar' => 'a' } }, { 'foo' => { 'bar' => 'b' } }, { 'foo' => { 'bar' => 'c' } }]
|
||||
end
|
||||
|
||||
def test_map_doesnt_call_arbitrary_stuff
|
||||
assert_template_result "", '{{ "foo" | map: "__id__" }}'
|
||||
assert_template_result "", '{{ "foo" | map: "inspect" }}'
|
||||
assert_template_result '', '{{ "foo" | map: "__id__" }}'
|
||||
assert_template_result '', '{{ "foo" | map: "inspect" }}'
|
||||
end
|
||||
|
||||
def test_map_calls_to_liquid
|
||||
t = TestThing.new
|
||||
assert_template_result "woot: 1", '{{ foo | map: "whatever" }}', "foo" => [t]
|
||||
assert_template_result 'woot: 1', '{{ foo | map: "whatever" }}', 'foo' => [t]
|
||||
end
|
||||
|
||||
def test_map_on_hashes
|
||||
assert_template_result "4217", '{{ thing | map: "foo" | map: "bar" }}',
|
||||
"thing" => { "foo" => [ { "bar" => 42 }, { "bar" => 17 } ] }
|
||||
assert_template_result '4217', '{{ thing | map: "foo" | map: "bar" }}',
|
||||
'thing' => { 'foo' => [{ 'bar' => 42 }, { 'bar' => 17 }] }
|
||||
end
|
||||
|
||||
def test_legacy_map_on_hashes_with_dynamic_key
|
||||
template = "{% assign key = 'foo' %}{{ thing | map: key | map: 'bar' }}"
|
||||
hash = { "foo" => { "bar" => 42 } }
|
||||
assert_template_result "42", template, "thing" => hash
|
||||
hash = { 'foo' => { 'bar' => 42 } }
|
||||
assert_template_result '42', template, 'thing' => hash
|
||||
end
|
||||
|
||||
def test_sort_calls_to_liquid
|
||||
t = TestThing.new
|
||||
Liquid::Template.parse('{{ foo | sort: "whatever" }}').render("foo" => [t])
|
||||
assert t.foo > 0
|
||||
Liquid::Template.parse('{{ foo | sort: "whatever" }}').render('foo' => [t])
|
||||
assert t.foo.positive?
|
||||
end
|
||||
|
||||
def test_map_over_proc
|
||||
drop = TestDrop.new
|
||||
p = proc{ drop }
|
||||
p = proc { drop }
|
||||
templ = '{{ procs | map: "test" }}'
|
||||
assert_template_result "testfoo", templ, "procs" => [p]
|
||||
assert_template_result 'testfoo', templ, 'procs' => [p]
|
||||
end
|
||||
|
||||
def test_map_over_drops_returning_procs
|
||||
drops = [
|
||||
{
|
||||
"proc" => ->{ "foo" },
|
||||
'proc' => -> { 'foo' },
|
||||
},
|
||||
{
|
||||
"proc" => ->{ "bar" },
|
||||
'proc' => -> { 'bar' },
|
||||
},
|
||||
]
|
||||
templ = '{{ drops | map: "proc" }}'
|
||||
assert_template_result "foobar", templ, "drops" => drops
|
||||
assert_template_result 'foobar', templ, 'drops' => drops
|
||||
end
|
||||
|
||||
def test_map_works_on_enumerables
|
||||
assert_template_result "123", '{{ foo | map: "foo" }}', "foo" => TestEnumerable.new
|
||||
assert_template_result '123', '{{ foo | map: "foo" }}', 'foo' => TestEnumerable.new
|
||||
end
|
||||
|
||||
def test_map_returns_empty_on_2d_input_array
|
||||
foo = [
|
||||
[1],
|
||||
[2],
|
||||
[3],
|
||||
]
|
||||
|
||||
assert_raises Liquid::ArgumentError do
|
||||
@filters.map(foo, 'bar')
|
||||
end
|
||||
end
|
||||
|
||||
def test_map_returns_empty_with_no_property
|
||||
foo = [
|
||||
[1],
|
||||
[2],
|
||||
[3],
|
||||
]
|
||||
assert_raises Liquid::ArgumentError do
|
||||
@filters.map(foo, nil)
|
||||
end
|
||||
end
|
||||
|
||||
def test_sort_works_on_enumerables
|
||||
assert_template_result "213", '{{ foo | sort: "bar" | map: "foo" }}', "foo" => TestEnumerable.new
|
||||
assert_template_result '213', '{{ foo | sort: "bar" | map: "foo" }}', 'foo' => TestEnumerable.new
|
||||
end
|
||||
|
||||
def test_first_and_last_call_to_liquid
|
||||
@@ -276,45 +452,45 @@ class StandardFiltersTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_truncate_calls_to_liquid
|
||||
assert_template_result "wo...", '{{ foo | truncate: 5 }}', "foo" => TestThing.new
|
||||
assert_template_result 'wo...', '{{ foo | truncate: 5 }}', 'foo' => TestThing.new
|
||||
end
|
||||
|
||||
def test_date
|
||||
assert_equal 'May', @filters.date(Time.parse("2006-05-05 10:00:00"), "%B")
|
||||
assert_equal 'June', @filters.date(Time.parse("2006-06-05 10:00:00"), "%B")
|
||||
assert_equal 'July', @filters.date(Time.parse("2006-07-05 10:00:00"), "%B")
|
||||
assert_equal 'May', @filters.date(Time.parse('2006-05-05 10:00:00'), '%B')
|
||||
assert_equal 'June', @filters.date(Time.parse('2006-06-05 10:00:00'), '%B')
|
||||
assert_equal 'July', @filters.date(Time.parse('2006-07-05 10:00:00'), '%B')
|
||||
|
||||
assert_equal 'May', @filters.date("2006-05-05 10:00:00", "%B")
|
||||
assert_equal 'June', @filters.date("2006-06-05 10:00:00", "%B")
|
||||
assert_equal 'July', @filters.date("2006-07-05 10:00:00", "%B")
|
||||
assert_equal 'May', @filters.date('2006-05-05 10:00:00', '%B')
|
||||
assert_equal 'June', @filters.date('2006-06-05 10:00:00', '%B')
|
||||
assert_equal 'July', @filters.date('2006-07-05 10:00:00', '%B')
|
||||
|
||||
assert_equal '2006-07-05 10:00:00', @filters.date("2006-07-05 10:00:00", "")
|
||||
assert_equal '2006-07-05 10:00:00', @filters.date("2006-07-05 10:00:00", "")
|
||||
assert_equal '2006-07-05 10:00:00', @filters.date("2006-07-05 10:00:00", "")
|
||||
assert_equal '2006-07-05 10:00:00', @filters.date("2006-07-05 10:00:00", nil)
|
||||
assert_equal '2006-07-05 10:00:00', @filters.date('2006-07-05 10:00:00', '')
|
||||
assert_equal '2006-07-05 10:00:00', @filters.date('2006-07-05 10:00:00', '')
|
||||
assert_equal '2006-07-05 10:00:00', @filters.date('2006-07-05 10:00:00', '')
|
||||
assert_equal '2006-07-05 10:00:00', @filters.date('2006-07-05 10:00:00', nil)
|
||||
|
||||
assert_equal '07/05/2006', @filters.date("2006-07-05 10:00:00", "%m/%d/%Y")
|
||||
assert_equal '07/05/2006', @filters.date('2006-07-05 10:00:00', '%m/%d/%Y')
|
||||
|
||||
assert_equal "07/16/2004", @filters.date("Fri Jul 16 01:00:00 2004", "%m/%d/%Y")
|
||||
assert_equal "#{Date.today.year}", @filters.date('now', '%Y')
|
||||
assert_equal "#{Date.today.year}", @filters.date('today', '%Y')
|
||||
assert_equal "#{Date.today.year}", @filters.date('Today', '%Y')
|
||||
assert_equal '07/16/2004', @filters.date('Fri Jul 16 01:00:00 2004', '%m/%d/%Y')
|
||||
assert_equal Date.today.year.to_s, @filters.date('now', '%Y')
|
||||
assert_equal Date.today.year.to_s, @filters.date('today', '%Y')
|
||||
assert_equal Date.today.year.to_s, @filters.date('Today', '%Y')
|
||||
|
||||
assert_equal nil, @filters.date(nil, "%B")
|
||||
assert_nil @filters.date(nil, '%B')
|
||||
|
||||
assert_equal '', @filters.date('', "%B")
|
||||
assert_equal '', @filters.date('', '%B')
|
||||
|
||||
with_timezone("UTC") do
|
||||
assert_equal "07/05/2006", @filters.date(1152098955, "%m/%d/%Y")
|
||||
assert_equal "07/05/2006", @filters.date("1152098955", "%m/%d/%Y")
|
||||
with_timezone('UTC') do
|
||||
assert_equal '07/05/2006', @filters.date(1_152_098_955, '%m/%d/%Y')
|
||||
assert_equal '07/05/2006', @filters.date('1152098955', '%m/%d/%Y')
|
||||
end
|
||||
end
|
||||
|
||||
def test_first_last
|
||||
assert_equal 1, @filters.first([1, 2, 3])
|
||||
assert_equal 3, @filters.last([1, 2, 3])
|
||||
assert_equal nil, @filters.first([])
|
||||
assert_equal nil, @filters.last([])
|
||||
assert_nil @filters.first([])
|
||||
assert_nil @filters.last([])
|
||||
end
|
||||
|
||||
def test_replace
|
||||
@@ -326,10 +502,10 @@ class StandardFiltersTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_remove
|
||||
assert_equal ' ', @filters.remove("a a a a", 'a')
|
||||
assert_equal ' ', @filters.remove("1 1 1 1", 1)
|
||||
assert_equal 'a a a', @filters.remove_first("a a a a", 'a ')
|
||||
assert_equal ' 1 1 1', @filters.remove_first("1 1 1 1", 1)
|
||||
assert_equal ' ', @filters.remove('a a a a', 'a')
|
||||
assert_equal ' ', @filters.remove('1 1 1 1', 1)
|
||||
assert_equal 'a a a', @filters.remove_first('a a a a', 'a ')
|
||||
assert_equal ' 1 1 1', @filters.remove_first('1 1 1 1', 1)
|
||||
assert_template_result 'a a a', "{{ 'a a a a' | remove_first: 'a ' }}"
|
||||
end
|
||||
|
||||
@@ -338,97 +514,148 @@ class StandardFiltersTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_strip
|
||||
assert_template_result 'ab c', "{{ source | strip }}", 'source' => " ab c "
|
||||
assert_template_result 'ab c', "{{ source | strip }}", 'source' => " \tab c \n \t"
|
||||
assert_template_result 'ab c', '{{ source | strip }}', 'source' => ' ab c '
|
||||
assert_template_result 'ab c', '{{ source | strip }}', 'source' => " \tab c \n \t"
|
||||
end
|
||||
|
||||
def test_lstrip
|
||||
assert_template_result 'ab c ', "{{ source | lstrip }}", 'source' => " ab c "
|
||||
assert_template_result "ab c \n \t", "{{ source | lstrip }}", 'source' => " \tab c \n \t"
|
||||
assert_template_result 'ab c ', '{{ source | lstrip }}', 'source' => ' ab c '
|
||||
assert_template_result "ab c \n \t", '{{ source | lstrip }}', 'source' => " \tab c \n \t"
|
||||
end
|
||||
|
||||
def test_rstrip
|
||||
assert_template_result " ab c", "{{ source | rstrip }}", 'source' => " ab c "
|
||||
assert_template_result " \tab c", "{{ source | rstrip }}", 'source' => " \tab c \n \t"
|
||||
assert_template_result ' ab c', '{{ source | rstrip }}', 'source' => ' ab c '
|
||||
assert_template_result " \tab c", '{{ source | rstrip }}', 'source' => " \tab c \n \t"
|
||||
end
|
||||
|
||||
def test_strip_newlines
|
||||
assert_template_result 'abc', "{{ source | strip_newlines }}", 'source' => "a\nb\nc"
|
||||
assert_template_result 'abc', "{{ source | strip_newlines }}", 'source' => "a\r\nb\nc"
|
||||
assert_template_result 'abc', '{{ source | strip_newlines }}', 'source' => "a\nb\nc"
|
||||
assert_template_result 'abc', '{{ source | strip_newlines }}', 'source' => "a\r\nb\nc"
|
||||
end
|
||||
|
||||
def test_newlines_to_br
|
||||
assert_template_result "a<br />\nb<br />\nc", "{{ source | newline_to_br }}", 'source' => "a\nb\nc"
|
||||
assert_template_result "a<br />\nb<br />\nc", '{{ source | newline_to_br }}', 'source' => "a\nb\nc"
|
||||
end
|
||||
|
||||
def test_plus
|
||||
assert_template_result "2", "{{ 1 | plus:1 }}"
|
||||
assert_template_result "2.0", "{{ '1' | plus:'1.0' }}"
|
||||
assert_template_result '2', '{{ 1 | plus:1 }}'
|
||||
assert_template_result '2.0', "{{ '1' | plus:'1.0' }}"
|
||||
|
||||
assert_template_result '5', "{{ price | plus:'2' }}", 'price' => NumberLikeThing.new(3)
|
||||
end
|
||||
|
||||
def test_minus
|
||||
assert_template_result "4", "{{ input | minus:operand }}", 'input' => 5, 'operand' => 1
|
||||
assert_template_result "2.3", "{{ '4.3' | minus:'2' }}"
|
||||
assert_template_result '4', '{{ input | minus:operand }}', 'input' => 5, 'operand' => 1
|
||||
assert_template_result '2.3', "{{ '4.3' | minus:'2' }}"
|
||||
|
||||
assert_template_result '5', "{{ price | minus:'2' }}", 'price' => NumberLikeThing.new(7)
|
||||
end
|
||||
|
||||
def test_abs
|
||||
assert_template_result '17', '{{ 17 | abs }}'
|
||||
assert_template_result '17', '{{ -17 | abs }}'
|
||||
assert_template_result '17', "{{ '17' | abs }}"
|
||||
assert_template_result '17', "{{ '-17' | abs }}"
|
||||
assert_template_result '0', '{{ 0 | abs }}'
|
||||
assert_template_result '0', "{{ '0' | abs }}"
|
||||
assert_template_result '17.42', '{{ 17.42 | abs }}'
|
||||
assert_template_result '17.42', '{{ -17.42 | abs }}'
|
||||
assert_template_result '17.42', "{{ '17.42' | abs }}"
|
||||
assert_template_result '17.42', "{{ '-17.42' | abs }}"
|
||||
end
|
||||
|
||||
def test_times
|
||||
assert_template_result "12", "{{ 3 | times:4 }}"
|
||||
assert_template_result "0", "{{ 'foo' | times:4 }}"
|
||||
|
||||
assert_template_result "6", "{{ '2.1' | times:3 | replace: '.','-' | plus:0}}"
|
||||
|
||||
assert_template_result "7.25", "{{ 0.0725 | times:100 }}"
|
||||
assert_template_result '12', '{{ 3 | times:4 }}'
|
||||
assert_template_result '0', "{{ 'foo' | times:4 }}"
|
||||
assert_template_result '6', "{{ '2.1' | times:3 | replace: '.','-' | plus:0}}"
|
||||
assert_template_result '7.25', '{{ 0.0725 | times:100 }}'
|
||||
assert_template_result '-7.25', '{{ "-0.0725" | times:100 }}'
|
||||
assert_template_result '7.25', '{{ "-0.0725" | times: -100 }}'
|
||||
assert_template_result '4', '{{ price | times:2 }}', 'price' => NumberLikeThing.new(2)
|
||||
end
|
||||
|
||||
def test_divided_by
|
||||
assert_template_result "4", "{{ 12 | divided_by:3 }}"
|
||||
assert_template_result "4", "{{ 14 | divided_by:3 }}"
|
||||
assert_template_result '4', '{{ 12 | divided_by:3 }}'
|
||||
assert_template_result '4', '{{ 14 | divided_by:3 }}'
|
||||
|
||||
assert_template_result "5", "{{ 15 | divided_by:3 }}"
|
||||
assert_equal "Liquid error: divided by 0", Template.parse("{{ 5 | divided_by:0 }}").render
|
||||
assert_template_result '5', '{{ 15 | divided_by:3 }}'
|
||||
assert_equal 'Liquid error: divided by 0', Template.parse('{{ 5 | divided_by:0 }}').render
|
||||
|
||||
assert_template_result "0.5", "{{ 2.0 | divided_by:4 }}"
|
||||
assert_template_result '0.5', '{{ 2.0 | divided_by:4 }}'
|
||||
assert_raises(Liquid::ZeroDivisionError) do
|
||||
assert_template_result "4", "{{ 1 | modulo: 0 }}"
|
||||
assert_template_result '4', '{{ 1 | modulo: 0 }}'
|
||||
end
|
||||
|
||||
assert_template_result '5', '{{ price | divided_by:2 }}', 'price' => NumberLikeThing.new(10)
|
||||
end
|
||||
|
||||
def test_modulo
|
||||
assert_template_result "1", "{{ 3 | modulo:2 }}"
|
||||
assert_template_result '1', '{{ 3 | modulo:2 }}'
|
||||
assert_raises(Liquid::ZeroDivisionError) do
|
||||
assert_template_result "4", "{{ 1 | modulo: 0 }}"
|
||||
assert_template_result '4', '{{ 1 | modulo: 0 }}'
|
||||
end
|
||||
|
||||
assert_template_result '1', '{{ price | modulo:2 }}', 'price' => NumberLikeThing.new(3)
|
||||
end
|
||||
|
||||
def test_round
|
||||
assert_template_result "5", "{{ input | round }}", 'input' => 4.6
|
||||
assert_template_result "4", "{{ '4.3' | round }}"
|
||||
assert_template_result "4.56", "{{ input | round: 2 }}", 'input' => 4.5612
|
||||
assert_template_result '5', '{{ input | round }}', 'input' => 4.6
|
||||
assert_template_result '4', "{{ '4.3' | round }}"
|
||||
assert_template_result '4.56', '{{ input | round: 2 }}', 'input' => 4.5612
|
||||
assert_raises(Liquid::FloatDomainError) do
|
||||
assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | round }}"
|
||||
assert_template_result '4', '{{ 1.0 | divided_by: 0.0 | round }}'
|
||||
end
|
||||
|
||||
assert_template_result '5', '{{ price | round }}', 'price' => NumberLikeThing.new(4.6)
|
||||
assert_template_result '4', '{{ price | round }}', 'price' => NumberLikeThing.new(4.3)
|
||||
end
|
||||
|
||||
def test_ceil
|
||||
assert_template_result "5", "{{ input | ceil }}", 'input' => 4.6
|
||||
assert_template_result "5", "{{ '4.3' | ceil }}"
|
||||
assert_template_result '5', '{{ input | ceil }}', 'input' => 4.6
|
||||
assert_template_result '5', "{{ '4.3' | ceil }}"
|
||||
assert_raises(Liquid::FloatDomainError) do
|
||||
assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | ceil }}"
|
||||
assert_template_result '4', '{{ 1.0 | divided_by: 0.0 | ceil }}'
|
||||
end
|
||||
|
||||
assert_template_result '5', '{{ price | ceil }}', 'price' => NumberLikeThing.new(4.6)
|
||||
end
|
||||
|
||||
def test_floor
|
||||
assert_template_result "4", "{{ input | floor }}", 'input' => 4.6
|
||||
assert_template_result "4", "{{ '4.3' | floor }}"
|
||||
assert_template_result '4', '{{ input | floor }}', 'input' => 4.6
|
||||
assert_template_result '4', "{{ '4.3' | floor }}"
|
||||
assert_raises(Liquid::FloatDomainError) do
|
||||
assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | floor }}"
|
||||
assert_template_result '4', '{{ 1.0 | divided_by: 0.0 | floor }}'
|
||||
end
|
||||
|
||||
assert_template_result '5', '{{ price | floor }}', 'price' => NumberLikeThing.new(5.4)
|
||||
end
|
||||
|
||||
def test_at_most
|
||||
assert_template_result '4', '{{ 5 | at_most:4 }}'
|
||||
assert_template_result '5', '{{ 5 | at_most:5 }}'
|
||||
assert_template_result '5', '{{ 5 | at_most:6 }}'
|
||||
|
||||
assert_template_result '4.5', '{{ 4.5 | at_most:5 }}'
|
||||
assert_template_result '5', '{{ width | at_most:5 }}', 'width' => NumberLikeThing.new(6)
|
||||
assert_template_result '4', '{{ width | at_most:5 }}', 'width' => NumberLikeThing.new(4)
|
||||
assert_template_result '4', '{{ 5 | at_most: width }}', 'width' => NumberLikeThing.new(4)
|
||||
end
|
||||
|
||||
def test_at_least
|
||||
assert_template_result '5', '{{ 5 | at_least:4 }}'
|
||||
assert_template_result '5', '{{ 5 | at_least:5 }}'
|
||||
assert_template_result '6', '{{ 5 | at_least:6 }}'
|
||||
|
||||
assert_template_result '5', '{{ 4.5 | at_least:5 }}'
|
||||
assert_template_result '6', '{{ width | at_least:5 }}', 'width' => NumberLikeThing.new(6)
|
||||
assert_template_result '5', '{{ width | at_least:5 }}', 'width' => NumberLikeThing.new(4)
|
||||
assert_template_result '6', '{{ 5 | at_least: width }}', 'width' => NumberLikeThing.new(6)
|
||||
end
|
||||
|
||||
def test_append
|
||||
assigns = { 'a' => 'bc', 'b' => 'd' }
|
||||
assert_template_result('bcd', "{{ a | append: 'd'}}", assigns)
|
||||
assert_template_result('bcd', "{{ a | append: b}}", assigns)
|
||||
assert_template_result('bcd', '{{ a | append: b}}', assigns)
|
||||
end
|
||||
|
||||
def test_concat
|
||||
@@ -436,8 +663,7 @@ class StandardFiltersTest < Minitest::Test
|
||||
assert_equal [1, 2, 'a'], @filters.concat([1, 2], ['a'])
|
||||
assert_equal [1, 2, 10], @filters.concat([1, 2], [10])
|
||||
|
||||
assert_raises(TypeError) do
|
||||
# no implicit conversion of Fixnum into Array
|
||||
assert_raises(Liquid::ArgumentError, 'concat filter requires an array argument') do
|
||||
@filters.concat([1, 2], 10)
|
||||
end
|
||||
end
|
||||
@@ -445,16 +671,16 @@ class StandardFiltersTest < Minitest::Test
|
||||
def test_prepend
|
||||
assigns = { 'a' => 'bc', 'b' => 'a' }
|
||||
assert_template_result('abc', "{{ a | prepend: 'a'}}", assigns)
|
||||
assert_template_result('abc', "{{ a | prepend: b}}", assigns)
|
||||
assert_template_result('abc', '{{ a | prepend: b}}', assigns)
|
||||
end
|
||||
|
||||
def test_default
|
||||
assert_equal "foo", @filters.default("foo", "bar")
|
||||
assert_equal "bar", @filters.default(nil, "bar")
|
||||
assert_equal "bar", @filters.default("", "bar")
|
||||
assert_equal "bar", @filters.default(false, "bar")
|
||||
assert_equal "bar", @filters.default([], "bar")
|
||||
assert_equal "bar", @filters.default({}, "bar")
|
||||
assert_equal 'foo', @filters.default('foo', 'bar')
|
||||
assert_equal 'bar', @filters.default(nil, 'bar')
|
||||
assert_equal 'bar', @filters.default('', 'bar')
|
||||
assert_equal 'bar', @filters.default(false, 'bar')
|
||||
assert_equal 'bar', @filters.default([], 'bar')
|
||||
assert_equal 'bar', @filters.default({}, 'bar')
|
||||
end
|
||||
|
||||
def test_cannot_access_private_methods
|
||||
@@ -466,6 +692,78 @@ class StandardFiltersTest < Minitest::Test
|
||||
assert_template_result('abc', "{{ 'abc' | date: '%D' }}")
|
||||
end
|
||||
|
||||
def test_where
|
||||
input = [
|
||||
{ 'handle' => 'alpha', 'ok' => true },
|
||||
{ 'handle' => 'beta', 'ok' => false },
|
||||
{ 'handle' => 'gamma', 'ok' => false },
|
||||
{ 'handle' => 'delta', 'ok' => true },
|
||||
]
|
||||
|
||||
expectation = [
|
||||
{ 'handle' => 'alpha', 'ok' => true },
|
||||
{ 'handle' => 'delta', 'ok' => true },
|
||||
]
|
||||
|
||||
assert_equal expectation, @filters.where(input, 'ok', true)
|
||||
assert_equal expectation, @filters.where(input, 'ok')
|
||||
end
|
||||
|
||||
def test_where_no_key_set
|
||||
input = [
|
||||
{ 'handle' => 'alpha', 'ok' => true },
|
||||
{ 'handle' => 'beta' },
|
||||
{ 'handle' => 'gamma' },
|
||||
{ 'handle' => 'delta', 'ok' => true },
|
||||
]
|
||||
|
||||
expectation = [
|
||||
{ 'handle' => 'alpha', 'ok' => true },
|
||||
{ 'handle' => 'delta', 'ok' => true },
|
||||
]
|
||||
|
||||
assert_equal expectation, @filters.where(input, 'ok', true)
|
||||
assert_equal expectation, @filters.where(input, 'ok')
|
||||
end
|
||||
|
||||
def test_where_non_array_map_input
|
||||
assert_equal [{ 'a' => 'ok' }], @filters.where({ 'a' => 'ok' }, 'a', 'ok')
|
||||
assert_equal [], @filters.where({ 'a' => 'not ok' }, 'a', 'ok')
|
||||
end
|
||||
|
||||
def test_where_indexable_but_non_map_value
|
||||
assert_raises(Liquid::ArgumentError) { @filters.where(1, 'ok', true) }
|
||||
assert_raises(Liquid::ArgumentError) { @filters.where(1, 'ok') }
|
||||
end
|
||||
|
||||
def test_where_non_boolean_value
|
||||
input = [
|
||||
{ 'message' => 'Bonjour!', 'language' => 'French' },
|
||||
{ 'message' => 'Hello!', 'language' => 'English' },
|
||||
{ 'message' => 'Hallo!', 'language' => 'German' },
|
||||
]
|
||||
|
||||
assert_equal [{ 'message' => 'Bonjour!', 'language' => 'French' }], @filters.where(input, 'language', 'French')
|
||||
assert_equal [{ 'message' => 'Hallo!', 'language' => 'German' }], @filters.where(input, 'language', 'German')
|
||||
assert_equal [{ 'message' => 'Hello!', 'language' => 'English' }], @filters.where(input, 'language', 'English')
|
||||
end
|
||||
|
||||
def test_where_array_of_only_unindexable_values
|
||||
assert_nil @filters.where([nil], 'ok', true)
|
||||
assert_nil @filters.where([nil], 'ok')
|
||||
end
|
||||
|
||||
def test_where_no_target_value
|
||||
input = [
|
||||
{ 'foo' => false },
|
||||
{ 'foo' => true },
|
||||
{ 'foo' => 'for sure' },
|
||||
{ 'bar' => true },
|
||||
]
|
||||
|
||||
assert_equal [{ 'foo' => true }, { 'foo' => 'for sure' }], @filters.where(input, 'foo')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def with_timezone(tz)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class BreakTagTest < Minitest::Test
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class ContinueTagTest < Minitest::Test
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class ThingWithValue < Liquid::Drop
|
||||
@@ -23,16 +25,16 @@ class ForTagTest < Minitest::Test
|
||||
yo
|
||||
|
||||
HERE
|
||||
template = <<HERE
|
||||
{%for item in array%}
|
||||
yo
|
||||
{%endfor%}
|
||||
HERE
|
||||
template = <<~HERE
|
||||
{%for item in array%}
|
||||
yo
|
||||
{%endfor%}
|
||||
HERE
|
||||
assert_template_result(expected, template, 'array' => [1, 2, 3])
|
||||
end
|
||||
|
||||
def test_for_reversed
|
||||
assigns = { 'array' => [ 1, 2, 3] }
|
||||
assigns = { 'array' => [1, 2, 3] }
|
||||
assert_template_result('321', '{%for item in array reversed %}{{item}}{%endfor%}', assigns)
|
||||
end
|
||||
|
||||
@@ -40,31 +42,31 @@ HERE
|
||||
assert_template_result(' 1 2 3 ', '{%for item in (1..3) %} {{item}} {%endfor%}')
|
||||
|
||||
assert_raises(Liquid::ArgumentError) do
|
||||
Template.parse('{% for i in (a..2) %}{% endfor %}').render!("a" => [1, 2])
|
||||
Template.parse('{% for i in (a..2) %}{% endfor %}').render!('a' => [1, 2])
|
||||
end
|
||||
|
||||
assert_template_result(' 0 1 2 3 ', '{% for item in (a..3) %} {{item}} {% endfor %}', "a" => "invalid integer")
|
||||
assert_template_result(' 0 1 2 3 ', '{% for item in (a..3) %} {{item}} {% endfor %}', 'a' => 'invalid integer')
|
||||
end
|
||||
|
||||
def test_for_with_variable_range
|
||||
assert_template_result(' 1 2 3 ', '{%for item in (1..foobar) %} {{item}} {%endfor%}', "foobar" => 3)
|
||||
assert_template_result(' 1 2 3 ', '{%for item in (1..foobar) %} {{item}} {%endfor%}', 'foobar' => 3)
|
||||
end
|
||||
|
||||
def test_for_with_hash_value_range
|
||||
foobar = { "value" => 3 }
|
||||
assert_template_result(' 1 2 3 ', '{%for item in (1..foobar.value) %} {{item}} {%endfor%}', "foobar" => foobar)
|
||||
foobar = { 'value' => 3 }
|
||||
assert_template_result(' 1 2 3 ', '{%for item in (1..foobar.value) %} {{item}} {%endfor%}', 'foobar' => foobar)
|
||||
end
|
||||
|
||||
def test_for_with_drop_value_range
|
||||
foobar = ThingWithValue.new
|
||||
assert_template_result(' 1 2 3 ', '{%for item in (1..foobar.value) %} {{item}} {%endfor%}', "foobar" => foobar)
|
||||
assert_template_result(' 1 2 3 ', '{%for item in (1..foobar.value) %} {{item}} {%endfor%}', 'foobar' => foobar)
|
||||
end
|
||||
|
||||
def test_for_with_variable
|
||||
assert_template_result(' 1 2 3 ', '{%for item in array%} {{item}} {%endfor%}', 'array' => [1, 2, 3])
|
||||
assert_template_result('123', '{%for item in array%}{{item}}{%endfor%}', 'array' => [1, 2, 3])
|
||||
assert_template_result('123', '{% for item in array %}{{item}}{% endfor %}', 'array' => [1, 2, 3])
|
||||
assert_template_result('abcd', '{%for item in array%}{{item}}{%endfor%}', 'array' => ['a', 'b', 'c', 'd'])
|
||||
assert_template_result('abcd', '{%for item in array%}{{item}}{%endfor%}', 'array' => %w[a b c d])
|
||||
assert_template_result('a b c', '{%for item in array%}{{item}}{%endfor%}', 'array' => ['a', ' ', 'b', ' ', 'c'])
|
||||
assert_template_result('abc', '{%for item in array%}{{item}}{%endfor%}', 'array' => ['a', '', 'b', '', 'c'])
|
||||
end
|
||||
@@ -72,8 +74,8 @@ HERE
|
||||
def test_for_helpers
|
||||
assigns = { 'array' => [1, 2, 3] }
|
||||
assert_template_result(' 1/3 2/3 3/3 ',
|
||||
'{%for item in array%} {{forloop.index}}/{{forloop.length}} {%endfor%}',
|
||||
assigns)
|
||||
'{%for item in array%} {{forloop.index}}/{{forloop.length}} {%endfor%}',
|
||||
assigns)
|
||||
assert_template_result(' 1 2 3 ', '{%for item in array%} {{forloop.index}} {%endfor%}', assigns)
|
||||
assert_template_result(' 0 1 2 ', '{%for item in array%} {{forloop.index0}} {%endfor%}', assigns)
|
||||
assert_template_result(' 2 1 0 ', '{%for item in array%} {{forloop.rindex0}} {%endfor%}', assigns)
|
||||
@@ -85,8 +87,8 @@ HERE
|
||||
def test_for_and_if
|
||||
assigns = { 'array' => [1, 2, 3] }
|
||||
assert_template_result('+--',
|
||||
'{%for item in array%}{% if forloop.first %}+{% else %}-{% endif %}{%endfor%}',
|
||||
assigns)
|
||||
'{%for item in array%}{% if forloop.first %}+{% else %}-{% endif %}{%endfor%}',
|
||||
assigns)
|
||||
end
|
||||
|
||||
def test_for_else
|
||||
@@ -103,6 +105,34 @@ HERE
|
||||
assert_template_result('3456', '{%for i in array limit: 4 offset: 2 %}{{ i }}{%endfor%}', assigns)
|
||||
end
|
||||
|
||||
def test_limiting_with_invalid_limit
|
||||
assigns = { 'array' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] }
|
||||
template = <<-MKUP
|
||||
{% for i in array limit: true offset: 1 %}
|
||||
{{ i }}
|
||||
{% endfor %}
|
||||
MKUP
|
||||
|
||||
exception = assert_raises(Liquid::ArgumentError) do
|
||||
Template.parse(template).render!(assigns)
|
||||
end
|
||||
assert_equal('Liquid error: invalid integer', exception.message)
|
||||
end
|
||||
|
||||
def test_limiting_with_invalid_offset
|
||||
assigns = { 'array' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] }
|
||||
template = <<-MKUP
|
||||
{% for i in array limit: 1 offset: true %}
|
||||
{{ i }}
|
||||
{% endfor %}
|
||||
MKUP
|
||||
|
||||
exception = assert_raises(Liquid::ArgumentError) do
|
||||
Template.parse(template).render!(assigns)
|
||||
end
|
||||
assert_equal('Liquid error: invalid integer', exception.message)
|
||||
end
|
||||
|
||||
def test_dynamic_variable_limiting
|
||||
assigns = { 'array' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] }
|
||||
assigns['limit'] = 2
|
||||
@@ -129,14 +159,14 @@ HERE
|
||||
{%for i in array.items offset:continue limit: 3 %}{{i}}{%endfor%}
|
||||
next
|
||||
{%for i in array.items offset:continue limit: 3 %}{{i}}{%endfor%}
|
||||
MKUP
|
||||
MKUP
|
||||
expected = <<-XPCTD
|
||||
123
|
||||
next
|
||||
456
|
||||
next
|
||||
789
|
||||
XPCTD
|
||||
XPCTD
|
||||
assert_template_result(expected, markup, assigns)
|
||||
end
|
||||
|
||||
@@ -148,18 +178,18 @@ HERE
|
||||
{%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%}
|
||||
next
|
||||
{%for i in array.items offset:continue limit:1 %}{{i}}{%endfor%}
|
||||
MKUP
|
||||
MKUP
|
||||
expected = <<-XPCTD
|
||||
123
|
||||
next
|
||||
456
|
||||
next
|
||||
7
|
||||
XPCTD
|
||||
XPCTD
|
||||
assert_template_result(expected, markup, assigns)
|
||||
end
|
||||
|
||||
def test_pause_resume_BIG_limit
|
||||
def test_pause_resume_big_limit
|
||||
assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } }
|
||||
markup = <<-MKUP
|
||||
{%for i in array.items limit:3 %}{{i}}{%endfor%}
|
||||
@@ -167,18 +197,18 @@ HERE
|
||||
{%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%}
|
||||
next
|
||||
{%for i in array.items offset:continue limit:1000 %}{{i}}{%endfor%}
|
||||
MKUP
|
||||
MKUP
|
||||
expected = <<-XPCTD
|
||||
123
|
||||
next
|
||||
456
|
||||
next
|
||||
7890
|
||||
XPCTD
|
||||
XPCTD
|
||||
assert_template_result(expected, markup, assigns)
|
||||
end
|
||||
|
||||
def test_pause_resume_BIG_offset
|
||||
def test_pause_resume_big_offset
|
||||
assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } }
|
||||
markup = '{%for i in array.items limit:3 %}{{i}}{%endfor%}
|
||||
next
|
||||
@@ -197,19 +227,19 @@ HERE
|
||||
assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] } }
|
||||
|
||||
markup = '{% for i in array.items %}{% break %}{% endfor %}'
|
||||
expected = ""
|
||||
expected = ''
|
||||
assert_template_result(expected, markup, assigns)
|
||||
|
||||
markup = '{% for i in array.items %}{{ i }}{% break %}{% endfor %}'
|
||||
expected = "1"
|
||||
expected = '1'
|
||||
assert_template_result(expected, markup, assigns)
|
||||
|
||||
markup = '{% for i in array.items %}{% break %}{{ i }}{% endfor %}'
|
||||
expected = ""
|
||||
expected = ''
|
||||
assert_template_result(expected, markup, assigns)
|
||||
|
||||
markup = '{% for i in array.items %}{{ i }}{% if i > 3 %}{% break %}{% endif %}{% endfor %}'
|
||||
expected = "1234"
|
||||
expected = '1234'
|
||||
assert_template_result(expected, markup, assigns)
|
||||
|
||||
# tests to ensure it only breaks out of the local for loop
|
||||
@@ -237,23 +267,23 @@ HERE
|
||||
assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5] } }
|
||||
|
||||
markup = '{% for i in array.items %}{% continue %}{% endfor %}'
|
||||
expected = ""
|
||||
expected = ''
|
||||
assert_template_result(expected, markup, assigns)
|
||||
|
||||
markup = '{% for i in array.items %}{{ i }}{% continue %}{% endfor %}'
|
||||
expected = "12345"
|
||||
expected = '12345'
|
||||
assert_template_result(expected, markup, assigns)
|
||||
|
||||
markup = '{% for i in array.items %}{% continue %}{{ i }}{% endfor %}'
|
||||
expected = ""
|
||||
expected = ''
|
||||
assert_template_result(expected, markup, assigns)
|
||||
|
||||
markup = '{% for i in array.items %}{% if i > 3 %}{% continue %}{% endif %}{{ i }}{% endfor %}'
|
||||
expected = "123"
|
||||
expected = '123'
|
||||
assert_template_result(expected, markup, assigns)
|
||||
|
||||
markup = '{% for i in array.items %}{% if i == 3 %}{% continue %}{% else %}{{ i }}{% endif %}{% endfor %}'
|
||||
expected = "1245"
|
||||
expected = '1245'
|
||||
assert_template_result(expected, markup, assigns)
|
||||
|
||||
# tests to ensure it only continues the local for loop and not all of them.
|
||||
@@ -282,41 +312,41 @@ HERE
|
||||
# the functionality for backwards compatibility
|
||||
|
||||
assert_template_result('test string',
|
||||
'{%for val in string%}{{val}}{%endfor%}',
|
||||
'string' => "test string")
|
||||
'{%for val in string%}{{val}}{%endfor%}',
|
||||
'string' => 'test string')
|
||||
|
||||
assert_template_result('test string',
|
||||
'{%for val in string limit:1%}{{val}}{%endfor%}',
|
||||
'string' => "test string")
|
||||
'{%for val in string limit:1%}{{val}}{%endfor%}',
|
||||
'string' => 'test string')
|
||||
|
||||
assert_template_result('val-string-1-1-0-1-0-true-true-test string',
|
||||
'{%for val in string%}' \
|
||||
'{{forloop.name}}-' \
|
||||
'{{forloop.index}}-' \
|
||||
'{{forloop.length}}-' \
|
||||
'{{forloop.index0}}-' \
|
||||
'{{forloop.rindex}}-' \
|
||||
'{{forloop.rindex0}}-' \
|
||||
'{{forloop.first}}-' \
|
||||
'{{forloop.last}}-' \
|
||||
'{{val}}{%endfor%}',
|
||||
'string' => "test string")
|
||||
'{%for val in string%}' \
|
||||
'{{forloop.name}}-' \
|
||||
'{{forloop.index}}-' \
|
||||
'{{forloop.length}}-' \
|
||||
'{{forloop.index0}}-' \
|
||||
'{{forloop.rindex}}-' \
|
||||
'{{forloop.rindex0}}-' \
|
||||
'{{forloop.first}}-' \
|
||||
'{{forloop.last}}-' \
|
||||
'{{val}}{%endfor%}',
|
||||
'string' => 'test string')
|
||||
end
|
||||
|
||||
def test_for_parentloop_references_parent_loop
|
||||
assert_template_result('1.1 1.2 1.3 2.1 2.2 2.3 ',
|
||||
'{% for inner in outer %}{% for k in inner %}' \
|
||||
'{{ forloop.parentloop.index }}.{{ forloop.index }} ' \
|
||||
'{% endfor %}{% endfor %}',
|
||||
'outer' => [[1, 1, 1], [1, 1, 1]])
|
||||
'{% for inner in outer %}{% for k in inner %}' \
|
||||
'{{ forloop.parentloop.index }}.{{ forloop.index }} ' \
|
||||
'{% endfor %}{% endfor %}',
|
||||
'outer' => [[1, 1, 1], [1, 1, 1]])
|
||||
end
|
||||
|
||||
def test_for_parentloop_nil_when_not_present
|
||||
assert_template_result('.1 .2 ',
|
||||
'{% for inner in outer %}' \
|
||||
'{{ forloop.parentloop.index }}.{{ forloop.index }} ' \
|
||||
'{% endfor %}',
|
||||
'outer' => [[1, 1, 1], [1, 1, 1]])
|
||||
'{% for inner in outer %}' \
|
||||
'{{ forloop.parentloop.index }}.{{ forloop.index }} ' \
|
||||
'{% endfor %}',
|
||||
'outer' => [[1, 1, 1], [1, 1, 1]])
|
||||
end
|
||||
|
||||
def test_inner_for_over_empty_input
|
||||
@@ -324,7 +354,7 @@ HERE
|
||||
end
|
||||
|
||||
def test_blank_string_not_iterable
|
||||
assert_template_result('', "{% for char in characters %}I WILL NOT BE OUTPUT{% endfor %}", 'characters' => '')
|
||||
assert_template_result('', '{% for char in characters %}I WILL NOT BE OUTPUT{% endfor %}', 'characters' => '')
|
||||
end
|
||||
|
||||
def test_bad_variable_naming_in_for_loop
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user