Compare commits

..

10 Commits

Author SHA1 Message Date
Arthur Neves
95b340a7cf Bump to version 3.0.1 2015-01-23 10:45:47 -05:00
Arthur Neves
36a8696c07 Add ruby 2.2 to travis
and allow failure on ruby head

Conflicts:
	.travis.yml
2015-01-23 10:43:28 -05:00
Florian Weingarten
cbc163ba1c Merge pull request #506 from Shopify/fix_capture_with_hyphen
Use VariableSignature as Syntax for Capture tag to allow hyphens in variable names
2015-01-23 10:37:52 -05:00
Arthur Nogueira Neves
9faf8f9a56 Merge pull request #504 from alfredxing/duplicate-keys
Remove duplicate `index0` key in TableRow tag
2015-01-23 10:14:19 -05:00
Florian Weingarten
d6db28c854 Revert "Merge pull request #463 from Shopify/stricter-identifiers"
This reverts commit a056f6521c, reversing
changes made to 7843bcca8d.
2014-11-07 01:49:01 +00:00
Florian Weingarten
475ea51f1f Revert "Merge pull request #466 from Shopify/remove-expression-cache"
This reverts commit d9ae36ec40, reversing
changes made to 2da9d49478.
2014-11-07 01:48:51 +00:00
Florian Weingarten
9c33e9601b Revert "Merge pull request #476 from Shopify/missing-variable-name-error"
This reverts commit 4dc682313f, reversing
changes made to a8f60ff6b1.
2014-11-07 01:48:16 +00:00
Florian Weingarten
b242a7273a Revert "Merge pull request #478 from Shopify/numbers-in-identifiers"
This reverts commit 263e90e772, reversing
changes made to 4dc682313f.
2014-11-07 01:48:05 +00:00
Florian Weingarten
4b1835e3c0 Revert "Merge pull request #458 from Shopify/block-body"
This reverts commit 12d526a05c, reversing
changes made to 263e90e772.

Conflicts:
	lib/liquid/block_body.rb
2014-11-07 01:47:47 +00:00
Florian Weingarten
2fe3a21a5d Revert "Merge pull request #479 from Shopify/tweaks-for-c"
This reverts commit aa182f64b4, reversing
changes made to 70c45f8cd8.
2014-11-07 01:46:59 +00:00
122 changed files with 2151 additions and 3974 deletions

1
.gitignore vendored
View File

@@ -6,4 +6,3 @@ pkg
.rvmrc .rvmrc
.ruby-version .ruby-version
Gemfile.lock Gemfile.lock
.bundle

View File

@@ -1,125 +0,0 @@
inherit_from: ./.rubocop_todo.yml
AllCops:
Exclude:
- 'performance/shopify/*'
- 'pkg/**'
Metrics/BlockNesting:
Max: 3
Exclude:
- 'lib/liquid/block_body.rb'
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

View File

@@ -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

View File

@@ -1,29 +1,19 @@
language: ruby
rvm: rvm:
- 1.9
- 2.0
- 2.1 - 2.1
- 2.2 - 2.2
- 2.3.3
- ruby-head - ruby-head
- jruby-19mode
- jruby-head - jruby-head
# - rbx-2 - rbx-2
sudo: false
addons:
apt:
packages:
- libgmp3-dev
matrix: matrix:
allow_failures: allow_failures:
- rvm: rbx-2
- rvm: jruby-head - rvm: jruby-head
- rvm: ruby-head
install: script: "rake test"
- gem install rainbow -v 2.2.1
- bundle install
script: "bundle exec rake"
notifications: notifications:
disable: true disable: true

View File

@@ -4,22 +4,23 @@
* Bugfixes * Bugfixes
* Performance improvements * Performance improvements
* Features that are likely to be useful to the majority of Liquid users * Features which are likely to be useful to the majority of Liquid users
## Things we won't merge ## Things we won't merge
* Code that introduces considerable performance degrations * Code which introduces considerable performance degrations
* Code that touches performance-critical parts of Liquid and comes without benchmarks * Code which 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 which 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) * Features which can easily be implemented on top of Liquid (for example as a custom filter or custom filesystem)
* Code that does not include tests * Code which comes without tests
* Code that breaks existing tests * Code which breaks existing tests
## Workflow ## Workflow
* Fork the Liquid repository * Fork the Liquid repository
* Create a new branch in your fork * Create a new branch in your fork
* If it makes sense, add tests for your code and/or run a performance benchmark * If it makes sense, add tests for your code and run a performance benchmark
* Make sure all tests pass (`bundle exec rake`) * Make sure all tests pass
* Create a pull request * 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.

14
Gemfile
View File

@@ -1,17 +1,9 @@
source 'https://rubygems.org' source 'https://rubygems.org'
gemspec gemspec
gem 'stackprof', platforms: :mri_21
gem 'stackprof', platforms: :mri
group :benchmark, :test do
gem 'benchmark-ips'
end
group :test do group :test do
gem 'rubocop', '0.34.2' gem 'spy', '0.4.1'
gem 'benchmark-ips'
platform :mri do
gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'bd53db95de3d44d631e7c5a267c3d934e66107dd'
end
end end

View File

@@ -1,117 +1,46 @@
# Liquid Change Log # Liquid Version History
## 4.0.0 / 2016-12-14 / branch "4-0-stable" ## 3.0.0 / not yet released / branch "master"
### Changed * ...
* Render an opaque internal error by default for non-Liquid::Error (#835) [Dylan Thacker-Smith] * Removed Block#end_tag. Instead, override parse with `super` followed by your code. See #446 [Dylan Thacker-Smith, dylanahsmith]
* Ruby 2.0 support dropped (#832) [Dylan Thacker-Smith] * Fixed condition with wrong data types, see #423 [Bogdan Gusiev]
* Add to_number Drop method to allow custom drops to work with number filters (#731) * Add url_encode to standard filters, see #421 [Derrick Reimer, djreimer]
* Add strict_variables and strict_filters options to detect undefined references (#691) * Add uniq to standard filters [Florian Weingarten, fw42]
* Improve loop performance (#681) [Florian Weingarten] * Add exception_handler feature, see #397 and #254 [Bogdan Gusiev, bogdan and Florian Weingarten, fw42]
* Rename Drop method `before_method` to `liquid_method_missing` (#661) [Thierry Joyal] * Optimize variable parsing to avoid repeated regex evaluation during template rendering #383 [Jason Hiltz-Laforge, jasonhl]
* Add url_decode filter to invert url_encode (#645) [Larry Archer] * Optimize checking for block interrupts to reduce object allocation #380 [Jason Hiltz-Laforge, jasonhl]
* Add global_filter to apply a filter to all output (#610) [Loren Hale] * Properly set context rethrow_errors on render! #349 [Thierry Joyal, tjoyal]
* Add compact filter (#600) [Carson Reinke] * Fix broken rendering of variables which are equal to false, see #345 [Florian Weingarten, fw42]
* Rename deprecated "has_key?" and "has_interrupt?" methods (#593) [Florian Weingarten] * Remove ActionView template handler [Dylan Thacker-Smith, dylanahsmith]
* Include template name with line numbers in render errors (574) [Dylan Thacker-Smith] * Freeze lots of string literals for new Ruby 2.1 optimization, see #297 [Florian Weingarten, fw42]
* Add sort_natural filter (#554) [Martin Hanzel] * Allow newlines in tags and variables, see #324 [Dylan Thacker-Smith, dylanahsmith]
* Add forloop.parentloop as a reference to the parent loop (#520) [Justin Li] * Tag#parse is called after initialize, which now takes options instead of tokens as the 3rd argument. See #321 [Dylan Thacker-Smith, dylanahsmith]
* Block parsing moved to BlockBody class (#458) [Dylan Thacker-Smith] * Raise `Liquid::ArgumentError` instead of `::ArgumentError` when filter has wrong number of arguments #309 [Bogdan Gusiev, bogdan]
* Add concat filter to concatenate arrays (#429) [Diogo Beato] * Add a to_s default for liquid drops, see #306 [Adam Doeler, releod]
* Ruby 1.9 support dropped (#491) [Justin Li] * Add strip, lstrip, and rstrip to standard filters [Florian Weingarten, fw42]
* Liquid::Template.file_system's read_template_file method is no longer passed the context. (#441) [James Reid-Smith] * Make if, for & case tags return complete and consistent nodelists, see #250 [Nick Jones, dntj]
* Remove `liquid_methods` (See https://github.com/Shopify/liquid/pull/568 for replacement) * Prevent arbitrary method invocation on condition objects, see #274 [Dylan Thacker-Smith, dylanahsmith]
* Liquid::Template.register_filter raises when the module overrides registered public methods as private or protected (#705) [Gaurav Chande] * Don't call to_sym when creating conditions for security reasons, see #273 [Bouke van der Bijl, bouk]
* Fix resource counting bug with respond_to?(:length), see #263 [Florian Weingarten, fw42]
* Allow specifying custom patterns for template filenames, see #284 [Andrei Gladkyi, agladkyi]
* Allow drops to optimize loading a slice of elements, see #282 [Tom Burns, boourns]
* Support for passing variables to snippets in subdirs, see #271 [Joost Hietbrink, joost]
* Add a class cache to avoid runtime extend calls, see #249 [James Tucker, raggi]
* Remove some legacy Ruby 1.8 compatibility code, see #276 [Florian Weingarten, fw42]
* Add default filter to standard filters, see #267 [Derrick Reimer, djreimer]
* Add optional strict parsing and warn parsing, see #235 [Tristan Hume, trishume]
* Add I18n syntax error translation, see #241 [Simon Hørup Eskildsen, Sirupsen]
* Make sort filter work on enumerable drops, see #239 [Florian Weingarten, fw42]
* Fix clashing method names in enumerable drops, see #238 [Florian Weingarten, fw42]
* Make map filter work on enumerable drops, see #233 [Florian Weingarten, fw42]
* Improved whitespace stripping for blank blocks, related to #216 [Florian Weingarten, fw42]
### Fixed ## 2.6.1 / 2014-01-10 / branch "2-6-stable"
* 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]
* Fix sort filter behaviour with empty array input (#652) [Marcel Cary]
* Fix test failure under certain timezones (#631) [Dylan Thacker-Smith]
* Fix bug in uniq filter (#595) [Florian Weingarten]
* Fix bug when "blank" and "empty" are used as variable names (#592) [Florian Weingarten]
* Fix condition parse order in strict mode (#569) [Justin Li]
* Fix naming of the "context variable" when dynamically including a template (#559) [Justin Li]
* Gracefully accept empty strings in the date filter (#555) [Loren Hale]
* Fix capturing into variables with a hyphen in the name (#505) [Florian Weingarten]
* Fix case sensitivity regression in date standard filter (#499) [Kelley Reynolds]
* Disallow filters with no variable in strict mode (#475) [Justin Li]
* Disallow variable names in the strict parser that are not valid in the lax parser (#463) [Justin Li]
* Fix BlockBody#warnings taking exponential time to compute (#486) [Justin Li]
## 3.0.5 / 2015-07-23 / branch "3-0-stable"
* Fix test failure under certain timezones [Dylan Thacker-Smith]
## 3.0.4 / 2015-07-17
* Fix chained access to multi-dimensional hashes [Florian Weingarten]
## 3.0.3 / 2015-05-28
* Fix condition parse order in strict mode (#569) [Justin Li]
## 3.0.2 / 2015-04-24
* Expose VariableLookup private members (#551) [Justin Li]
* Documentation fixes
## 3.0.1 / 2015-01-23
* Remove duplicate `index0` key in TableRow tag (#502) [Alfred Xing]
## 3.0.0 / 2014-11-12
* Removed Block#end_tag. Instead, override parse with `super` followed by your code. See #446 [Dylan Thacker-Smith]
* Fixed condition with wrong data types (#423) [Bogdan Gusiev]
* Add url_encode to standard filters (#421) [Derrick Reimer]
* Add uniq to standard filters [Florian Weingarten]
* Add exception_handler feature (#397) and #254 [Bogdan Gusiev, Florian Weingarten]
* Optimize variable parsing to avoid repeated regex evaluation during template rendering #383 [Jason Hiltz-Laforge]
* Optimize checking for block interrupts to reduce object allocation #380 [Jason Hiltz-Laforge]
* Properly set context rethrow_errors on render! #349 [Thierry Joyal]
* Fix broken rendering of variables which are equal to false (#345) [Florian Weingarten]
* Remove ActionView template handler [Dylan Thacker-Smith]
* Freeze lots of string literals for new Ruby 2.1 optimization (#297) [Florian Weingarten]
* Allow newlines in tags and variables (#324) [Dylan Thacker-Smith]
* Tag#parse is called after initialize, which now takes options instead of tokens as the 3rd argument. See #321 [Dylan Thacker-Smith]
* Raise `Liquid::ArgumentError` instead of `::ArgumentError` when filter has wrong number of arguments #309 [Bogdan Gusiev]
* Add a to_s default for liquid drops (#306) [Adam Doeler]
* Add strip, lstrip, and rstrip to standard filters [Florian Weingarten]
* Make if, for & case tags return complete and consistent nodelists (#250) [Nick Jones]
* Prevent arbitrary method invocation on condition objects (#274) [Dylan Thacker-Smith]
* Don't call to_sym when creating conditions for security reasons (#273) [Bouke van der Bijl]
* Fix resource counting bug with respond_to?(:length) (#263) [Florian Weingarten]
* Allow specifying custom patterns for template filenames (#284) [Andrei Gladkyi]
* Allow drops to optimize loading a slice of elements (#282) [Tom Burns]
* Support for passing variables to snippets in subdirs (#271) [Joost Hietbrink]
* Add a class cache to avoid runtime extend calls (#249) [James Tucker]
* Remove some legacy Ruby 1.8 compatibility code (#276) [Florian Weingarten]
* Add default filter to standard filters (#267) [Derrick Reimer]
* Add optional strict parsing and warn parsing (#235) [Tristan Hume]
* Add I18n syntax error translation (#241) [Simon Hørup Eskildsen, Sirupsen]
* Make sort filter work on enumerable drops (#239) [Florian Weingarten]
* Fix clashing method names in enumerable drops (#238) [Florian Weingarten]
* Make map filter work on enumerable drops (#233) [Florian Weingarten]
* Improved whitespace stripping for blank blocks, related to #216 [Florian Weingarten]
## 2.6.3 / 2015-07-23 / branch "2-6-stable"
* Fix test failure under certain timezones [Dylan Thacker-Smith]
## 2.6.2 / 2015-01-23
* Remove duplicate hash key [Parker Moore]
## 2.6.1 / 2014-01-10
Security fix, cherry-picked from master (4e14a65): Security fix, cherry-picked from master (4e14a65):
* Don't call to_sym when creating conditions for security reasons (#273) [Bouke van der Bijl] * Don't call to_sym when creating conditions for security reasons, see #273 [Bouke van der Bijl, bouk]
* Prevent arbitrary method invocation on condition objects (#274) [Dylan Thacker-Smith] * Prevent arbitrary method invocation on condition objects, see #274 [Dylan Thacker-Smith, dylanahsmith]
## 2.6.0 / 2013-11-25 ## 2.6.0 / 2013-11-25
@@ -119,37 +48,37 @@ IMPORTANT: Liquid 2.6 is going to be the last version of Liquid which maintains
The following releases will only be tested against Ruby 1.9 and Ruby 2.0 and are likely to break on Ruby 1.8. The following releases will only be tested against Ruby 1.9 and Ruby 2.0 and are likely to break on Ruby 1.8.
* Bugfix for #106: fix example servlet [gnowoel] * Bugfix for #106: fix example servlet [gnowoel]
* Bugfix for #97: strip_html filter supports multi-line tags [Jo Liss] * Bugfix for #97: strip_html filter supports multi-line tags [Jo Liss, joliss]
* Bugfix for #114: strip_html filter supports style tags [James Allardice] * Bugfix for #114: strip_html filter supports style tags [James Allardice, jamesallardice]
* Bugfix for #117: 'now' support for date filter in Ruby 1.9 [Notre Dame Webgroup] * Bugfix for #117: 'now' support for date filter in Ruby 1.9 [Notre Dame Webgroup, ndwebgroup]
* Bugfix for #166: truncate filter on UTF-8 strings with Ruby 1.8 [Florian Weingarten] * Bugfix for #166: truncate filter on UTF-8 strings with Ruby 1.8 [Florian Weingarten, fw42]
* Bugfix for #204: 'raw' parsing bug [Florian Weingarten] * Bugfix for #204: 'raw' parsing bug [Florian Weingarten, fw42]
* Bugfix for #150: 'for' parsing bug [Peter Schröder] * Bugfix for #150: 'for' parsing bug [Peter Schröder, phoet]
* Bugfix for #126: Strip CRLF in strip_newline [Peter Schröder] * Bugfix for #126: Strip CRLF in strip_newline [Peter Schröder, phoet]
* Bugfix for #174, "can't convert Fixnum into String" for "replace" [jsw0528] * Bugfix for #174, "can't convert Fixnum into String" for "replace" [wǒ_is神仙, jsw0528]
* Allow a Liquid::Drop to be passed into Template#render [Daniel Huckstep] * Allow a Liquid::Drop to be passed into Template#render [Daniel Huckstep, darkhelmet]
* Resource limits [Florian Weingarten] * Resource limits [Florian Weingarten, fw42]
* Add reverse filter [Jay Strybis] * Add reverse filter [Jay Strybis, unreal]
* Add utf-8 support * Add utf-8 support
* Use array instead of Hash to keep the registered filters [Tasos Stathopoulos] * Use array instead of Hash to keep the registered filters [Tasos Stathopoulos, astathopoulos]
* Cache tokenized partial templates [Tom Burns] * Cache tokenized partial templates [Tom Burns, boourns]
* Avoid warnings in Ruby 1.9.3 [Marcus Stollsteimer] * Avoid warnings in Ruby 1.9.3 [Marcus Stollsteimer, stomar]
* Better documentation for 'include' tag (closes #163) [Peter Schröder] * Better documentation for 'include' tag (closes #163) [Peter Schröder, phoet]
* Use of BigDecimal on filters to have better precision (closes #155) [Arthur Nogueira Neves] * Use of BigDecimal on filters to have better precision (closes #155) [Arthur Nogueira Neves, arthurnn]
## 2.5.5 / 2014-01-10 / branch "2-5-stable" ## 2.5.5 / 2014-01-10 / branch "2-5-stable"
Security fix, cherry-picked from master (4e14a65): Security fix, cherry-picked from master (4e14a65):
* Don't call to_sym when creating conditions for security reasons (#273) [Bouke van der Bijl] * Don't call to_sym when creating conditions for security reasons, see #273 [Bouke van der Bijl, bouk]
* Prevent arbitrary method invocation on condition objects (#274) [Dylan Thacker-Smith] * Prevent arbitrary method invocation on condition objects, see #274 [Dylan Thacker-Smith, dylanahsmith]
## 2.5.4 / 2013-11-11 ## 2.5.4 / 2013-11-11
* Fix "can't convert Fixnum into String" for "replace" (#173), [jsw0528] * Fix "can't convert Fixnum into String" for "replace", see #173, [wǒ_is神仙, jsw0528]
## 2.5.3 / 2013-10-09 ## 2.5.3 / 2013-10-09
* #232, #234, #237: Fix map filter bugs [Florian Weingarten] * #232, #234, #237: Fix map filter bugs [Florian Weingarten, fw42]
## 2.5.2 / 2013-09-03 / deleted ## 2.5.2 / 2013-09-03 / deleted
@@ -157,7 +86,7 @@ Yanked from rubygems, as it contained too many changes that broke compatibility.
## 2.5.1 / 2013-07-24 ## 2.5.1 / 2013-07-24
* #230: Fix security issue with map filter, Use invoke_drop in map filter [Florian Weingarten] * #230: Fix security issue with map filter, Use invoke_drop in map filter [Florian Weingarten, fw42]
## 2.5.0 / 2013-03-06 ## 2.5.0 / 2013-03-06

View File

@@ -73,34 +73,3 @@ 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 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. 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
```

View File

@@ -3,7 +3,7 @@ require 'rake/testtask'
$LOAD_PATH.unshift File.expand_path("../lib", __FILE__) $LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
require "liquid/version" require "liquid/version"
task default: [:rubocop, :test] task :default => 'test'
desc 'run test suite with default parser' desc 'run test suite with default parser'
Rake::TestTask.new(:base_test) do |t| Rake::TestTask.new(:base_test) do |t|
@@ -18,43 +18,25 @@ task :warn_test do
Rake::Task['base_test'].invoke Rake::Task['base_test'].invoke
end end
task :rubocop do
require 'rubocop/rake_task'
RuboCop::RakeTask.new
end
desc 'runs test suite with both strict and lax parsers' desc 'runs test suite with both strict and lax parsers'
task :test do task :test do
ENV['LIQUID_PARSER_MODE'] = 'lax' ENV['LIQUID_PARSER_MODE'] = 'lax'
Rake::Task['base_test'].invoke Rake::Task['base_test'].invoke
ENV['LIQUID_PARSER_MODE'] = 'strict' ENV['LIQUID_PARSER_MODE'] = 'strict'
Rake::Task['base_test'].reenable Rake::Task['base_test'].reenable
Rake::Task['base_test'].invoke Rake::Task['base_test'].invoke
if RUBY_ENGINE == 'ruby'
ENV['LIQUID-C'] = '1'
ENV['LIQUID_PARSER_MODE'] = 'lax'
Rake::Task['base_test'].reenable
Rake::Task['base_test'].invoke
ENV['LIQUID_PARSER_MODE'] = 'strict'
Rake::Task['base_test'].reenable
Rake::Task['base_test'].invoke
end
end end
task gem: :build task :gem => :build
task :build do task :build do
system "gem build liquid.gemspec" system "gem build liquid.gemspec"
end end
task install: :build do task :install => :build do
system "gem install liquid-#{Liquid::VERSION}.gem" system "gem install liquid-#{Liquid::VERSION}.gem"
end end
task release: :build do task :release => :build do
system "git tag -a v#{Liquid::VERSION} -m 'Tagging #{Liquid::VERSION}'" 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 "gem push liquid-#{Liquid::VERSION}.gem"
@@ -62,6 +44,7 @@ task release: :build do
end end
namespace :benchmark do namespace :benchmark do
desc "Run the liquid benchmark with lax parsing" desc "Run the liquid benchmark with lax parsing"
task :run do task :run do
ruby "./performance/benchmark.rb lax" ruby "./performance/benchmark.rb lax"
@@ -73,7 +56,9 @@ namespace :benchmark do
end end
end end
namespace :profile do namespace :profile do
desc "Run the liquid profile/performance coverage" desc "Run the liquid profile/performance coverage"
task :run do task :run do
ruby "./performance/profile.rb" ruby "./performance/profile.rb"
@@ -83,13 +68,10 @@ namespace :profile do
task :strict do task :strict do
ruby "./performance/profile.rb strict" ruby "./performance/profile.rb strict"
end end
end end
desc "Run example" desc "Run example"
task :example do task :example do
ruby "-w -d -Ilib example/server/server.rb" ruby "-w -d -Ilib example/server/server.rb"
end end
task :console do
exec 'irb -I lib -r liquid'
end

View File

@@ -1,3 +0,0 @@
machine:
ruby:
version: ruby-2.1

View File

@@ -4,7 +4,7 @@ module ProductsFilter
end end
def prettyprint(text) def prettyprint(text)
text.gsub(/\*(.*)\*/, '<b>\1</b>') text.gsub( /\*(.*)\*/, '<b>\1</b>' )
end end
def count(array) def count(array)
@@ -17,28 +17,25 @@ module ProductsFilter
end end
class Servlet < LiquidServlet class Servlet < LiquidServlet
def index def index
{ 'date' => Time.now } { 'date' => Time.now }
end end
def products def products
{ 'products' => products_list, 'more_products' => more_products_list, 'description' => description, 'section' => 'Snowboards', 'cool_products' => true } { 'products' => products_list, 'description' => description, 'section' => 'Snowboards', 'cool_products' => true}
end end
private private
def products_list def products_list
[{ 'name' => 'Arbor Draft', 'price' => 39900, 'description' => 'the *arbor draft* is a excellent product' }, [{'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 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 Diamond', 'price' => 59900, 'description' => 'the *arbor diamond* is a made up product because im obsessed with arbor and have no creativity'}]
end
def more_products_list
[{ 'name' => 'Arbor Catalyst', 'price' => 39900, 'description' => 'the *arbor catalyst* is an advanced drop-through for freestyle and flatground performance and versatility' },
{ 'name' => 'Arbor Fish', 'price' => 40000, 'description' => 'the *arbor fish* is a compact pin that features an extended wheelbase and time-honored teardrop shape' }]
end end
def description 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
end end

View File

@@ -1,4 +1,5 @@
class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet
def do_GET(req, res) def do_GET(req, res)
handle(:get, req, res) handle(:get, req, res)
end end
@@ -19,10 +20,10 @@ class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet
@response['Content-Type'] = "text/html" @response['Content-Type'] = "text/html"
@response.status = 200 @response.status = 200
@response.body = Liquid::Template.parse(read_template).render(@assigns, filters: [ProductsFilter]) @response.body = Liquid::Template.parse(read_template).render(@assigns, :filters => [ProductsFilter])
end end
def read_template(filename = @action) def read_template(filename = @action)
File.read("#{__dir__}/templates/#{filename}.liquid") File.read( File.dirname(__FILE__) + "/templates/#{filename}.liquid" )
end end
end end

View File

@@ -1,12 +1,14 @@
require 'webrick' require 'webrick'
require 'rexml/document' require 'rexml/document'
require_relative '../../lib/liquid' DIR = File.expand_path(File.dirname(__FILE__))
require_relative 'liquid_servlet'
require_relative 'example_servlet' require DIR + '/../../lib/liquid'
require DIR + '/liquid_servlet'
require DIR + '/example_servlet'
# Setup webrick # Setup webrick
server = WEBrick::HTTPServer.new(Port: ARGV[1] || 3000) server = WEBrick::HTTPServer.new( :Port => ARGV[1] || 3000 )
server.mount('/', Servlet) server.mount('/', Servlet)
trap("INT"){ server.shutdown } trap("INT"){ server.shutdown }
server.start server.start

View File

@@ -16,12 +16,12 @@
</head> </head>
<body> <body>
{% assign all_products = products | concat: more_products %}
<h1>{{ description | split: '~' | first }}</h1> <h1>{{ description | split: '~' | first }}</h1>
<h2>{{ description | split: '~' | last }}</h2> <h2>{{ description | split: '~' | last }}</h2>
<h2>There are currently {{all_products | count}} products in the {{section}} catalog</h2> <h2>There are currently {{products | count}} products in the {{section}} catalog</h2>
{% if cool_products %} {% if cool_products %}
Cool products :) Cool products :)
@@ -31,7 +31,7 @@
<ul id="products"> <ul id="products">
{% for product in all_products %} {% for product in products %}
<li> <li>
<h2>{{product.name}}</h2> <h2>{{product.name}}</h2>
Only {{product.price | price }} Only {{product.price | price }}

View File

@@ -24,7 +24,6 @@ module Liquid
ArgumentSeparator = ','.freeze ArgumentSeparator = ','.freeze
FilterArgumentSeparator = ':'.freeze FilterArgumentSeparator = ':'.freeze
VariableAttributeSeparator = '.'.freeze VariableAttributeSeparator = '.'.freeze
WhitespaceControl = '-'.freeze
TagStart = /\{\%/ TagStart = /\{\%/
TagEnd = /\%\}/ TagEnd = /\%\}/
VariableSignature = /\(?[\w\-\.\[\]]\)?/ VariableSignature = /\(?[\w\-\.\[\]]\)?/
@@ -35,7 +34,7 @@ module Liquid
QuotedString = /"[^"]*"|'[^']*'/ QuotedString = /"[^"]*"|'[^']*'/
QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o
TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/o TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/o
AnyStartingTag = /#{TagStart}|#{VariableStart}/o AnyStartingTag = /\{\{|\{\%/
PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om
VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o
@@ -49,8 +48,6 @@ require 'liquid/lexer'
require 'liquid/parser' require 'liquid/parser'
require 'liquid/i18n' require 'liquid/i18n'
require 'liquid/drop' require 'liquid/drop'
require 'liquid/tablerowloop_drop'
require 'liquid/forloop_drop'
require 'liquid/extensions' require 'liquid/extensions'
require 'liquid/errors' require 'liquid/errors'
require 'liquid/interrupts' require 'liquid/interrupts'
@@ -60,20 +57,21 @@ require 'liquid/context'
require 'liquid/parser_switching' require 'liquid/parser_switching'
require 'liquid/tag' require 'liquid/tag'
require 'liquid/block' require 'liquid/block'
require 'liquid/block_body'
require 'liquid/document' require 'liquid/document'
require 'liquid/variable' require 'liquid/variable'
require 'liquid/variable_lookup' require 'liquid/variable_lookup'
require 'liquid/range_lookup' require 'liquid/range_lookup'
require 'liquid/file_system' require 'liquid/file_system'
require 'liquid/resource_limits'
require 'liquid/template' require 'liquid/template'
require 'liquid/standardfilters' require 'liquid/standardfilters'
require 'liquid/condition' require 'liquid/condition'
require 'liquid/module_ex'
require 'liquid/utils' require 'liquid/utils'
require 'liquid/tokenizer' require 'liquid/token'
require 'liquid/parse_context'
# Load all the tags of the standard library # Load all the tags of the standard library
# #
Dir["#{__dir__}/liquid/tags/*.rb"].each { |f| require f } Dir[File.dirname(__FILE__) + '/liquid/tags/*.rb'].each { |f| require f }
require 'liquid/profiler'
require 'liquid/profiler/hooks'

View File

@@ -1,41 +1,90 @@
module Liquid module Liquid
class Block < Tag class Block < Tag
MAX_DEPTH = 100 FullToken = /\A#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
ContentOfVariable = /\A#{VariableStart}(.*)#{VariableEnd}\z/om
def initialize(tag_name, markup, options) TAGSTART = "{%".freeze
super VARSTART = "{{".freeze
@blank = true
end
def parse(tokens)
@body = BlockBody.new
while parse_body(@body, tokens)
end
end
def render(context)
@body.render(context)
end
def blank? def blank?
@blank @blank
end end
def nodelist def parse(tokens)
@body.nodelist @blank = true
@nodelist ||= []
@nodelist.clear
while token = tokens.shift
begin
unless token.empty?
case
when token.start_with?(TAGSTART)
if token =~ FullToken
# if we found the proper block delimiter just end parsing here and let the outer block
# proceed
return if block_delimiter == $1
# fetch the tag from registered blocks
if tag = Template.tags[$1]
markup = token.is_a?(Token) ? token.child($2) : $2
new_tag = tag.parse($1, markup, tokens, @options)
new_tag.line_number = token.line_number if token.is_a?(Token)
@blank &&= new_tag.blank?
@nodelist << new_tag
else
# this tag is not registered with the system
# pass it to the current block for special handling or error reporting
unknown_tag($1, $2, tokens)
end
else
raise SyntaxError.new(options[:locale].t("errors.syntax.tag_termination".freeze, :token => token, :tag_end => TagEnd.inspect))
end
when token.start_with?(VARSTART)
new_var = create_variable(token)
new_var.line_number = token.line_number if token.is_a?(Token)
@nodelist << new_var
@blank = false
else
@nodelist << token
@blank &&= (token =~ /\A\s*\z/)
end
end
rescue SyntaxError => e
e.set_line_number_from_token(token)
raise
end
end
# Make sure that it's ok to end parsing in the current block.
# Effectively this method will throw an exception unless the current block is
# of type Document
assert_missing_delimitation!
end end
def unknown_tag(tag, _params, _tokens) # warnings of this block and all sub-tags
def warnings
all_warnings = []
all_warnings.concat(@warnings) if @warnings
(nodelist || []).each do |node|
all_warnings.concat(node.warnings || []) if node.respond_to?(:warnings)
end
all_warnings
end
def unknown_tag(tag, params, tokens)
case tag case tag
when 'else'.freeze when 'else'.freeze
raise SyntaxError.new(parse_context.locale.t("errors.syntax.unexpected_else".freeze, raise SyntaxError.new(options[:locale].t("errors.syntax.unexpected_else".freeze,
block_name: block_name)) :block_name => block_name))
when 'end'.freeze when 'end'.freeze
raise SyntaxError.new(parse_context.locale.t("errors.syntax.invalid_delimiter".freeze, raise SyntaxError.new(options[:locale].t("errors.syntax.invalid_delimiter".freeze,
block_name: block_name, :block_name => block_name,
block_delimiter: block_delimiter)) :block_delimiter => block_delimiter))
else else
raise SyntaxError.new(parse_context.locale.t("errors.syntax.unknown_tag".freeze, tag: tag)) raise SyntaxError.new(options[:locale].t("errors.syntax.unknown_tag".freeze, :tag => tag))
end end
end end
@@ -47,31 +96,65 @@ module Liquid
@block_delimiter ||= "end#{block_name}" @block_delimiter ||= "end#{block_name}"
end end
def create_variable(token)
token.scan(ContentOfVariable) do |content|
markup = token.is_a?(Token) ? token.child(content.first) : content.first
return Variable.new(markup, @options)
end
raise SyntaxError.new(options[:locale].t("errors.syntax.variable_termination".freeze, :token => token, :tag_end => VariableEnd.inspect))
end
def render(context)
render_all(@nodelist, context)
end
protected protected
def parse_body(body, tokens) def assert_missing_delimitation!
if parse_context.depth >= MAX_DEPTH raise SyntaxError.new(options[:locale].t("errors.syntax.tag_never_closed".freeze, :block_name => block_name))
raise StackLevelError, "Nesting too deep".freeze end
end
parse_context.depth += 1
begin
body.parse(tokens, parse_context) do |end_tag_name, end_tag_params|
@blank &&= body.blank?
return false if end_tag_name == block_delimiter def render_all(list, context)
unless end_tag_name output = []
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name)) context.resource_limits[:render_length_current] = 0
context.resource_limits[:render_score_current] += list.length
list.each do |token|
# Break out if we have any unhanded interrupts.
break if context.has_interrupt?
begin
# 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 or token.is_a? Break
context.push_interrupt(token.interrupt)
break
end end
# this tag is not registered with the system token_output = render_token(token, context)
# pass it to the current block for special handling or error reporting
unknown_tag(end_tag_name, end_tag_params, tokens) unless token.is_a?(Block) && token.blank?
output << token_output
end
rescue MemoryError => e
raise e
rescue ::StandardError => e
output << (context.handle_error(e, token))
end end
ensure
parse_context.depth -= 1
end end
true output.join
end
def render_token(token, context)
token_output = (token.respond_to?(:render) ? token.render(context) : token)
context.increment_used_resources(:render_length_current, token_output)
if context.resource_limits_reached?
context.resource_limits[:reached] = true
raise MemoryError.new("Memory limits exceeded".freeze)
end
token_output
end end
end end
end end

View File

@@ -1,7 +1,7 @@
module Liquid module Liquid
class BlockBody class BlockBody
FullToken = /\A#{TagStart}#{WhitespaceControl}?\s*(\w+)\s*(.*?)#{WhitespaceControl}?#{TagEnd}\z/om FullToken = /\A#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om ContentOfVariable = /\A#{VariableStart}(.*)#{VariableEnd}\z/om
TAGSTART = "{%".freeze TAGSTART = "{%".freeze
VARSTART = "{{".freeze VARSTART = "{{".freeze
@@ -12,92 +12,88 @@ module Liquid
@blank = true @blank = true
end end
def parse(tokenizer, parse_context) def parse(tokens, options)
parse_context.line_number = tokenizer.line_number while token = tokens.shift
while token = tokenizer.shift begin
unless token.empty? unless token.empty?
case case
when token.start_with?(TAGSTART) when token.start_with?(TAGSTART)
whitespace_handler(token, parse_context) if token =~ FullToken
if token =~ FullToken tag_name = $1
tag_name = $1 markup = $2
markup = $2 # fetch the tag from registered blocks
# fetch the tag from registered blocks if tag = Template.tags[tag_name]
if tag = registered_tags[tag_name] markup = token.child(markup) if token.is_a?(Token)
new_tag = tag.parse(tag_name, markup, tokenizer, parse_context) new_tag = tag.parse(tag_name, markup, tokens, options)
@blank &&= new_tag.blank? new_tag.line_number = token.line_number if token.is_a?(Token)
@nodelist << new_tag @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 else
# end parsing if we reach an unknown tag and let the caller decide raise SyntaxError.new(options[:locale].t("errors.syntax.tag_termination".freeze, :token => token, :tag_end => TagEnd.inspect))
# determine how to proceed
return yield tag_name, markup
end end
when token.start_with?(VARSTART)
new_var = create_variable(token, options)
new_var.line_number = token.line_number if token.is_a?(Token)
@nodelist << new_var
@blank = false
else else
raise_missing_tag_terminator(token, parse_context) @nodelist << token
@blank &&= !!(token =~ /\A\s*\z/)
end end
when token.start_with?(VARSTART)
whitespace_handler(token, parse_context)
@nodelist << create_variable(token, parse_context)
@blank = false
else
if parse_context.trim_whitespace
token.lstrip!
end
parse_context.trim_whitespace = false
@nodelist << token
@blank &&= !!(token =~ /\A\s*\z/)
end end
rescue SyntaxError => e
e.set_line_number_from_token(token)
raise
end end
parse_context.line_number = tokenizer.line_number
end end
yield nil, nil yield nil, nil
end end
def whitespace_handler(token, parse_context)
if token[2] == WhitespaceControl
previous_token = @nodelist.last
if previous_token.is_a? String
previous_token.rstrip!
end
end
parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
end
def blank? def blank?
@blank @blank
end end
def warnings
all_warnings = []
nodelist.each do |node|
all_warnings.concat(node.warnings) if node.respond_to?(:warnings) && node.warnings
end
all_warnings
end
def render(context) def render(context)
output = [] output = []
context.resource_limits.render_score += @nodelist.length context.resource_limits[:render_length_current] = 0
context.resource_limits[:render_score_current] += @nodelist.length
@nodelist.each do |token| @nodelist.each do |token|
# Break out if we have any unhanded interrupts. # Break out if we have any unhanded interrupts.
break if context.interrupt? break if context.has_interrupt?
begin begin
# If we get an Interrupt that means the block must stop processing. An # If we get an Interrupt that means the block must stop processing. An
# Interrupt is any command that stops block execution such as {% break %} # Interrupt is any command that stops block execution such as {% break %}
# or {% continue %} # or {% continue %}
if token.is_a?(Continue) || token.is_a?(Break) if token.is_a?(Continue) or token.is_a?(Break)
context.push_interrupt(token.interrupt) context.push_interrupt(token.interrupt)
break break
end end
node_output = render_node(token, context) token_output = render_token(token, context)
unless token.is_a?(Block) && token.blank? unless token.is_a?(Block) && token.blank?
output << node_output output << token_output
end end
rescue MemoryError => e rescue MemoryError => e
raise e raise e
rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
context.handle_error(e, token.line_number)
output << nil
rescue ::StandardError => e rescue ::StandardError => e
line_number = token.is_a?(String) ? nil : token.line_number output << context.handle_error(e, token)
output << context.handle_error(e, line_number)
end end
end end
@@ -106,35 +102,22 @@ module Liquid
private private
def render_node(node, context) def render_token(token, context)
node_output = node.is_a?(String) ? node : node.render(context) token_output = (token.respond_to?(:render) ? token.render(context) : token)
node_output = node_output.is_a?(Array) ? node_output.join : node_output.to_s context.increment_used_resources(:render_length_current, token_output)
if context.resource_limits_reached?
context.resource_limits.render_length += node_output.length context.resource_limits[:reached] = true
if context.resource_limits.reached?
raise MemoryError.new("Memory limits exceeded".freeze) raise MemoryError.new("Memory limits exceeded".freeze)
end end
node_output token_output
end end
def create_variable(token, parse_context) def create_variable(token, options)
token.scan(ContentOfVariable) do |content| token.scan(ContentOfVariable) do |content|
markup = content.first markup = token.is_a?(Token) ? token.child(content.first) : content.first
return Variable.new(markup, parse_context) return Variable.new(markup, options)
end end
raise_missing_variable_terminator(token, parse_context) raise SyntaxError.new(options[:locale].t("errors.syntax.variable_termination".freeze, :token => token, :tag_end => VariableEnd.inspect))
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))
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))
end
def registered_tags
Template.tags
end end
end end
end end

View File

@@ -3,26 +3,21 @@ module Liquid
# #
# Example: # Example:
# #
# c = Condition.new(1, '==', 1) # c = Condition.new('1', '==', '1')
# c.evaluate #=> true # c.evaluate #=> true
# #
class Condition #:nodoc: class Condition #:nodoc:
@@operators = { @@operators = {
'=='.freeze => ->(cond, left, right) { cond.send(:equal_variables, left, right) }, '=='.freeze => lambda { |cond, left, right| cond.send(:equal_variables, left, right) },
'!='.freeze => ->(cond, left, right) { !cond.send(:equal_variables, left, right) }, '!='.freeze => lambda { |cond, left, right| !cond.send(:equal_variables, left, right) },
'<>'.freeze => ->(cond, left, right) { !cond.send(:equal_variables, left, right) }, '<>'.freeze => lambda { |cond, left, right| !cond.send(:equal_variables, left, right) },
'<'.freeze => :<, '<'.freeze => :<,
'>'.freeze => :>, '>'.freeze => :>,
'>='.freeze => :>=, '>='.freeze => :>=,
'<='.freeze => :<=, '<='.freeze => :<=,
'contains'.freeze => lambda do |cond, left, right| 'contains'.freeze => lambda { |cond, left, right|
if left && right && left.respond_to?(:include?) left && right && left.respond_to?(:include?) ? left.include?(right) : false
right = right.to_s if left.is_a?(String) }
left.include?(right)
else
false
end
end
} }
def self.operators def self.operators
@@ -41,22 +36,16 @@ module Liquid
end end
def evaluate(context = Context.new) def evaluate(context = Context.new)
condition = self result = interpret_condition(left, right, operator, context)
result = nil
loop do
result = interpret_condition(condition.left, condition.right, condition.operator, context)
case condition.child_relation case @child_relation
when :or when :or
break if result result || @child_condition.evaluate(context)
when :and when :and
break unless result result && @child_condition.evaluate(context)
else else
break result
end
condition = condition.child_condition
end end
result
end end
def or(condition) def or(condition)
@@ -81,24 +70,20 @@ module Liquid
"#<Condition #{[@left, @operator, @right].compact.join(' '.freeze)}>" "#<Condition #{[@left, @operator, @right].compact.join(' '.freeze)}>"
end end
protected
attr_reader :child_relation, :child_condition
private private
def equal_variables(left, right) def equal_variables(left, right)
if left.is_a?(Liquid::Expression::MethodLiteral) if left.is_a?(Symbol)
if right.respond_to?(left.method_name) if right.respond_to?(left)
return right.send(left.method_name) return right.send(left.to_s)
else else
return nil return nil
end end
end end
if right.is_a?(Liquid::Expression::MethodLiteral) if right.is_a?(Symbol)
if left.respond_to?(right.method_name) if left.respond_to?(right)
return left.send(right.method_name) return left.send(right.to_s)
else else
return nil return nil
end end
@@ -111,32 +96,36 @@ module Liquid
# If the operator is empty this means that the decision statement is just # If the operator is empty this means that the decision statement is just
# a single variable. We can just poll this variable from the context and # a single variable. We can just poll this variable from the context and
# return this as the result. # return this as the result.
return context.evaluate(left) if op.nil? return context[left] if op == nil
left = context.evaluate(left) left = context[left]
right = context.evaluate(right) right = context[right]
operation = self.class.operators[op] || raise(Liquid::ArgumentError.new("Unknown operator #{op}")) operation = self.class.operators[op] || raise(Liquid::ArgumentError.new("Unknown operator #{op}"))
if operation.respond_to?(:call) if operation.respond_to?(:call)
operation.call(self, left, right) operation.call(self, left, right)
elsif left.respond_to?(operation) && right.respond_to?(operation) && !left.is_a?(Hash) && !right.is_a?(Hash) elsif left.respond_to?(operation) and right.respond_to?(operation)
begin begin
left.send(operation, right) left.send(operation, right)
rescue ::ArgumentError => e rescue ::ArgumentError => e
raise Liquid::ArgumentError.new(e.message) raise Liquid::ArgumentError.new(e.message)
end end
else
nil
end end
end end
end end
class ElseCondition < Condition class ElseCondition < Condition
def else? def else?
true true
end end
def evaluate(_context) def evaluate(context)
true true
end end
end end
end end

View File

@@ -1,4 +1,5 @@
module Liquid module Liquid
# Context keeps the variable stack and resolves variables, as well as keywords # Context keeps the variable stack and resolves variables, as well as keywords
# #
# context['variable'] = 'testing' # context['variable'] = 'testing'
@@ -13,32 +14,41 @@ module Liquid
# context['bob'] #=> nil class Context # context['bob'] #=> nil class Context
class Context class Context
attr_reader :scopes, :errors, :registers, :environments, :resource_limits attr_reader :scopes, :errors, :registers, :environments, :resource_limits
attr_accessor :exception_renderer, :template_name, :partial, :global_filter, :strict_variables, :strict_filters attr_accessor :exception_handler
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil) def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil)
@environments = [environments].flatten @environments = [environments].flatten
@scopes = [(outer_scope || {})] @scopes = [(outer_scope || {})]
@registers = registers @registers = registers
@errors = [] @errors = []
@partial = false @resource_limits = resource_limits || Template.default_resource_limits.dup
@strict_variables = false @resource_limits[:render_score_current] = 0
@resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits) @resource_limits[:assign_score_current] = 0
@parsed_expression = Hash.new{ |cache, markup| cache[markup] = Expression.parse(markup) }
squash_instance_assigns_with_environments squash_instance_assigns_with_environments
@this_stack_used = false @this_stack_used = false
self.exception_renderer = Template.default_exception_renderer
if rethrow_errors if rethrow_errors
self.exception_renderer = ->(e) { raise } self.exception_handler = ->(e) { true }
end end
@interrupts = [] @interrupts = []
@filters = [] @filters = []
@global_filter = nil
end end
def warnings def increment_used_resources(key, obj)
@warnings ||= [] @resource_limits[key] += if obj.kind_of?(String) || obj.kind_of?(Array) || obj.kind_of?(Hash)
obj.length
else
1
end
end
def resource_limits_reached?
(@resource_limits[:render_length_limit] && @resource_limits[:render_length_current] > @resource_limits[:render_length_limit]) ||
(@resource_limits[:render_score_limit] && @resource_limits[:render_score_current] > @resource_limits[:render_score_limit] ) ||
(@resource_limits[:assign_score_limit] && @resource_limits[:assign_score_current] > @resource_limits[:assign_score_limit] )
end end
def strainer def strainer
@@ -51,16 +61,25 @@ module Liquid
# for that # for that
def add_filters(filters) def add_filters(filters)
filters = [filters].flatten.compact filters = [filters].flatten.compact
@filters += filters filters.each do |f|
@strainer = nil raise ArgumentError, "Expected module but got: #{f.class}" unless f.is_a?(Module)
end Strainer.add_known_filter(f)
end
def apply_global_filter(obj) # If strainer is already setup then there's no choice but to use a runtime
global_filter.nil? ? obj : global_filter.call(obj) # extend call. If strainer is not yet created, we can utilize strainers
# cached class based API, which avoids busting the method cache.
if @strainer
filters.each do |f|
strainer.extend(f)
end
else
@filters.concat filters
end
end end
# are there any not handled interrupts? # are there any not handled interrupts?
def interrupt? def has_interrupt?
!@interrupts.empty? !@interrupts.empty?
end end
@@ -74,12 +93,15 @@ module Liquid
@interrupts.pop @interrupts.pop
end end
def handle_error(e, line_number = nil)
e = internal_error unless e.is_a?(Liquid::Error) def handle_error(e, token=nil)
e.template_name ||= template_name if e.is_a?(Liquid::Error)
e.line_number ||= line_number e.set_line_number_from_token(token)
end
errors.push(e) errors.push(e)
exception_renderer.call(e).to_s raise if exception_handler && exception_handler.call(e)
Liquid::Error.render(e)
end end
def invoke(method, *args) def invoke(method, *args)
@@ -87,9 +109,9 @@ module Liquid
end end
# Push new local scope on the stack. use <tt>Context#stack</tt> instead # Push new local scope on the stack. use <tt>Context#stack</tt> instead
def push(new_scope = {}) def push(new_scope={})
@scopes.unshift(new_scope) @scopes.unshift(new_scope)
raise StackLevelError, "Nesting too deep".freeze if @scopes.length > Block::MAX_DEPTH raise StackLevelError, "Nesting too deep".freeze if @scopes.length > 100
end end
# Merge a hash of variables in the current local scope # Merge a hash of variables in the current local scope
@@ -111,7 +133,7 @@ module Liquid
# end # end
# #
# context['var] #=> nil # context['var] #=> nil
def stack(new_scope = nil) def stack(new_scope=nil)
old_stack_used = @this_stack_used old_stack_used = @this_stack_used
if new_scope if new_scope
push(new_scope) push(new_scope)
@@ -148,10 +170,10 @@ module Liquid
# Example: # Example:
# products == empty #=> products.empty? # products == empty #=> products.empty?
def [](expression) def [](expression)
evaluate(Expression.parse(expression)) evaluate(@parsed_expression[expression])
end end
def key?(key) def has_key?(key)
self[key] != nil self[key] != nil
end end
@@ -160,17 +182,18 @@ module Liquid
end end
# Fetches an object starting at the local scope and then moving up the hierachy # Fetches an object starting at the local scope and then moving up the hierachy
def find_variable(key, raise_on_not_found: true) def find_variable(key)
# This was changed from find() to find_index() because this is a very hot # 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 # path and find_index() is optimized in MRI to reduce object allocation
index = @scopes.find_index { |s| s.key?(key) } index = @scopes.find_index { |s| s.has_key?(key) }
scope = @scopes[index] if index scope = @scopes[index] if index
variable = nil variable = nil
if scope.nil? if scope.nil?
@environments.each do |e| @environments.each do |e|
variable = lookup_and_evaluate(e, key, raise_on_not_found: raise_on_not_found) variable = lookup_and_evaluate(e, key)
unless variable.nil? unless variable.nil?
scope = e scope = e
break break
@@ -178,23 +201,17 @@ module Liquid
end end
end end
scope ||= @environments.last || @scopes.last scope ||= @environments.last || @scopes.last
variable ||= lookup_and_evaluate(scope, key, raise_on_not_found: raise_on_not_found) variable ||= lookup_and_evaluate(scope, key)
variable = variable.to_liquid variable = variable.to_liquid
variable.context = self if variable.respond_to?(:context=) variable.context = self if variable.respond_to?(:context=)
variable return variable
end end
def lookup_and_evaluate(obj, key, raise_on_not_found: true) def lookup_and_evaluate(obj, key)
if @strict_variables && raise_on_not_found && obj.respond_to?(:key?) && !obj.key?(key) if (value = obj[key]).is_a?(Proc) && obj.respond_to?(:[]=)
raise Liquid::UndefinedVariable, "undefined variable #{key}"
end
value = obj[key]
if value.is_a?(Proc) && obj.respond_to?(:[]=)
obj[key] = (value.arity == 0) ? value.call : value.call(self) obj[key] = (value.arity == 0) ? value.call : value.call(self)
else else
value value
@@ -202,23 +219,15 @@ module Liquid
end end
private private
def squash_instance_assigns_with_environments
def internal_error @scopes.last.each_key do |k|
# raise and catch to set backtrace and cause on exception @environments.each do |env|
raise Liquid::InternalError, 'internal' if env.has_key?(k)
rescue Liquid::InternalError => exc scopes.last[k] = lookup_and_evaluate(env, k)
exc break
end end
def squash_instance_assigns_with_environments
@scopes.last.each_key do |k|
@environments.each do |env|
if env.key?(k)
scopes.last[k] = lookup_and_evaluate(env, k)
break
end end
end end
end end # squash_instance_assigns_with_environments
end # squash_instance_assigns_with_environments
end # Context end # Context
end # Liquid end # Liquid

View File

@@ -1,27 +1,17 @@
module Liquid module Liquid
class Document < BlockBody class Document < Block
def self.parse(tokens, parse_context) def self.parse(tokens, options={})
doc = new # we don't need markup to open this block
doc.parse(tokens, parse_context) super(nil, nil, tokens, options)
doc
end end
def parse(tokens, parse_context) # There isn't a real delimiter
super do |end_tag_name, end_tag_params| def block_delimiter
unknown_tag(end_tag_name, parse_context) if end_tag_name []
end
rescue SyntaxError => e
e.line_number ||= parse_context.line_number
raise
end end
def unknown_tag(tag, parse_context) # Document blocks don't need to be terminated since they are not actually opened
case tag def assert_missing_delimitation!
when 'else'.freeze, 'end'.freeze
raise SyntaxError.new(parse_context.locale.t("errors.syntax.unexpected_outer_tag".freeze, tag: tag))
else
raise SyntaxError.new(parse_context.locale.t("errors.syntax.unknown_tag".freeze, tag: tag))
end
end end
end end
end end

View File

@@ -1,6 +1,7 @@
require 'set' require 'set'
module Liquid module Liquid
# A drop in liquid is a class which allows you to export DOM like things to liquid. # A drop in liquid is a class which allows you to export DOM like things to liquid.
# Methods of drops are callable. # Methods of drops are callable.
# The main use for liquid drops is to implement lazy loaded objects. # The main use for liquid drops is to implement lazy loaded objects.
@@ -18,27 +19,28 @@ module Liquid
# tmpl = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {%endfor%} ' ) # tmpl = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {%endfor%} ' )
# tmpl.render('product' => ProductDrop.new ) # will invoke top_sales query. # tmpl.render('product' => ProductDrop.new ) # will invoke top_sales query.
# #
# Your drop can either implement the methods sans any parameters # Your drop can either implement the methods sans any parameters or implement the before_method(name) method which is a
# or implement the liquid_method_missing(name) method which is a catch all. # catch all.
class Drop class Drop
attr_writer :context attr_writer :context
EMPTY_STRING = ''.freeze
# Catch all for the method # Catch all for the method
def liquid_method_missing(method) def before_method(method)
return nil unless @context && @context.strict_variables nil
raise Liquid::UndefinedDropMethod, "undefined method #{method}"
end end
# called by liquid to invoke a drop # called by liquid to invoke a drop
def invoke_drop(method_or_key) def invoke_drop(method_or_key)
if self.class.invokable?(method_or_key) if method_or_key && method_or_key != EMPTY_STRING && self.class.invokable?(method_or_key)
send(method_or_key) send(method_or_key)
else else
liquid_method_missing(method_or_key) before_method(method_or_key)
end end
end end
def key?(_name) def has_key?(name)
true true
end end
@@ -54,25 +56,22 @@ module Liquid
self.class.name self.class.name
end end
alias_method :[], :invoke_drop alias :[] :invoke_drop
private
# Check for method existence without invoking respond_to?, which creates symbols # Check for method existence without invoking respond_to?, which creates symbols
def self.invokable?(method_name) def self.invokable?(method_name)
invokable_methods.include?(method_name.to_s) unless @invokable_methods
end
def self.invokable_methods
@invokable_methods ||= begin
blacklist = Liquid::Drop.public_instance_methods + [:each] blacklist = Liquid::Drop.public_instance_methods + [:each]
if include?(Enumerable) if include?(Enumerable)
blacklist += Enumerable.public_instance_methods blacklist += Enumerable.public_instance_methods
blacklist -= [:sort, :count, :first, :min, :max, :include?] blacklist -= [:sort, :count, :first, :min, :max, :include?]
end end
whitelist = [:to_liquid] + (public_instance_methods - blacklist) whitelist = [:to_liquid] + (public_instance_methods - blacklist)
Set.new(whitelist.map(&:to_s)) @invokable_methods = Set.new(whitelist.map(&:to_s))
end end
@invokable_methods.include?(method_name.to_s)
end end
end end
end end

View File

@@ -1,10 +1,9 @@
module Liquid module Liquid
class Error < ::StandardError class Error < ::StandardError
attr_accessor :line_number attr_accessor :line_number
attr_accessor :template_name
attr_accessor :markup_context attr_accessor :markup_context
def to_s(with_prefix = true) def to_s(with_prefix=true)
str = "" str = ""
str << message_prefix if with_prefix str << message_prefix if with_prefix
str << super() str << super()
@@ -17,6 +16,20 @@ module Liquid
str str
end end
def set_line_number_from_token(token)
return unless token.respond_to?(:line_number)
return if self.line_number
self.line_number = token.line_number
end
def self.render(e)
if e.is_a?(Liquid::Error)
e.to_s
else
"Liquid error: #{e.to_s}"
end
end
private private
def message_prefix def message_prefix
@@ -28,9 +41,7 @@ module Liquid
end end
if line_number if line_number
str << " (" str << " (line #{line_number})"
str << template_name << " " if template_name
str << "line " << line_number.to_s << ")"
end end
str << ": " str << ": "
@@ -38,19 +49,12 @@ module Liquid
end end
end end
ArgumentError = Class.new(Error) class ArgumentError < Error; end
ContextError = Class.new(Error) class ContextError < Error; end
FileSystemError = Class.new(Error) class FileSystemError < Error; end
StandardError = Class.new(Error) class StandardError < Error; end
SyntaxError = Class.new(Error) class SyntaxError < Error; end
StackLevelError = Class.new(Error) class StackLevelError < Error; end
TaintedError = Class.new(Error) class TaintedError < Error; end
MemoryError = Class.new(Error) class MemoryError < Error; end
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 end

View File

@@ -1,24 +1,11 @@
module Liquid module Liquid
class Expression class Expression
class MethodLiteral
attr_reader :method_name, :to_s
def initialize(method_name, to_s)
@method_name = method_name
@to_s = to_s
end
def to_liquid
to_s
end
end
LITERALS = { LITERALS = {
nil => nil, 'nil'.freeze => nil, 'null'.freeze => nil, ''.freeze => nil, nil => nil, 'nil'.freeze => nil, 'null'.freeze => nil, ''.freeze => nil,
'true'.freeze => true, 'true'.freeze => true,
'false'.freeze => false, 'false'.freeze => false,
'blank'.freeze => MethodLiteral.new(:blank?, '').freeze, 'blank'.freeze => :blank?,
'empty'.freeze => MethodLiteral.new(:empty?, '').freeze 'empty'.freeze => :empty?
} }
def self.parse(markup) def self.parse(markup)
@@ -41,5 +28,6 @@ module Liquid
end end
end end
end end
end end
end end

View File

@@ -7,50 +7,44 @@ class String # :nodoc:
end end
end end
class Array # :nodoc: class Array # :nodoc:
def to_liquid def to_liquid
self self
end end
end end
class Hash # :nodoc: class Hash # :nodoc:
def to_liquid def to_liquid
self self
end end
end end
class Numeric # :nodoc: class Numeric # :nodoc:
def to_liquid def to_liquid
self self
end end
end end
class Range # :nodoc: class Time # :nodoc:
def to_liquid def to_liquid
self self
end end
end end
class Time # :nodoc: class DateTime < Date # :nodoc:
def to_liquid def to_liquid
self self
end end
end end
class DateTime < Date # :nodoc: class Date # :nodoc:
def to_liquid
self
end
end
class Date # :nodoc:
def to_liquid def to_liquid
self self
end end
end end
class TrueClass class TrueClass
def to_liquid # :nodoc: def to_liquid # :nodoc:
self self
end end
end end

View File

@@ -8,13 +8,13 @@ module Liquid
# #
# Example: # Example:
# #
# Liquid::Template.file_system = Liquid::LocalFileSystem.new(template_path) # Liquid::Template.file_system = Liquid::LocalFileSystem.new(template_path)
# liquid = Liquid::Template.parse(template) # liquid = Liquid::Template.parse(template)
# #
# This will parse the template with a LocalFileSystem implementation rooted at 'template_path'. # This will parse the template with a LocalFileSystem implementation rooted at 'template_path'.
class BlankFileSystem class BlankFileSystem
# Called by Liquid to retrieve a template file # Called by Liquid to retrieve a template file
def read_template_file(_template_path) def read_template_file(template_path, context)
raise FileSystemError, "This liquid context does not allow includes." raise FileSystemError, "This liquid context does not allow includes."
end end
end end
@@ -26,10 +26,10 @@ module Liquid
# #
# Example: # 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("mypartial") # => "/some/path/_mypartial.liquid"
# file_system.full_path("dir/mypartial") # => "/some/path/dir/_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. # Optionally in the second argument you can specify a custom pattern for template filenames.
# The Kernel::sprintf format specification is used. # The Kernel::sprintf format specification is used.
@@ -37,9 +37,9 @@ module Liquid
# #
# Example: # 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 class LocalFileSystem
attr_accessor :root attr_accessor :root
@@ -49,9 +49,9 @@ module Liquid
@pattern = pattern @pattern = pattern
end end
def read_template_file(template_path) def read_template_file(template_path, context)
full_path = full_path(template_path) full_path = full_path(template_path)
raise FileSystemError, "No such template '#{template_path}'" unless File.exist?(full_path) raise FileSystemError, "No such template '#{template_path}'" unless File.exists?(full_path)
File.read(full_path) File.read(full_path)
end end
@@ -65,7 +65,7 @@ module Liquid
File.join(root, @pattern % template_path) File.join(root, @pattern % template_path)
end end
raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path).start_with?(File.expand_path(root)) raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path) =~ /\A#{File.expand_path(root)}/
full_path full_path
end end

View File

@@ -1,42 +0,0 @@
module Liquid
class ForloopDrop < Drop
def initialize(name, length, parentloop)
@name = name
@length = length
@parentloop = parentloop
@index = 0
end
attr_reader :name, :length, :parentloop
def index
@index + 1
end
def index0
@index
end
def rindex
@length - @index
end
def rindex0
@length - @index - 1
end
def first
@index == 0
end
def last
@index == @length - 1
end
protected
def increment!
@index += 1
end
end
end

View File

@@ -2,9 +2,10 @@ require 'yaml'
module Liquid module Liquid
class I18n class I18n
DEFAULT_LOCALE = File.join(File.expand_path(__dir__), "locales", "en.yml") DEFAULT_LOCALE = File.join(File.expand_path(File.dirname(__FILE__)), "locales", "en.yml")
TranslationError = Class.new(StandardError) class TranslationError < StandardError
end
attr_reader :path attr_reader :path
@@ -22,12 +23,11 @@ module Liquid
end end
private private
def interpolate(name, vars) def interpolate(name, vars)
name.gsub(/%\{(\w+)\}/) do name.gsub(/%\{(\w+)\}/) {
# raise TranslationError, "Undefined key #{$1} for interpolation in translation #{name}" unless vars[$1.to_sym] # raise TranslationError, "Undefined key #{$1} for interpolation in translation #{name}" unless vars[$1.to_sym]
"#{vars[$1.to_sym]}" "#{vars[$1.to_sym]}"
end }
end end
def deep_fetch_translation(name) def deep_fetch_translation(name)

View File

@@ -1,9 +1,10 @@
module Liquid module Liquid
# An interrupt is any command that breaks processing of a block (ex: a for loop). # An interrupt is any command that breaks processing of a block (ex: a for loop).
class Interrupt class Interrupt
attr_reader :message attr_reader :message
def initialize(message = nil) def initialize(message=nil)
@message = message || "interrupt".freeze @message = message || "interrupt".freeze
end end
end end

View File

@@ -9,27 +9,24 @@ module Liquid
'['.freeze => :open_square, '['.freeze => :open_square,
']'.freeze => :close_square, ']'.freeze => :close_square,
'('.freeze => :open_round, '('.freeze => :open_round,
')'.freeze => :close_round, ')'.freeze => :close_round
'?'.freeze => :question,
'-'.freeze => :dash
} }
IDENTIFIER = /[a-zA-Z_][\w-]*\??/ IDENTIFIER = /[\w\-?!]+/
SINGLE_STRING_LITERAL = /'[^\']*'/ SINGLE_STRING_LITERAL = /'[^\']*'/
DOUBLE_STRING_LITERAL = /"[^\"]*"/ DOUBLE_STRING_LITERAL = /"[^\"]*"/
NUMBER_LITERAL = /-?\d+(\.\d+)?/ NUMBER_LITERAL = /-?\d+(\.\d+)?/
DOTDOT = /\.\./ DOTDOT = /\.\./
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/ COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains/
def initialize(input) def initialize(input)
@ss = StringScanner.new(input) @ss = StringScanner.new(input.rstrip)
end end
def tokenize def tokenize
@output = [] @output = []
until @ss.eos? while !@ss.eos?
@ss.skip(/\s*/) @ss.skip(/\s*/)
break if @ss.eos?
tok = case tok = case
when t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t] when t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t]
when t = @ss.scan(SINGLE_STRING_LITERAL) then [:string, t] when t = @ss.scan(SINGLE_STRING_LITERAL) then [:string, t]
@@ -40,7 +37,7 @@ module Liquid
else else
c = @ss.getch c = @ss.getch
if s = SPECIALS[c] if s = SPECIALS[c]
[s, c] [s,c]
else else
raise SyntaxError, "Unexpected character #{c}" raise SyntaxError, "Unexpected character #{c}"
end end

View File

@@ -1,7 +1,6 @@
--- ---
errors: errors:
syntax: syntax:
tag_unexpected_args: "Syntax Error in '%{tag}' - Valid syntax: %{tag}"
assign: "Syntax Error in 'assign' - Valid syntax: assign [var] = [source]" assign: "Syntax Error in 'assign' - Valid syntax: assign [var] = [source]"
capture: "Syntax Error in 'capture' - Valid syntax: capture [var]" capture: "Syntax Error in 'capture' - Valid syntax: capture [var]"
case: "Syntax Error in 'case' - Valid syntax: case [condition]" case: "Syntax Error in 'case' - Valid syntax: case [condition]"
@@ -15,12 +14,9 @@
include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]" include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]"
unknown_tag: "Unknown tag '%{tag}'" unknown_tag: "Unknown tag '%{tag}'"
invalid_delimiter: "'end' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}" invalid_delimiter: "'end' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}"
unexpected_else: "%{block_name} tag does not expect 'else' tag" 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}" tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"
variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}" variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}"
tag_never_closed: "'%{block_name}' tag was never closed" tag_never_closed: "'%{block_name}' tag was never closed"
meta_syntax_error: "Liquid syntax error: #{e.message}" 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" 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"

62
lib/liquid/module_ex.rb Normal file
View File

@@ -0,0 +1,62 @@
# Copyright 2007 by Domizio Demichelis
# This library is free software. It may be used, redistributed and/or modified
# under the same terms as Ruby itself
#
# This extension is used in order to expose the object of the implementing class
# to liquid as it were a Drop. It also limits the liquid-callable methods of the instance
# to the allowed method passed with the liquid_methods call
# Example:
#
# class SomeClass
# liquid_methods :an_allowed_method
#
# def an_allowed_method
# 'this comes from an allowed method'
# end
# def unallowed_method
# 'this will never be an output'
# end
# end
#
# if you want to extend the drop to other methods you can defines more methods
# in the class <YourClass>::LiquidDropClass
#
# class SomeClass::LiquidDropClass
# def another_allowed_method
# 'and this from another allowed method'
# end
# end
# end
#
# usage:
# @something = SomeClass.new
#
# template:
# {{something.an_allowed_method}}{{something.unallowed_method}} {{something.another_allowed_method}}
#
# output:
# 'this comes from an allowed method and this from another allowed method'
#
# You can also chain associations, by adding the liquid_method call in the
# association models.
#
class Module
def liquid_methods(*allowed_methods)
drop_class = eval "class #{self.to_s}::LiquidDropClass < Liquid::Drop; self; end"
define_method :to_liquid do
drop_class.new(self)
end
drop_class.class_eval do
def initialize(object)
@object = object
end
allowed_methods.each do |sym|
define_method sym do
@object.send sym
end
end
end
end
end

View File

@@ -1,38 +0,0 @@
module Liquid
class ParseContext
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
def [](option_key)
@options[option_key]
end
def partial=(value)
@partial = value
@options = value ? partial_options : @template_options
@error_mode = @options[:error_mode] || Template.error_mode
value
end
def partial_options
@partial_options ||= begin
dont_pass = @template_options[:include_options_blacklist]
if dont_pass == true
{ locale: locale }
elsif dont_pass.is_a?(Array)
@template_options.reject { |k, v| dont_pass.include?(k) }
else
@template_options
end
end
end
end
end

View File

@@ -75,7 +75,7 @@ module Liquid
def variable_signature def variable_signature
str = consume(:id) str = consume(:id)
while look(:open_square) if look(:open_square)
str << consume str << consume
str << expression str << expression
str << consume(:close_square) str << consume(:close_square)

View File

@@ -1,25 +1,25 @@
module Liquid module Liquid
module ParserSwitching module ParserSwitching
def parse_with_selected_parser(markup) def parse_with_selected_parser(markup)
case parse_context.error_mode case @options[:error_mode] || Template.error_mode
when :strict then strict_parse_with_error_context(markup) when :strict then strict_parse_with_error_context(markup)
when :lax then lax_parse(markup) when :lax then lax_parse(markup)
when :warn when :warn
begin begin
return strict_parse_with_error_context(markup) return strict_parse_with_error_context(markup)
rescue SyntaxError => e rescue SyntaxError => e
parse_context.warnings << e e.set_line_number_from_token(markup)
@warnings ||= []
@warnings << e
return lax_parse(markup) return lax_parse(markup)
end end
end end
end end
private private
def strict_parse_with_error_context(markup) def strict_parse_with_error_context(markup)
strict_parse(markup) strict_parse(markup)
rescue SyntaxError => e rescue SyntaxError => e
e.line_number = line_number
e.markup_context = markup_context(markup) e.markup_context = markup_context(markup)
raise e raise e
end end

View File

@@ -1,11 +1,9 @@
require 'liquid/profiler/hooks'
module Liquid module Liquid
# Profiler enables support for profiling template rendering to help track down performance issues. # Profiler enables support for profiling template rendering to help track down performance issues.
# #
# To enable profiling, first require 'liquid/profiler'. # To enable profiling, pass the <tt>profile: true</tt> option to <tt>Liquid::Template.parse</tt>. Then, after
# Then, to profile a parse/render cycle, pass the <tt>profile: true</tt> option to <tt>Liquid::Template.parse</tt>. # <tt>Liquid::Template#render</tt> is called, the template object makes available an instance of this
# After <tt>Liquid::Template#render</tt> is called, the template object makes available an instance of this
# class via the <tt>Liquid::Template#profiler</tt> method. # class via the <tt>Liquid::Template#profiler</tt> method.
# #
# template = Liquid::Template.parse(template_content, profile: true) # template = Liquid::Template.parse(template_content, profile: true)
@@ -19,7 +17,7 @@ module Liquid
# inside of <tt>{% include %}</tt> tags. # inside of <tt>{% include %}</tt> tags.
# #
# profile.each do |node| # profile.each do |node|
# # Access to the node itself # # Access to the token itself
# node.code # node.code
# #
# # Which template and line number of this node. # # Which template and line number of this node.
@@ -46,15 +44,17 @@ module Liquid
class Timing class Timing
attr_reader :code, :partial, :line_number, :children attr_reader :code, :partial, :line_number, :children
def initialize(node, partial) def initialize(token, partial)
@code = node.respond_to?(:raw) ? node.raw : node @code = token.respond_to?(:raw) ? token.raw : token
@partial = partial @partial = partial
@line_number = node.respond_to?(:line_number) ? node.line_number : nil @line_number = token.respond_to?(:line_number) ? token.line_number : nil
@children = [] @children = []
end end
def self.start(node, partial) def self.start(token, partial)
new(node, partial).tap(&:start) new(token, partial).tap do |t|
t.start
end
end end
def start def start
@@ -70,11 +70,11 @@ module Liquid
end end
end end
def self.profile_node_render(node) def self.profile_token_render(token)
if Profiler.current_profile && node.respond_to?(:render) if Profiler.current_profile && token.respond_to?(:render)
Profiler.current_profile.start_node(node) Profiler.current_profile.start_token(token)
output = yield output = yield
Profiler.current_profile.end_node(node) Profiler.current_profile.end_token(token)
output output
else else
yield yield
@@ -132,11 +132,11 @@ module Liquid
@root_timing.children.length @root_timing.children.length
end end
def start_node(node) def start_token(token)
@timing_stack.push(Timing.start(node, current_partial)) @timing_stack.push(Timing.start(token, current_partial))
end end
def end_node(_node) def end_token(token)
timing = @timing_stack.pop timing = @timing_stack.pop
timing.finish timing.finish
@@ -154,5 +154,6 @@ module Liquid
def pop_partial def pop_partial
@partial_stack.pop @partial_stack.pop
end end
end end
end end

View File

@@ -1,18 +1,18 @@
module Liquid module Liquid
class BlockBody class Block < Tag
def render_node_with_profiling(node, context) def render_token_with_profiling(token, context)
Profiler.profile_node_render(node) do Profiler.profile_token_render(token) do
render_node_without_profiling(node, context) render_token_without_profiling(token, context)
end end
end end
alias_method :render_node_without_profiling, :render_node alias_method :render_token_without_profiling, :render_token
alias_method :render_node, :render_node_with_profiling alias_method :render_token, :render_token_with_profiling
end end
class Include < Tag class Include < Tag
def render_with_profiling(context) def render_with_profiling(context)
Profiler.profile_children(context.evaluate(@template_name_expr).to_s) do Profiler.profile_children(@template_name) do
render_without_profiling(context) render_without_profiling(context)
end end
end end

View File

@@ -16,22 +16,7 @@ module Liquid
end end
def evaluate(context) def evaluate(context)
start_int = to_integer(context.evaluate(@start_obj)) context.evaluate(@start_obj).to_i..context.evaluate(@end_obj).to_i
end_int = to_integer(context.evaluate(@end_obj))
start_int..end_int
end
private
def to_integer(input)
case input
when Integer
input
when NilClass, String
input.to_i
else
Utils.to_integer(input)
end
end end
end end
end end

View File

@@ -1,23 +0,0 @@
module Liquid
class ResourceLimits
attr_accessor :render_length, :render_score, :assign_score,
:render_length_limit, :render_score_limit, :assign_score_limit
def initialize(limits)
@render_length_limit = limits[:render_length_limit]
@render_score_limit = limits[:render_score_limit]
@assign_score_limit = limits[:assign_score_limit]
reset
end
def reached?
(@render_length_limit && @render_length > @render_length_limit) ||
(@render_score_limit && @render_score > @render_score_limit) ||
(@assign_score_limit && @assign_score > @assign_score_limit)
end
def reset
@render_length = @render_score = @assign_score = 0
end
end
end

View File

@@ -2,6 +2,7 @@ require 'cgi'
require 'bigdecimal' require 'bigdecimal'
module Liquid module Liquid
module StandardFilters module StandardFilters
HTML_ESCAPE = { HTML_ESCAPE = {
'&'.freeze => '&amp;'.freeze, '&'.freeze => '&amp;'.freeze,
@@ -33,7 +34,7 @@ module Liquid
end end
def escape(input) def escape(input)
CGI.escapeHTML(input.to_s).untaint unless input.nil? CGI.escapeHTML(input).untaint rescue input
end end
alias_method :h, :escape alias_method :h, :escape
@@ -42,16 +43,12 @@ module Liquid
end end
def url_encode(input) def url_encode(input)
CGI.escape(input.to_s) unless input.nil? CGI.escape(input) rescue input
end end
def url_decode(input) def slice(input, offset, length=nil)
CGI.unescape(input.to_s) unless input.nil? offset = Integer(offset)
end length = length ? Integer(length) : 1
def slice(input, offset, length = nil)
offset = Utils.to_integer(offset)
length = length ? Utils.to_integer(length) : 1
if input.is_a?(Array) if input.is_a?(Array)
input.slice(offset, length) || [] input.slice(offset, length) || []
@@ -62,22 +59,18 @@ module Liquid
# Truncate a string down to x characters # Truncate a string down to x characters
def truncate(input, length = 50, truncate_string = "...".freeze) def truncate(input, length = 50, truncate_string = "...".freeze)
return if input.nil? if input.nil? then return end
input_str = input.to_s l = length.to_i - truncate_string.length
length = Utils.to_integer(length)
truncate_string_str = truncate_string.to_s
l = length - truncate_string_str.length
l = 0 if l < 0 l = 0 if l < 0
input_str.length > length ? input_str[0...l] + truncate_string_str : input_str input.length > length.to_i ? input[0...l] + truncate_string : input
end end
def truncatewords(input, words = 15, truncate_string = "...".freeze) def truncatewords(input, words = 15, truncate_string = "...".freeze)
return if input.nil? if input.nil? then return end
wordlist = input.to_s.split wordlist = input.to_s.split
words = Utils.to_integer(words) l = words.to_i - 1
l = words - 1
l = 0 if l < 0 l = 0 if l < 0
wordlist.length > l ? wordlist[0..l].join(" ".freeze) + truncate_string.to_s : input wordlist.length > l ? wordlist[0..l].join(" ".freeze) + truncate_string : input
end end
# Split input string into an array of substrings separated by given pattern. # Split input string into an array of substrings separated by given pattern.
@@ -86,7 +79,7 @@ module Liquid
# <div class="summary">{{ post | split '//' | first }}</div> # <div class="summary">{{ post | split '//' | first }}</div>
# #
def split(input, pattern) def split(input, pattern)
input.to_s.split(pattern.to_s) input.to_s.split(pattern)
end end
def strip(input) def strip(input)
@@ -122,32 +115,10 @@ module Liquid
ary = InputIterator.new(input) ary = InputIterator.new(input)
if property.nil? if property.nil?
ary.sort ary.sort
elsif ary.empty? # The next two cases assume a non-empty array.
[]
elsif ary.first.respond_to?(:[]) && !ary.first[property].nil? elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
ary.sort do |a, b| ary.sort {|a,b| a[property] <=> b[property] }
a = a[property] elsif ary.first.respond_to?(property)
b = b[property] ary.sort {|a,b| a.send(property) <=> b.send(property) }
if a && b
a <=> b
else
a ? -1 : 1
end
end
end
end
# Sort elements of an array ignoring case if strings
# provide optional property with which to sort an array of hashes or drops
def sort_natural(input, property = nil)
ary = InputIterator.new(input)
if property.nil?
ary.sort { |a, b| a.casecmp(b) }
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].casecmp(b[property]) }
end end
end end
@@ -155,13 +126,10 @@ module Liquid
# provide optional property with which to determine uniqueness # provide optional property with which to determine uniqueness
def uniq(input, property = nil) def uniq(input, property = nil)
ary = InputIterator.new(input) ary = InputIterator.new(input)
if property.nil? if property.nil?
ary.uniq input.uniq
elsif ary.empty? # The next two cases assume a non-empty array. elsif input.first.respond_to?(:[])
[] input.uniq{ |a| a[property] }
elsif ary.first.respond_to?(:[])
ary.uniq{ |a| a[property] }
end end
end end
@@ -179,44 +147,29 @@ module Liquid
if property == "to_liquid".freeze if property == "to_liquid".freeze
e e
elsif e.respond_to?(:[]) elsif e.respond_to?(:[])
r = e[property] e[property]
r.is_a?(Proc) ? r.call : r
end end
end end
end end
# Remove nils within an array
# provide optional property with which to check for nil
def compact(input, property = nil)
ary = InputIterator.new(input)
if property.nil?
ary.compact
elsif ary.empty? # The next two cases assume a non-empty array.
[]
elsif ary.first.respond_to?(:[])
ary.reject{ |a| a[property].nil? }
end
end
# Replace occurrences of a string with another # Replace occurrences of a string with another
def replace(input, string, replacement = ''.freeze) def replace(input, string, replacement = ''.freeze)
input.to_s.gsub(string.to_s, replacement.to_s) input.to_s.gsub(string, replacement.to_s)
end end
# Replace the first occurrences of a string with another # Replace the first occurrences of a string with another
def replace_first(input, string, replacement = ''.freeze) def replace_first(input, string, replacement = ''.freeze)
input.to_s.sub(string.to_s, replacement.to_s) input.to_s.sub(string, replacement.to_s)
end end
# remove a substring # remove a substring
def remove(input, string) def remove(input, string)
input.to_s.gsub(string.to_s, ''.freeze) input.to_s.gsub(string, ''.freeze)
end end
# remove the first occurrences of a substring # remove the first occurrences of a substring
def remove_first(input, string) def remove_first(input, string)
input.to_s.sub(string.to_s, ''.freeze) input.to_s.sub(string, ''.freeze)
end end
# add one string to another # add one string to another
@@ -224,13 +177,6 @@ module Liquid
input.to_s + string.to_s input.to_s + string.to_s
end end
def concat(input, array)
unless array.respond_to?(:to_ary)
raise ArgumentError.new("concat filter requires an array argument")
end
InputIterator.new(input).concat(array)
end
# prepend a string to another # prepend a string to another
def prepend(input, string) def prepend(input, string)
string.to_s + input.to_s string.to_s + input.to_s
@@ -275,7 +221,7 @@ module Liquid
def date(input, format) def date(input, format)
return input if format.to_s.empty? return input if format.to_s.empty?
return input unless date = Utils.to_date(input) return input unless date = to_date(input)
date.strftime(format.to_s) date.strftime(format.to_s)
end end
@@ -298,12 +244,6 @@ module Liquid
array.last if array.respond_to?(:last) array.last if array.respond_to?(:last)
end end
# absolute value
def abs(input)
result = Utils.to_number(input).abs
result.is_a?(BigDecimal) ? result.to_f : result
end
# addition # addition
def plus(input, operand) def plus(input, operand)
apply_operation(input, operand, :+) apply_operation(input, operand, :+)
@@ -322,49 +262,66 @@ module Liquid
# division # division
def divided_by(input, operand) def divided_by(input, operand)
apply_operation(input, operand, :/) apply_operation(input, operand, :/)
rescue ::ZeroDivisionError => e
raise Liquid::ZeroDivisionError, e.message
end end
def modulo(input, operand) def modulo(input, operand)
apply_operation(input, operand, :%) apply_operation(input, operand, :%)
rescue ::ZeroDivisionError => e
raise Liquid::ZeroDivisionError, e.message
end end
def round(input, n = 0) def round(input, n = 0)
result = Utils.to_number(input).round(Utils.to_number(n)) result = to_number(input).round(to_number(n))
result = result.to_f if result.is_a?(BigDecimal) result = result.to_f if result.is_a?(BigDecimal)
result = result.to_i if n == 0 result = result.to_i if n == 0
result result
rescue ::FloatDomainError => e
raise Liquid::FloatDomainError, e.message
end end
def ceil(input) def ceil(input)
Utils.to_number(input).ceil.to_i to_number(input).ceil.to_i
rescue ::FloatDomainError => e
raise Liquid::FloatDomainError, e.message
end end
def floor(input) def floor(input)
Utils.to_number(input).floor.to_i to_number(input).floor.to_i
rescue ::FloatDomainError => e
raise Liquid::FloatDomainError, e.message
end end
def default(input, default_value = ''.freeze) def default(input, default_value = "".freeze)
if !input || input.respond_to?(:empty?) && input.empty? is_blank = input.respond_to?(:empty?) ? input.empty? : !input
default_value is_blank ? default_value : input
else
input
end
end end
private private
def to_number(obj)
case obj
when Float
BigDecimal.new(obj.to_s)
when Numeric
obj
when String
(obj.strip =~ /\A\d+\.\d+\z/) ? BigDecimal.new(obj) : obj.to_i
else
0
end
end
def to_date(obj)
return obj if obj.respond_to?(:strftime)
case obj
when 'now'.freeze, 'today'.freeze
Time.now
when /\A\d+\z/, Integer
Time.at(obj.to_i)
when String
Time.parse(obj)
else
nil
end
rescue ArgumentError
nil
end
def apply_operation(input, operand, operation) def apply_operation(input, operand, operation)
result = Utils.to_number(input).send(operation, Utils.to_number(operand)) result = to_number(input).send(operation, to_number(operand))
result.is_a?(BigDecimal) ? result.to_f : result result.is_a?(BigDecimal) ? result.to_f : result
end end
@@ -384,30 +341,13 @@ module Liquid
end end
def join(glue) def join(glue)
to_a.join(glue.to_s) to_a.join(glue)
end
def concat(args)
to_a.concat(args)
end end
def reverse def reverse
reverse_each.to_a reverse_each.to_a
end end
def uniq(&block)
to_a.uniq(&block)
end
def compact
to_a.compact
end
def empty?
@input.each { return false }
true
end
def each def each
@input.each do |e| @input.each do |e|
yield(e.respond_to?(:to_liquid) ? e.to_liquid : e) yield(e.respond_to?(:to_liquid) ? e.to_liquid : e)

View File

@@ -1,19 +1,19 @@
require 'set' require 'set'
module Liquid module Liquid
# Strainer is the parent class for the filters system. # Strainer is the parent class for the filters system.
# New filters are mixed into the strainer class which is then instantiated for each liquid template render run. # New filters are mixed into the strainer class which is then instantiated for each liquid template render run.
# #
# The Strainer only allows method calls defined in filters given to it via Strainer.global_filter, # The Strainer only allows method calls defined in filters given to it via Strainer.global_filter,
# Context#add_filters or Template.register_filter # Context#add_filters or Template.register_filter
class Strainer #:nodoc: class Strainer #:nodoc:
@@global_strainer = Class.new(Strainer) do @@filters = []
@filter_methods = Set.new @@known_filters = Set.new
end @@known_methods = Set.new
@@strainer_class_cache = Hash.new do |hash, filters| @@strainer_class_cache = Hash.new do |hash, filters|
hash[filters] = Class.new(@@global_strainer) do hash[filters] = Class.new(Strainer) do
@filter_methods = @@global_strainer.filter_methods.dup filters.each { |f| include f }
filters.each { |f| add_filter(f) }
end end
end end
@@ -21,46 +21,43 @@ module Liquid
@context = context @context = context
end end
class << self def self.global_filter(filter)
attr_reader :filter_methods raise ArgumentError, "Passed filter is not a module" unless filter.is_a?(Module)
add_known_filter(filter)
@@filters << filter unless @@filters.include?(filter)
end end
def self.add_filter(filter) def self.add_known_filter(filter)
raise ArgumentError, "Expected module but got: #{filter.class}" unless filter.is_a?(Module) unless @@known_filters.include?(filter)
unless self.include?(filter) @@method_blacklist ||= Set.new(Strainer.instance_methods.map(&:to_s))
invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) } new_methods = filter.instance_methods.map(&:to_s)
if invokable_non_public_methods.any? new_methods.reject!{ |m| @@method_blacklist.include?(m) }
raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}" @@known_methods.merge(new_methods)
else @@known_filters.add(filter)
send(:include, filter)
@filter_methods.merge(filter.public_instance_methods.map(&:to_s))
end
end end
end end
def self.global_filter(filter) def self.strainer_class_cache
@@strainer_class_cache.clear @@strainer_class_cache
@@global_strainer.add_filter(filter)
end
def self.invokable?(method)
@filter_methods.include?(method.to_s)
end end
def self.create(context, filters = []) def self.create(context, filters = [])
@@strainer_class_cache[filters].new(context) filters = @@filters + filters
strainer_class_cache[filters].new(context)
end end
def invoke(method, *args) def invoke(method, *args)
if self.class.invokable?(method) if invokable?(method)
send(method, *args) send(method, *args)
elsif @context && @context.strict_filters
raise Liquid::UndefinedFilter, "undefined filter #{method}"
else else
args.first args.first
end end
rescue ::ArgumentError => e rescue ::ArgumentError => e
raise Liquid::ArgumentError, e.message, e.backtrace raise Liquid::ArgumentError.new(e.message)
end
def invokable?(method)
@@known_methods.include?(method.to_s) && respond_to?(method)
end end
end end
end end

View File

@@ -1,62 +0,0 @@
module Liquid
class TablerowloopDrop < Drop
def initialize(length, cols)
@length = length
@row = 1
@col = 1
@cols = cols
@index = 0
end
attr_reader :length, :col, :row
def index
@index + 1
end
def index0
@index
end
def col0
@col - 1
end
def rindex
@length - @index
end
def rindex0
@length - @index - 1
end
def first
@index == 0
end
def last
@index == @length - 1
end
def col_first
@col == 1
end
def col_last
@col == @cols
end
protected
def increment!
@index += 1
if @col == @cols
@col = 1
@row += 1
else
@col += 1
end
end
end
end

View File

@@ -1,27 +1,26 @@
module Liquid module Liquid
class Tag class Tag
attr_reader :nodelist, :tag_name, :line_number, :parse_context attr_accessor :options, :line_number
alias_method :options, :parse_context attr_reader :nodelist, :warnings
include ParserSwitching include ParserSwitching
class << self class << self
def parse(tag_name, markup, tokenizer, options) def parse(tag_name, markup, tokens, options)
tag = new(tag_name, markup, options) tag = new(tag_name, markup, options)
tag.parse(tokenizer) tag.parse(tokens)
tag tag
end end
private :new private :new
end end
def initialize(tag_name, markup, parse_context) def initialize(tag_name, markup, options)
@tag_name = tag_name @tag_name = tag_name
@markup = markup @markup = markup
@parse_context = parse_context @options = options
@line_number = parse_context.line_number
end end
def parse(_tokens) def parse(tokens)
end end
def raw def raw
@@ -32,7 +31,7 @@ module Liquid
self.class.name.downcase self.class.name.downcase
end end
def render(_context) def render(context)
''.freeze ''.freeze
end end

View File

@@ -1,4 +1,5 @@
module Liquid module Liquid
# Assign sets a variable in your template. # Assign sets a variable in your template.
# #
# {% assign foo = 'monkey' %} # {% assign foo = 'monkey' %}
@@ -14,7 +15,8 @@ module Liquid
super super
if markup =~ Syntax if markup =~ Syntax
@to = $1 @to = $1
@from = Variable.new($2, options) @from = Variable.new($2,options)
@from.line_number = line_number
else else
raise SyntaxError.new options[:locale].t("errors.syntax.assign".freeze) raise SyntaxError.new options[:locale].t("errors.syntax.assign".freeze)
end end
@@ -23,28 +25,13 @@ module Liquid
def render(context) def render(context)
val = @from.render(context) val = @from.render(context)
context.scopes.last[@to] = val context.scopes.last[@to] = val
context.resource_limits.assign_score += assign_score_of(val) context.increment_used_resources(:assign_score_current, val)
''.freeze ''.freeze
end end
def blank? def blank?
true true
end end
private
def assign_score_of(val)
if val.instance_of?(String)
val.length
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
end end
Template.register_tag('assign'.freeze, Assign) Template.register_tag('assign'.freeze, Assign)

View File

@@ -1,4 +1,5 @@
module Liquid module Liquid
# Break tag to be used to break out of a for loop. # Break tag to be used to break out of a for loop.
# #
# == Basic Usage: # == Basic Usage:
@@ -9,9 +10,11 @@ module Liquid
# {% endfor %} # {% endfor %}
# #
class Break < Tag class Break < Tag
def interrupt def interrupt
BreakInterrupt.new BreakInterrupt.new
end end
end end
Template.register_tag('break'.freeze, Break) Template.register_tag('break'.freeze, Break)

View File

@@ -25,7 +25,7 @@ module Liquid
def render(context) def render(context)
output = super output = super
context.scopes.last[@to] = output context.scopes.last[@to] = output
context.resource_limits.assign_score += output.length context.increment_used_resources(:assign_score_current, output)
''.freeze ''.freeze
end end

View File

@@ -8,24 +8,18 @@ module Liquid
@blocks = [] @blocks = []
if markup =~ Syntax if markup =~ Syntax
@left = Expression.parse($1) @left = $1
else else
raise SyntaxError.new(options[:locale].t("errors.syntax.case".freeze)) raise SyntaxError.new(options[:locale].t("errors.syntax.case".freeze))
end end
end end
def parse(tokens)
body = BlockBody.new
while parse_body(body, tokens)
body = @blocks.last.attachment
end
end
def nodelist def nodelist
@blocks.map(&:attachment) @blocks.flat_map(&:attachment)
end end
def unknown_tag(tag, markup, tokens) def unknown_tag(tag, markup, tokens)
@nodelist = []
case tag case tag
when 'when'.freeze when 'when'.freeze
record_when_condition(markup) record_when_condition(markup)
@@ -43,10 +37,10 @@ module Liquid
output = '' output = ''
@blocks.each do |block| @blocks.each do |block|
if block.else? if block.else?
return block.attachment.render(context) if execute_else_block return render_all(block.attachment, context) if execute_else_block
elsif block.evaluate(context) elsif block.evaluate(context)
execute_else_block = false execute_else_block = false
output << block.attachment.render(context) output << render_all(block.attachment, context)
end end
end end
output output
@@ -56,28 +50,27 @@ module Liquid
private private
def record_when_condition(markup) def record_when_condition(markup)
body = BlockBody.new
while markup while markup
unless markup =~ WhenSyntax # Create a new nodelist and assign it to the new block
if not markup =~ WhenSyntax
raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_when".freeze)) raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_when".freeze))
end end
markup = $2 markup = $2
block = Condition.new(@left, '=='.freeze, Expression.parse($1)) block = Condition.new(@left, '=='.freeze, $1)
block.attach(body) block.attach(@nodelist)
@blocks << block @blocks.push(block)
end end
end end
def record_else_condition(markup) def record_else_condition(markup)
unless markup.strip.empty? if not markup.strip.empty?
raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_else".freeze)) raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_else".freeze))
end end
block = ElseCondition.new block = ElseCondition.new
block.attach(BlockBody.new) block.attach(@nodelist)
@blocks << block @blocks << block
end end
end end

View File

@@ -1,10 +1,10 @@
module Liquid module Liquid
class Comment < Block class Comment < Block
def render(_context) def render(context)
''.freeze ''.freeze
end end
def unknown_tag(_tag, _markup, _tokens) def unknown_tag(tag, markup, tokens)
end end
def blank? def blank?

View File

@@ -20,10 +20,10 @@ module Liquid
case markup case markup
when NamedSyntax when NamedSyntax
@variables = variables_from_string($2) @variables = variables_from_string($2)
@name = Expression.parse($1) @name = $1
when SimpleSyntax when SimpleSyntax
@variables = variables_from_string(markup) @variables = variables_from_string(markup)
@name = @variables.to_s @name = "'#{@variables.to_s}'"
else else
raise SyntaxError.new(options[:locale].t("errors.syntax.cycle".freeze)) raise SyntaxError.new(options[:locale].t("errors.syntax.cycle".freeze))
end end
@@ -33,11 +33,11 @@ module Liquid
context.registers[:cycle] ||= Hash.new(0) context.registers[:cycle] ||= Hash.new(0)
context.stack do context.stack do
key = context.evaluate(@name) key = context[@name]
iteration = context.registers[:cycle][key] iteration = context.registers[:cycle][key]
result = context.evaluate(@variables[iteration]) result = context[@variables[iteration]]
iteration += 1 iteration += 1
iteration = 0 if iteration >= @variables.size iteration = 0 if iteration >= @variables.size
context.registers[:cycle][key] = iteration context.registers[:cycle][key] = iteration
result result
end end
@@ -48,7 +48,7 @@ module Liquid
def variables_from_string(markup) def variables_from_string(markup)
markup.split(',').collect do |var| markup.split(',').collect do |var|
var =~ /\s*(#{QuotedFragment})\s*/o var =~ /\s*(#{QuotedFragment})\s*/o
$1 ? Expression.parse($1) : nil $1 ? $1 : nil
end.compact end.compact
end end
end end

View File

@@ -1,4 +1,5 @@
module Liquid module Liquid
# decrement is used in a place where one needs to insert a counter # decrement is used in a place where one needs to insert a counter
# into a template, and needs the counter to survive across # into a template, and needs the counter to survive across
# multiple instantiations of the template. # multiple instantiations of the template.
@@ -25,10 +26,12 @@ module Liquid
def render(context) def render(context)
value = context.environments.first[@variable] ||= 0 value = context.environments.first[@variable] ||= 0
value -= 1 value = value - 1
context.environments.first[@variable] = value context.environments.first[@variable] = value
value.to_s value.to_s
end end
private
end end
Template.register_tag('decrement'.freeze, Decrement) Template.register_tag('decrement'.freeze, Decrement)

View File

@@ -1,4 +1,5 @@
module Liquid module Liquid
# "For" iterates over an array or collection. # "For" iterates over an array or collection.
# Several useful variables are available to you within the loop. # Several useful variables are available to you within the loop.
# #
@@ -23,7 +24,7 @@ module Liquid
# {{ item.name }} # {{ item.name }}
# {% end %} # {% end %}
# #
# To reverse the for loop simply use {% for item in collection reversed %} (note that the flag's spelling is different to the filter `reverse`) # To reverse the for loop simply use {% for item in collection reversed %}
# #
# == Available variables: # == Available variables:
# #
@@ -41,44 +42,85 @@ module Liquid
# where 0 is the last item. # where 0 is the last item.
# forloop.first:: Returns true if the item is the first item. # forloop.first:: Returns true if the item is the first item.
# forloop.last:: Returns true if the item is the last item. # forloop.last:: Returns true if the item is the last item.
# forloop.parentloop:: Provides access to the parent loop, if present.
# #
class For < Block class For < Block
Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
attr_reader :collection_name
attr_reader :variable_name
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
@from = @limit = nil
parse_with_selected_parser(markup) parse_with_selected_parser(markup)
@for_block = BlockBody.new @nodelist = @for_block = []
@else_block = nil
end
def parse(tokens)
return unless parse_body(@for_block, tokens)
parse_body(@else_block, tokens)
end end
def nodelist def nodelist
@else_block ? [@for_block, @else_block] : [@for_block] if @else_block
@for_block + @else_block
else
@for_block
end
end end
def unknown_tag(tag, markup, tokens) def unknown_tag(tag, markup, tokens)
return super unless tag == 'else'.freeze return super unless tag == 'else'.freeze
@else_block = BlockBody.new @nodelist = @else_block = []
end end
def render(context) def render(context)
segment = collection_segment(context) context.registers[:for] ||= Hash.new(0)
if segment.empty? collection = context[@collection_name]
render_else(context) collection = collection.to_a if collection.is_a?(Range)
# Maintains Ruby 1.8.7 String#each behaviour on 1.9
return render_else(context) unless iterable?(collection)
from = if @attributes['offset'.freeze] == 'continue'.freeze
context.registers[:for][@name].to_i
else else
render_segment(context, segment) context[@attributes['offset'.freeze]].to_i
end end
limit = context[@attributes['limit'.freeze]]
to = limit ? limit.to_i + from : nil
segment = Utils.slice_collection(collection, from, to)
return render_else(context) if segment.empty?
segment.reverse! if @reversed
result = ''
length = segment.length
# Store our progress through the collection for the continue flag
context.registers[:for][@name] = from + segment.length
context.stack do
segment.each_with_index do |item, index|
context[@variable_name] = item
context['forloop'.freeze] = {
'name'.freeze => @name,
'length'.freeze => length,
'index'.freeze => index + 1,
'index0'.freeze => index,
'rindex'.freeze => length - index,
'rindex0'.freeze => length - index - 1,
'first'.freeze => (index == 0),
'last'.freeze => (index == length - 1)
}
result << render_all(@for_block, context)
# Handle any interrupts if they exist.
if context.has_interrupt?
interrupt = context.pop_interrupt
break if interrupt.is_a? BreakInterrupt
next if interrupt.is_a? ContinueInterrupt
end
end
end
result
end end
protected protected
@@ -86,12 +128,12 @@ module Liquid
def lax_parse(markup) def lax_parse(markup)
if markup =~ Syntax if markup =~ Syntax
@variable_name = $1 @variable_name = $1
collection_name = $2 @collection_name = $2
@reversed = !!$3 @name = "#{$1}-#{$2}"
@name = "#{@variable_name}-#{collection_name}" @reversed = $3
@collection_name = Expression.parse(collection_name) @attributes = {}
markup.scan(TagAttributes) do |key, value| markup.scan(TagAttributes) do |key, value|
set_attribute(key, value) @attributes[key] = value
end end
else else
raise SyntaxError.new(options[:locale].t("errors.syntax.for".freeze)) raise SyntaxError.new(options[:locale].t("errors.syntax.for".freeze))
@@ -101,96 +143,31 @@ module Liquid
def strict_parse(markup) def strict_parse(markup)
p = Parser.new(markup) p = Parser.new(markup)
@variable_name = p.consume(:id) @variable_name = p.consume(:id)
raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_in".freeze)) unless p.id?('in'.freeze) raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_in".freeze)) unless p.id?('in'.freeze)
collection_name = p.expression @collection_name = p.expression
@name = "#{@variable_name}-#{collection_name}" @name = "#{@variable_name}-#{@collection_name}"
@collection_name = Expression.parse(collection_name)
@reversed = p.id?('reversed'.freeze) @reversed = p.id?('reversed'.freeze)
@attributes = {}
while p.look(:id) && p.look(:colon, 1) while p.look(:id) && p.look(:colon, 1)
unless attribute = p.id?('limit'.freeze) || p.id?('offset'.freeze) unless attribute = p.id?('limit'.freeze) || p.id?('offset'.freeze)
raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_attribute".freeze)) raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_attribute".freeze))
end end
p.consume p.consume
set_attribute(attribute, p.expression) val = p.expression
@attributes[attribute] = val
end end
p.consume(:end_of_string) p.consume(:end_of_string)
end end
private private
def collection_segment(context)
offsets = context.registers[:for] ||= Hash.new(0)
from = if @from == :continue
offsets[@name].to_i
else
context.evaluate(@from).to_i
end
collection = context.evaluate(@collection_name)
collection = collection.step(1).to_a if collection.is_a?(Range)
limit = context.evaluate(@limit)
to = limit ? limit.to_i + from : nil
segment = Utils.slice_collection(collection, from, to)
segment.reverse! if @reversed
offsets[@name] = from + segment.length
segment
end
def render_segment(context, 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
segment.each_with_index do |item, index|
context[@variable_name] = item
result << @for_block.render(context)
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
end
ensure
for_stack.pop
end
end
result
end
def set_attribute(key, expr)
case key
when 'offset'.freeze
@from = if expr == 'continue'.freeze
:continue
else
Expression.parse(expr)
end
when 'limit'.freeze
@limit = Expression.parse(expr)
end
end
def render_else(context) def render_else(context)
@else_block ? @else_block.render(context) : ''.freeze return @else_block ? [render_all(@else_block, context)] : ''.freeze
end
def iterable?(collection)
collection.respond_to?(:each) || Utils.non_blank_string?(collection)
end end
end end

View File

@@ -20,13 +20,8 @@ module Liquid
push_block('if'.freeze, markup) push_block('if'.freeze, markup)
end end
def parse(tokens)
while parse_body(@blocks.last.attachment, tokens)
end
end
def nodelist def nodelist
@blocks.map(&:attachment) @blocks.flat_map(&:attachment)
end end
def unknown_tag(tag, markup, tokens) def unknown_tag(tag, markup, tokens)
@@ -41,7 +36,7 @@ module Liquid
context.stack do context.stack do
@blocks.each do |block| @blocks.each do |block|
if block.evaluate(context) if block.evaluate(context)
return block.attachment.render(context) return render_all(block.attachment, context)
end end
end end
''.freeze ''.freeze
@@ -50,64 +45,61 @@ module Liquid
private private
def push_block(tag, markup) def push_block(tag, markup)
block = if tag == 'else'.freeze block = if tag == 'else'.freeze
ElseCondition.new ElseCondition.new
else else
parse_with_selected_parser(markup) parse_with_selected_parser(markup)
end
@blocks.push(block)
@nodelist = block.attach(Array.new)
end end
@blocks.push(block) def lax_parse(markup)
block.attach(BlockBody.new) expressions = markup.scan(ExpressionsAndOperators)
end raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop =~ Syntax
def lax_parse(markup) condition = Condition.new($1, $2, $3)
expressions = markup.scan(ExpressionsAndOperators)
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop =~ Syntax
condition = Condition.new(Expression.parse($1), $2, Expression.parse($3)) while not expressions.empty?
operator = expressions.pop.to_s.strip
until expressions.empty? raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop.to_s =~ Syntax
operator = expressions.pop.to_s.strip
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop.to_s =~ Syntax new_condition = Condition.new($1, $2, $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
new_condition = Condition.new(Expression.parse($1), $2, Expression.parse($3)) condition
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless BOOLEAN_OPERATORS.include?(operator)
new_condition.send(operator, condition)
condition = new_condition
end end
condition def strict_parse(markup)
end p = Parser.new(markup)
def strict_parse(markup) condition = parse_comparison(p)
p = Parser.new(markup)
condition = parse_binary_comparisons(p)
p.consume(:end_of_string)
condition
end
def parse_binary_comparisons(p) while op = (p.id?('and'.freeze) || p.id?('or'.freeze))
condition = parse_comparison(p) new_cond = parse_comparison(p)
first_condition = condition new_cond.send(op, condition)
while op = (p.id?('and'.freeze) || p.id?('or'.freeze)) condition = new_cond
child_condition = parse_comparison(p) end
condition.send(op, child_condition) p.consume(:end_of_string)
condition = child_condition
condition
end end
first_condition
end
def parse_comparison(p) def parse_comparison(p)
a = Expression.parse(p.expression) a = p.expression
if op = p.consume?(:comparison) if op = p.consume?(:comparison)
b = Expression.parse(p.expression) b = p.expression
Condition.new(a, op, b) Condition.new(a, op, b)
else else
Condition.new(a) Condition.new(a)
end
end end
end
end end
Template.register_tag('if'.freeze, If) Template.register_tag('if'.freeze, If)

View File

@@ -1,7 +1,9 @@
module Liquid module Liquid
class Ifchanged < Block class Ifchanged < Block
def render(context) def render(context)
context.stack do context.stack do
output = super output = super
if output != context.registers[:ifchanged] if output != context.registers[:ifchanged]

View File

@@ -1,4 +1,5 @@
module Liquid module Liquid
# Include allows templates to relate with other templates # Include allows templates to relate with other templates
# #
# Simply include another template: # Simply include another template:
@@ -21,15 +22,12 @@ module Liquid
if markup =~ Syntax if markup =~ Syntax
template_name = $1 @template_name = $1
variable_name = $3 @variable_name = $3
@attributes = {}
@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(TagAttributes) do |key, value|
@attributes[key] = Expression.parse(value) @attributes[key] = value
end end
else else
@@ -37,76 +35,69 @@ module Liquid
end end
end end
def parse(_tokens) def parse(tokens)
end end
def render(context) def render(context)
template_name = context.evaluate(@template_name_expr) partial = load_cached_partial(context)
raise ArgumentError.new(options[:locale].t("errors.argument.include")) unless template_name variable = context[@variable_name || @template_name[1..-2]]
partial = load_cached_partial(template_name, context) context.stack do
context_variable_name = template_name.split('/'.freeze).last @attributes.each do |key, value|
context[key] = context[value]
end
variable = if @variable_name_expr context_variable_name = @template_name[1..-2].split('/'.freeze).last
context.evaluate(@variable_name_expr) if variable.is_a?(Array)
else variable.collect do |var|
context.find_variable(template_name, raise_on_not_found: false) context[context_variable_name] = var
end
old_template_name = context.template_name
old_partial = context.partial
begin
context.template_name = template_name
context.partial = true
context.stack do
@attributes.each do |key, value|
context[key] = context.evaluate(value)
end
if variable.is_a?(Array)
variable.collect do |var|
context[context_variable_name] = var
partial.render(context)
end
else
context[context_variable_name] = variable
partial.render(context) partial.render(context)
end end
else
context[context_variable_name] = variable
partial.render(context)
end end
ensure
context.template_name = old_template_name
context.partial = old_partial
end end
end end
private private
def load_cached_partial(context)
cached_partials = context.registers[:cached_partials] || {}
template_name = context[@template_name]
alias_method :parse_context, :options if cached = cached_partials[template_name]
private :parse_context return cached
end
def load_cached_partial(template_name, context) source = read_template_from_file_system(context)
cached_partials = context.registers[:cached_partials] || {} partial = Liquid::Template.parse(source, pass_options)
cached_partials[template_name] = partial
if cached = cached_partials[template_name] context.registers[:cached_partials] = cached_partials
return cached partial
end end
source = read_template_from_file_system(context)
begin def read_template_from_file_system(context)
parse_context.partial = true file_system = context.registers[:file_system] || Liquid::Template.file_system
partial = Liquid::Template.parse(source, parse_context)
ensure # make read_template_file call backwards-compatible.
parse_context.partial = false case file_system.method(:read_template_file).arity
when 1
file_system.read_template_file(context[@template_name])
when 2
file_system.read_template_file(context[@template_name], context)
else
raise ArgumentError, "file_system.read_template_file expects two parameters: (template_name, context)"
end
end end
cached_partials[template_name] = partial
context.registers[:cached_partials] = cached_partials
partial
end
def read_template_from_file_system(context) def pass_options
file_system = context.registers[:file_system] || Liquid::Template.file_system dont_pass = @options[:include_options_blacklist]
return {locale: @options[:locale]} if dont_pass == true
file_system.read_template_file(context.evaluate(@template_name_expr)) opts = @options.merge(included: true, include_options_blacklist: false)
end if dont_pass.is_a?(Array)
dont_pass.each {|o| opts.delete(o)}
end
opts
end
end end
Template.register_tag('include'.freeze, Include) Template.register_tag('include'.freeze, Include)

View File

@@ -1,44 +1,16 @@
module Liquid module Liquid
class Raw < Block class Raw < Block
Syntax = /\A\s*\z/
FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
def initialize(tag_name, markup, parse_context)
super
ensure_valid_markup(tag_name, markup, parse_context)
end
def parse(tokens) def parse(tokens)
@body = '' @nodelist ||= []
@nodelist.clear
while token = tokens.shift while token = tokens.shift
if token =~ FullTokenPossiblyInvalid if token =~ FullTokenPossiblyInvalid
@body << $1 if $1 != "".freeze @nodelist << $1 if $1 != "".freeze
return if block_delimiter == $2 return if block_delimiter == $2
end end
@body << token unless token.empty? @nodelist << token if not token.empty?
end
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name))
end
def render(_context)
@body
end
def nodelist
[@body]
end
def blank?
@body.empty?
end
protected
def ensure_valid_markup(tag_name, markup, parse_context)
unless markup =~ Syntax
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_unexpected_args".freeze, tag: tag_name))
end end
end end
end end

View File

@@ -6,10 +6,10 @@ module Liquid
super super
if markup =~ Syntax if markup =~ Syntax
@variable_name = $1 @variable_name = $1
@collection_name = Expression.parse($2) @collection_name = $2
@attributes = {} @attributes = {}
markup.scan(TagAttributes) do |key, value| markup.scan(TagAttributes) do |key, value|
@attributes[key] = Expression.parse(value) @attributes[key] = value
end end
else else
raise SyntaxError.new(options[:locale].t("errors.syntax.table_row".freeze)) raise SyntaxError.new(options[:locale].t("errors.syntax.table_row".freeze))
@@ -17,32 +17,50 @@ module Liquid
end end
def render(context) def render(context)
collection = context.evaluate(@collection_name) or return ''.freeze collection = context[@collection_name] or return ''.freeze
from = @attributes.key?('offset'.freeze) ? context.evaluate(@attributes['offset'.freeze]).to_i : 0 from = @attributes['offset'.freeze] ? context[@attributes['offset'.freeze]].to_i : 0
to = @attributes.key?('limit'.freeze) ? from + context.evaluate(@attributes['limit'.freeze]).to_i : nil to = @attributes['limit'.freeze] ? from + context[@attributes['limit'.freeze]].to_i : nil
collection = Utils.slice_collection(collection, from, to) collection = Utils.slice_collection(collection, from, to)
length = collection.length length = collection.length
cols = context.evaluate(@attributes['cols'.freeze]).to_i cols = context[@attributes['cols'.freeze]].to_i
row = 1
col = 0
result = "<tr class=\"row1\">\n" result = "<tr class=\"row1\">\n"
context.stack do context.stack do
tablerowloop = Liquid::TablerowloopDrop.new(length, cols)
context['tablerowloop'.freeze] = tablerowloop
collection.each_with_index do |item, index| collection.each_with_index do |item, index|
context[@variable_name] = item context[@variable_name] = item
context['tablerowloop'.freeze] = {
'length'.freeze => length,
'index'.freeze => index + 1,
'index0'.freeze => index,
'col'.freeze => col + 1,
'col0'.freeze => col,
'rindex'.freeze => length - index,
'rindex0'.freeze => length - index - 1,
'first'.freeze => (index == 0),
'last'.freeze => (index == length - 1),
'col_first'.freeze => (col == 0),
'col_last'.freeze => (col == cols - 1)
}
result << "<td class=\"col#{tablerowloop.col}\">" << super << '</td>'
if tablerowloop.col_last && !tablerowloop.last col += 1
result << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">"
result << "<td class=\"col#{col}\">" << super << '</td>'
if col == cols and (index != length - 1)
col = 0
row += 1
result << "</tr>\n<tr class=\"row#{row}\">"
end end
tablerowloop.send(:increment!)
end end
end end
result << "</tr>\n" result << "</tr>\n"

View File

@@ -1,23 +1,24 @@
require_relative 'if' require File.dirname(__FILE__) + '/if'
module Liquid module Liquid
# Unless is a conditional just like 'if' but works on the inverse logic. # Unless is a conditional just like 'if' but works on the inverse logic.
# #
# {% unless x < 0 %} x is greater than zero {% endunless %} # {% unless x < 0 %} x is greater than zero {% end %}
# #
class Unless < If class Unless < If
def render(context) def render(context)
context.stack do context.stack do
# First condition is interpreted backwards ( if not ) # First condition is interpreted backwards ( if not )
first_block = @blocks.first first_block = @blocks.first
unless first_block.evaluate(context) unless first_block.evaluate(context)
return first_block.attachment.render(context) return render_all(first_block.attachment, context)
end end
# After the first condition unless works just like if # After the first condition unless works just like if
@blocks[1..-1].each do |block| @blocks[1..-1].each do |block|
if block.evaluate(context) if block.evaluate(context)
return block.attachment.render(context) return render_all(block.attachment, context)
end end
end end

View File

@@ -1,4 +1,5 @@
module Liquid module Liquid
# Templates are central to liquid. # Templates are central to liquid.
# Interpretating templates is a two step process. First you compile the # Interpretating templates is a two step process. First you compile the
# source code you got. During compile time some extensive error checking is performed. # source code you got. During compile time some extensive error checking is performed.
@@ -13,21 +14,21 @@ module Liquid
# template.render('user_name' => 'bob') # template.render('user_name' => 'bob')
# #
class Template class Template
attr_accessor :root DEFAULT_OPTIONS = {
attr_reader :resource_limits, :warnings :locale => I18n.new
}
attr_accessor :root, :resource_limits
@@file_system = BlankFileSystem.new @@file_system = BlankFileSystem.new
class TagRegistry class TagRegistry
include Enumerable
def initialize def initialize
@tags = {} @tags = {}
@cache = {} @cache = {}
end end
def [](tag_name) def [](tag_name)
return nil unless @tags.key?(tag_name) return nil unless @tags.has_key?(tag_name)
return @cache[tag_name] if Liquid.cache_classes return @cache[tag_name] if Liquid.cache_classes
lookup_class(@tags[tag_name]).tap { |o| @cache[tag_name] = o } lookup_class(@tags[tag_name]).tap { |o| @cache[tag_name] = o }
@@ -43,10 +44,6 @@ module Liquid
@cache.delete(tag_name) @cache.delete(tag_name)
end end
def each(&block)
@tags.each(&block)
end
private private
def lookup_class(name) def lookup_class(name)
@@ -69,11 +66,6 @@ module Liquid
# :error raises an error when tainted output is used # :error raises an error when tainted output is used
attr_writer :taint_mode attr_writer :taint_mode
attr_accessor :default_exception_renderer
Template.default_exception_renderer = lambda do |exception|
exception
end
def file_system def file_system
@@file_system @@file_system
end end
@@ -91,11 +83,11 @@ module Liquid
end end
def error_mode def error_mode
@error_mode ||= :lax @error_mode || :lax
end end
def taint_mode def taint_mode
@taint_mode ||= :lax @taint_mode || :lax
end end
# Pass a module with filter methods which should be available # Pass a module with filter methods which should be available
@@ -118,8 +110,7 @@ module Liquid
end end
def initialize def initialize
@rethrow_errors = false @resource_limits = self.class.default_resource_limits.dup
@resource_limits = ResourceLimits.new(self.class.default_resource_limits)
end end
# Parse source code. # Parse source code.
@@ -128,12 +119,16 @@ module Liquid
@options = options @options = options
@profiling = options[:profile] @profiling = options[:profile]
@line_numbers = options[:line_numbers] || @profiling @line_numbers = options[:line_numbers] || @profiling
parse_context = options.is_a?(ParseContext) ? options : ParseContext.new(options) @root = Document.parse(tokenize(source), DEFAULT_OPTIONS.merge(options))
@root = Document.parse(tokenize(source), parse_context) @warnings = nil
@warnings = parse_context.warnings
self self
end end
def warnings
return [] unless @root
@warnings ||= @root.warnings
end
def registers def registers
@registers ||= {} @registers ||= {}
end end
@@ -172,7 +167,7 @@ module Liquid
c = args.shift c = args.shift
if @rethrow_errors if @rethrow_errors
c.exception_renderer = ->(e) { raise } c.exception_handler = ->(e) { true }
end end
c c
@@ -191,20 +186,27 @@ module Liquid
when Hash when Hash
options = args.pop options = args.pop
registers.merge!(options[:registers]) if options[:registers].is_a?(Hash) if options[:registers].is_a?(Hash)
self.registers.merge!(options[:registers])
end
apply_options_to_context(context, options) if options[:filters]
when Module, Array context.add_filters(options[:filters])
end
if options[:exception_handler]
context.exception_handler = options[:exception_handler]
end
when Module
context.add_filters(args.pop)
when Array
context.add_filters(args.pop) context.add_filters(args.pop)
end end
# Retrying a render resets resource usage
context.resource_limits.reset
begin begin
# render the nodelist. # render the nodelist.
# for performance reasons we get an array back here. join will make a string out of it. # for performance reasons we get an array back here. join will make a string out of it.
result = with_profiling(context) do result = with_profiling do
@root.render(context) @root.render(context)
end end
result.respond_to?(:join) ? result.join : result result.respond_to?(:join) ? result.join : result
@@ -222,14 +224,32 @@ module Liquid
private private
# Uses the <tt>Liquid::TemplateParser</tt> regexp to tokenize the passed source
def tokenize(source) def tokenize(source)
Tokenizer.new(source, @line_numbers) source = source.source if source.respond_to?(:source)
return [] if source.to_s.empty?
tokens = calculate_line_numbers(source.split(TemplateParser))
# removes the rogue empty element at the beginning of the array
tokens.shift if tokens[0] and tokens[0].empty?
tokens
end end
def with_profiling(context) def calculate_line_numbers(raw_tokens)
if @profiling && !context.partial return raw_tokens unless @line_numbers
raise "Profiler not loaded, require 'liquid/profiler' first" unless defined?(Liquid::Profiler)
current_line = 1
raw_tokens.map do |token|
Token.new(token, current_line).tap do
current_line += token.count("\n")
end
end
end
def with_profiling
if @profiling && !@options[:included]
@profiler = Profiler.new @profiler = Profiler.new
@profiler.start @profiler.start
@@ -242,13 +262,5 @@ module Liquid
yield yield
end end
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
end end

18
lib/liquid/token.rb Normal file
View File

@@ -0,0 +1,18 @@
module Liquid
class Token < String
attr_reader :line_number
def initialize(content, line_number)
super(content)
@line_number = line_number
end
def raw
"<raw>"
end
def child(string)
Token.new(string, @line_number)
end
end
end

View File

@@ -1,31 +0,0 @@
module Liquid
class Tokenizer
attr_reader :line_number
def initialize(source, line_numbers = false)
@source = source
@line_number = line_numbers ? 1 : nil
@tokens = tokenize
end
def shift
token = @tokens.shift
@line_number += token.count("\n") if @line_number && token
token
end
private
def tokenize
@source = @source.source if @source.respond_to?(:source)
return [] if @source.to_s.empty?
tokens = @source.split(TemplateParser)
# removes the rogue empty element at the beginning of the array
tokens.shift if tokens[0] && tokens[0].empty?
tokens
end
end
end

View File

@@ -1,24 +1,27 @@
module Liquid module Liquid
module Utils module Utils
def self.slice_collection(collection, from, to) def self.slice_collection(collection, from, to)
if (from != 0 || !to.nil?) && collection.respond_to?(:load_slice) if (from != 0 || to != nil) && collection.respond_to?(:load_slice)
collection.load_slice(from, to) collection.load_slice(from, to)
else else
slice_collection_using_each(collection, from, to) slice_collection_using_each(collection, from, to)
end end
end end
def self.non_blank_string?(collection)
collection.is_a?(String) && collection != ''.freeze
end
def self.slice_collection_using_each(collection, from, to) def self.slice_collection_using_each(collection, from, to)
segments = [] segments = []
index = 0 index = 0
# Maintains Ruby 1.8.7 String#each behaviour on 1.9 # Maintains Ruby 1.8.7 String#each behaviour on 1.9
if collection.is_a?(String) return [collection] if non_blank_string?(collection)
return collection.empty? ? [] : [collection]
end
return [] unless collection.respond_to?(:each)
collection.each do |item| collection.each do |item|
if to && to <= index if to && to <= index
break break
end end
@@ -32,52 +35,5 @@ module Liquid
segments segments
end end
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"
end
end
def self.to_number(obj)
case obj
when Float
BigDecimal.new(obj.to_s)
when Numeric
obj
when String
(obj.strip =~ /\A-?\d+\.\d+\z/) ? BigDecimal.new(obj) : obj.to_i
else
if obj.respond_to?(:to_number)
obj.to_number
else
0
end
end
end
def self.to_date(obj)
return obj if obj.respond_to?(:strftime)
if obj.is_a?(String)
return nil if obj.empty?
obj = obj.downcase
end
case obj
when 'now'.freeze, 'today'.freeze
Time.now
when /\A\d+\z/, Integer
Time.at(obj.to_i)
when String
Time.parse(obj)
end
rescue ::ArgumentError
nil
end
end end
end end

View File

@@ -1,4 +1,5 @@
module Liquid module Liquid
# Holds variables. Variables are only loaded "just in time" # Holds variables. Variables are only loaded "just in time"
# and are not evaluated as part of the render stage # and are not evaluated as part of the render stage
# #
@@ -11,16 +12,15 @@ module Liquid
# #
class Variable class Variable
FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o
attr_accessor :filters, :name, :line_number EasyParse = /\A *(\w+(?:\.\w+)*) *\z/
attr_reader :parse_context attr_accessor :filters, :name, :warnings
alias_method :options, :parse_context attr_accessor :line_number
include ParserSwitching include ParserSwitching
def initialize(markup, parse_context) def initialize(markup, options = {})
@markup = markup @markup = markup
@name = nil @name = nil
@parse_context = parse_context @options = options || {}
@line_number = parse_context.line_number
parse_with_selected_parser(markup) parse_with_selected_parser(markup)
end end
@@ -35,27 +35,35 @@ module Liquid
def lax_parse(markup) def lax_parse(markup)
@filters = [] @filters = []
return unless markup =~ /(#{QuotedFragment})(.*)/om if markup =~ /(#{QuotedFragment})(.*)/om
name_markup = $1
name_markup = $1 filter_markup = $2
filter_markup = $2 @name = Expression.parse(name_markup)
@name = Expression.parse(name_markup) if filter_markup =~ /#{FilterSeparator}\s*(.*)/om
if filter_markup =~ /#{FilterSeparator}\s*(.*)/om filters = $1.scan(FilterParser)
filters = $1.scan(FilterParser) filters.each do |f|
filters.each do |f| if f =~ /\w+/
next unless f =~ /\w+/ filtername = Regexp.last_match(0)
filtername = Regexp.last_match(0) filterargs = f.scan(/(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o).flatten
filterargs = f.scan(/(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o).flatten @filters << parse_filter_expressions(filtername, filterargs)
@filters << parse_filter_expressions(filtername, filterargs) end
end
end end
end end
end end
def strict_parse(markup) def strict_parse(markup)
# Very simple valid cases
if markup =~ EasyParse
@name = Expression.parse($1)
@filters = []
return
end
@filters = [] @filters = []
p = Parser.new(markup) p = Parser.new(markup)
# Could be just filters with no input
@name = Expression.parse(p.expression) @name = p.look(:pipe) ? nil : Expression.parse(p.expression)
while p.consume?(:pipe) while p.consume?(:pipe)
filtername = p.consume(:id) filtername = p.consume(:id)
filterargs = p.consume?(:colon) ? parse_filterargs(p) : [] filterargs = p.consume?(:colon) ? parse_filterargs(p) : []
@@ -68,21 +76,17 @@ module Liquid
# first argument # first argument
filterargs = [p.argument] filterargs = [p.argument]
# followed by comma separated others # followed by comma separated others
filterargs << p.argument while p.consume?(:comma) while p.consume?(:comma)
filterargs << p.argument
end
filterargs filterargs
end end
def render(context) def render(context)
obj = @filters.inject(context.evaluate(@name)) do |output, (filter_name, filter_args, filter_kwargs)| @filters.inject(context.evaluate(@name)) do |output, (filter_name, filter_args, filter_kwargs)|
filter_args = evaluate_filter_expressions(context, filter_args, filter_kwargs) filter_args = evaluate_filter_expressions(context, filter_args, filter_kwargs)
context.invoke(filter_name, output, *filter_args) output = context.invoke(filter_name, output, *filter_args)
end end.tap{ |obj| taint_check(obj) }
obj = context.apply_global_filter(obj)
taint_check(context, obj)
obj
end end
private private
@@ -114,22 +118,17 @@ module Liquid
parsed_args parsed_args
end end
def taint_check(context, obj) def taint_check(obj)
return unless obj.tainted? if obj.tainted?
return if Template.taint_mode == :lax @markup =~ QuotedFragment
name = Regexp.last_match(0)
@markup =~ QuotedFragment case Template.taint_mode
name = Regexp.last_match(0) when :warn
@warnings ||= []
error = TaintedError.new("variable '#{name}' is tainted and was not escaped") @warnings << "variable '#{name}' is tainted and was not escaped"
error.line_number = line_number when :error
error.template_name = context.template_name raise TaintedError, "Error - variable '#{name}' is tainted and was not escaped"
end
case Template.taint_mode
when :warn
context.warnings << error
when :error
raise error
end end
end end
end end

View File

@@ -3,8 +3,6 @@ module Liquid
SQUARE_BRACKETED = /\A\[(.*)\]\z/m SQUARE_BRACKETED = /\A\[(.*)\]\z/m
COMMAND_METHODS = ['size'.freeze, 'first'.freeze, 'last'.freeze] COMMAND_METHODS = ['size'.freeze, 'first'.freeze, 'last'.freeze]
attr_reader :name, :lookups
def self.parse(markup) def self.parse(markup)
new(markup) new(markup)
end end
@@ -41,8 +39,8 @@ module Liquid
# If object is a hash- or array-like object we look for the # If object is a hash- or array-like object we look for the
# presence of the key and if its available we return it # presence of the key and if its available we return it
if object.respond_to?(:[]) && if object.respond_to?(:[]) &&
((object.respond_to?(:key?) && object.key?(key)) || ((object.respond_to?(:has_key?) && object.has_key?(key)) ||
(object.respond_to?(:fetch) && key.is_a?(Integer))) (object.respond_to?(:fetch) && key.is_a?(Integer)))
# if its a proc we will replace the entry with the proc # if its a proc we will replace the entry with the proc
res = context.lookup_and_evaluate(object, key) res = context.lookup_and_evaluate(object, key)
@@ -55,11 +53,9 @@ module Liquid
object = object.send(key).to_liquid object = object.send(key).to_liquid
# No key was present with the desired value and it wasn't one of the directly supported # 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 or # keywords either. The only thing we got left is to return nil
# raise an exception if `strict_variables` option is set to true
else else
return nil unless context.strict_variables return nil
raise Liquid::UndefinedVariable, "undefined variable #{key}"
end end
# If we are dealing with a drop here we have to # If we are dealing with a drop here we have to
@@ -70,7 +66,7 @@ module Liquid
end end
def ==(other) def ==(other)
self.class == other.class && state == other.state self.class == other.class && self.state == other.state
end end
protected protected

View File

@@ -1,4 +1,4 @@
# encoding: utf-8 # encoding: utf-8
module Liquid module Liquid
VERSION = "4.0.0" VERSION = "3.0.1"
end end

View File

@@ -1,6 +1,6 @@
# encoding: utf-8 # encoding: utf-8
lib = File.expand_path('../lib/', __FILE__) lib = File.expand_path('../lib/', __FILE__)
$LOAD_PATH.unshift lib unless $LOAD_PATH.include?(lib) $:.unshift lib unless $:.include?(lib)
require "liquid/version" require "liquid/version"
@@ -9,22 +9,21 @@ Gem::Specification.new do |s|
s.version = Liquid::VERSION s.version = Liquid::VERSION
s.platform = Gem::Platform::RUBY s.platform = Gem::Platform::RUBY
s.summary = "A secure, non-evaling end user template engine with aesthetic markup." s.summary = "A secure, non-evaling end user template engine with aesthetic markup."
s.authors = ["Tobias Lütke"] s.authors = ["Tobias Luetke"]
s.email = ["tobi@leetsoft.com"] s.email = ["tobi@leetsoft.com"]
s.homepage = "http://www.liquidmarkup.org" s.homepage = "http://www.liquidmarkup.org"
s.license = "MIT" s.license = "MIT"
# s.description = "A secure, non-evaling end user template engine with aesthetic markup." #s.description = "A secure, non-evaling end user template engine with aesthetic markup."
s.required_ruby_version = ">= 2.1.0"
s.required_rubygems_version = ">= 1.3.7" s.required_rubygems_version = ">= 1.3.7"
s.test_files = Dir.glob("{test}/**/*") s.test_files = Dir.glob("{test}/**/*")
s.files = Dir.glob("{lib}/**/*") + %w(LICENSE README.md) s.files = Dir.glob("{lib}/**/*") + %w(MIT-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', '~> 11.3' s.add_development_dependency 'rake'
s.add_development_dependency 'minitest' s.add_development_dependency 'minitest'
end end

View File

@@ -1,11 +1,11 @@
require 'benchmark/ips' require 'benchmark/ips'
require_relative 'theme_runner' require File.dirname(__FILE__) + '/theme_runner'
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new profiler = ThemeRunner.new
Benchmark.ips do |x| Benchmark.ips do |x|
x.time = 10 x.time = 60
x.warmup = 5 x.warmup = 5
puts puts
@@ -13,6 +13,5 @@ Benchmark.ips do |x|
puts puts
x.report("parse:") { profiler.compile } x.report("parse:") { profiler.compile }
x.report("render:") { profiler.render } x.report("parse & run:") { profiler.run }
x.report("parse & render:") { profiler.run }
end end

View File

@@ -1,24 +1,17 @@
require 'stackprof' require 'stackprof' rescue fail("install stackprof extension/gem")
require_relative 'theme_runner' require File.dirname(__FILE__) + '/theme_runner'
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new profiler = ThemeRunner.new
profiler.run profiler.run
[:cpu, :object].each do |profile_type| [:cpu, :object].each do |profile_type|
puts "Profiling in #{profile_type} mode..." puts "Profiling in #{profile_type.to_s} mode..."
results = StackProf.run(mode: profile_type) do results = StackProf.run(mode: profile_type) do
200.times do 100.times do
profiler.run profiler.run
end end
end end
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) 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 end

View File

@@ -28,6 +28,6 @@ class CommentForm < Liquid::Block
end end
def wrap_in_form(article, input) def wrap_in_form(article, input)
%(<form id="article-#{article.id}-comment-form" class="comment-form" method="post" action="">\n#{input}\n</form>) %Q{<form id="article-#{article.id}-comment-form" class="comment-form" method="post" action="">\n#{input}\n</form>}
end end
end end

View File

@@ -5,7 +5,7 @@ module Database
# to liquid as assigns. All this is based on Shopify # to liquid as assigns. All this is based on Shopify
def self.tables def self.tables
@tables ||= begin @tables ||= begin
db = YAML.load_file("#{__dir__}/vision.database.yml") db = YAML.load_file(File.dirname(__FILE__) + '/vision.database.yml')
# From vision source # From vision source
db['products'].each do |product| db['products'].each do |product|
@@ -39,7 +39,7 @@ module Database
end end
end end
if __FILE__ == $PROGRAM_NAME if __FILE__ == $0
p Database.tables['collections']['frontpage'].keys p Database.tables['collections']['frontpage'].keys
# p Database.tables['blog']['articles'] #p Database.tables['blog']['articles']
end end

View File

@@ -1,7 +1,9 @@
require 'json' require 'json'
module JsonFilter module JsonFilter
def json(object) def json(object)
JSON.dump(object.reject { |k, v| k == "collections" }) JSON.dump(object.reject {|k,v| k == "collections" })
end end
end end

View File

@@ -1,13 +1,13 @@
$:.unshift __dir__ + '/../../lib' $:.unshift File.dirname(__FILE__) + '/../../lib'
require_relative '../../lib/liquid' require File.dirname(__FILE__) + '/../../lib/liquid'
require_relative 'comment_form' require File.dirname(__FILE__) + '/comment_form'
require_relative 'paginate' require File.dirname(__FILE__) + '/paginate'
require_relative 'json_filter' require File.dirname(__FILE__) + '/json_filter'
require_relative 'money_filter' require File.dirname(__FILE__) + '/money_filter'
require_relative 'shop_filter' require File.dirname(__FILE__) + '/shop_filter'
require_relative 'tag_filter' require File.dirname(__FILE__) + '/tag_filter'
require_relative 'weight_filter' require File.dirname(__FILE__) + '/weight_filter'
Liquid::Template.register_tag 'paginate', Paginate Liquid::Template.register_tag 'paginate', Paginate
Liquid::Template.register_tag 'form', CommentForm Liquid::Template.register_tag 'form', CommentForm

View File

@@ -1,12 +1,13 @@
module MoneyFilter module MoneyFilter
def money_with_currency(money) def money_with_currency(money)
return '' if money.nil? return '' if money.nil?
sprintf("$ %.2f USD", money / 100.0) sprintf("$ %.2f USD", money/100.0)
end end
def money(money) def money(money)
return '' if money.nil? return '' if money.nil?
sprintf("$ %.2f", money / 100.0) sprintf("$ %.2f", money/100.0)
end end
private private

View File

@@ -42,22 +42,23 @@ class Paginate < Liquid::Block
page_count = (collection_size.to_f / @page_size.to_f).to_f.ceil + 1 page_count = (collection_size.to_f / @page_size.to_f).to_f.ceil + 1
pagination['items'] = collection_size pagination['items'] = collection_size
pagination['pages'] = page_count - 1 pagination['pages'] = page_count -1
pagination['previous'] = link('&laquo; Previous', current_page - 1) unless 1 >= current_page pagination['previous'] = link('&laquo; Previous', current_page-1 ) unless 1 >= current_page
pagination['next'] = link('Next &raquo;', current_page + 1) unless page_count <= current_page + 1 pagination['next'] = link('Next &raquo;', current_page+1 ) unless page_count <= current_page+1
pagination['parts'] = [] pagination['parts'] = []
hellip_break = false hellip_break = false
if page_count > 2 if page_count > 2
1.upto(page_count - 1) do |page| 1.upto(page_count-1) do |page|
if current_page == page if current_page == page
pagination['parts'] << no_link(page) pagination['parts'] << no_link(page)
elsif page == 1 elsif page == 1
pagination['parts'] << link(page, page) pagination['parts'] << link(page, page)
elsif page == page_count - 1 elsif page == page_count -1
pagination['parts'] << link(page, page) pagination['parts'] << link(page, page)
elsif page <= current_page - @attributes['window_size'] || page >= current_page + @attributes['window_size'] elsif page <= current_page - @attributes['window_size'] or page >= current_page + @attributes['window_size']
next if hellip_break next if hellip_break
pagination['parts'] << no_link('&hellip;') pagination['parts'] << no_link('&hellip;')
hellip_break = true hellip_break = true
@@ -77,11 +78,11 @@ class Paginate < Liquid::Block
private private
def no_link(title) def no_link(title)
{ 'title' => title, 'is_link' => false } { 'title' => title, 'is_link' => false}
end end
def link(title, page) def link(title, page)
{ 'title' => title, 'url' => current_url + "?page=#{page}", 'is_link' => true } { 'title' => title, 'url' => current_url + "?page=#{page}", 'is_link' => true}
end end
def current_url def current_url

View File

@@ -1,4 +1,5 @@
module ShopFilter module ShopFilter
def asset_url(input) def asset_url(input)
"/files/1/[shop_id]/[shop_id]/assets/#{input}" "/files/1/[shop_id]/[shop_id]/assets/#{input}"
end end
@@ -15,16 +16,16 @@ module ShopFilter
%(<script src="#{url}" type="text/javascript"></script>) %(<script src="#{url}" type="text/javascript"></script>)
end end
def stylesheet_tag(url, media = "all") def stylesheet_tag(url, media="all")
%(<link href="#{url}" rel="stylesheet" type="text/css" media="#{media}" />) %(<link href="#{url}" rel="stylesheet" type="text/css" media="#{media}" />)
end end
def link_to(link, url, title = "") def link_to(link, url, title="")
%(<a href="#{url}" title="#{title}">#{link}</a>) %|<a href="#{url}" title="#{title}">#{link}</a>|
end end
def img_tag(url, alt = "") def img_tag(url, alt="")
%(<img src="#{url}" alt="#{alt}" />) %|<img src="#{url}" alt="#{alt}" />|
end end
def link_to_vendor(vendor) def link_to_vendor(vendor)
@@ -52,6 +53,7 @@ module ShopFilter
end end
def product_img_url(url, style = 'small') def product_img_url(url, style = 'small')
unless url =~ /\Aproducts\/([\w\-\_]+)\.(\w{2,4})/ unless url =~ /\Aproducts\/([\w\-\_]+)\.(\w{2,4})/
raise ArgumentError, 'filter "size" can only be called on product images' raise ArgumentError, 'filter "size" can only be called on product images'
end end
@@ -67,6 +69,7 @@ module ShopFilter
end end
def default_pagination(paginate) def default_pagination(paginate)
html = [] html = []
html << %(<span class="prev">#{link_to(paginate['previous']['title'], paginate['previous']['url'])}</span>) if paginate['previous'] html << %(<span class="prev">#{link_to(paginate['previous']['title'], paginate['previous']['url'])}</span>) if paginate['previous']
@@ -103,4 +106,5 @@ module ShopFilter
result.gsub!(/\A-+/, '') if result[0] == '-' result.gsub!(/\A-+/, '') if result[0] == '-'
result result
end end
end end

View File

@@ -1,9 +1,10 @@
module TagFilter module TagFilter
def link_to_tag(label, tag) def link_to_tag(label, tag)
"<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tag}\">#{label}</a>" "<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tag}\">#{label}</a>"
end end
def highlight_active_tag(tag, css_class = 'active') def highlight_active_tag(tag, css_class='active')
if @context['current_tags'].include?(tag) if @context['current_tags'].include?(tag)
"<span class=\"#{css_class}\">#{tag}</span>" "<span class=\"#{css_class}\">#{tag}</span>"
else else
@@ -20,4 +21,5 @@ module TagFilter
tags = (@context['current_tags'] - [tag]).uniq 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
end end

View File

@@ -1,4 +1,5 @@
module WeightFilter module WeightFilter
def weight(grams) def weight(grams)
sprintf("%.2f", grams / 1000) sprintf("%.2f", grams / 1000)
end end
@@ -6,4 +7,5 @@ module WeightFilter
def weight_with_unit(grams) def weight_with_unit(grams)
"#{weight(grams)} kg" "#{weight(grams)} kg"
end end
end end

View File

@@ -6,115 +6,73 @@
# Shopify which is likely the biggest user of liquid in the world which something to the tune of several # Shopify which is likely the biggest user of liquid in the world which something to the tune of several
# million Template#render calls a day. # million Template#render calls a day.
require_relative 'shopify/liquid' require File.dirname(__FILE__) + '/shopify/liquid'
require_relative 'shopify/database' require File.dirname(__FILE__) + '/shopify/database.rb'
class ThemeRunner class ThemeRunner
class FileSystem class FileSystem
def initialize(path) def initialize(path)
@path = path @path = path
end end
# Called by Liquid to retrieve a template file # Called by Liquid to retrieve a template file
def read_template_file(template_path) def read_template_file(template_path, context)
File.read(@path + '/' + template_path + '.liquid') File.read(@path + '/' + template_path + '.liquid')
end end
end end
# Initialize a new liquid ThemeRunner instance # Load all templates into memory, do this now so that
# Will load all templates into memory, do this now so that we don't profile IO. # we don't profile IO.
def initialize def initialize
@tests = Dir[__dir__ + '/tests/**/*.liquid'].collect do |test| @tests = Dir[File.dirname(__FILE__) + '/tests/**/*.liquid'].collect do |test|
next if File.basename(test) == 'theme.liquid' next if File.basename(test) == 'theme.liquid'
theme_path = File.dirname(test) + '/theme.liquid' theme_path = File.dirname(test) + '/theme.liquid'
{
liquid: File.read(test), [File.read(test), (File.file?(theme_path) ? File.read(theme_path) : nil), test]
layout: (File.file?(theme_path) ? File.read(theme_path) : nil),
template_name: test
}
end.compact end.compact
compile_all_tests
end end
# `compile` will test just the compilation portion of liquid without any templates
def compile def compile
@tests.each do |test_hash| # Dup assigns because will make some changes to them
Liquid::Template.new.parse(test_hash[:liquid])
Liquid::Template.new.parse(test_hash[:layout]) @tests.each do |liquid, layout, template_name|
tmpl = Liquid::Template.new
tmpl.parse(liquid)
tmpl = Liquid::Template.new
tmpl.parse(layout)
end end
end end
# `run` is called to benchmark rendering and compiling at the same time def run
def run
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 # Dup assigns because will make some changes to them
assigns = Database.tables.dup assigns = Database.tables.dup
@tests.each do |test_hash| @tests.each do |liquid, layout, template_name|
# Compute page_template outside of profiler run, uninteresting to profiler
page_template = File.basename(test_hash[:template_name], File.extname(test_hash[:template_name])) # Compute page_tempalte outside of profiler run, uninteresting to profiler
yield(test_hash[:liquid], test_hash[:layout], assigns, page_template, test_hash[:template_name]) page_template = File.basename(template_name, File.extname(template_name))
compile_and_render(liquid, layout, assigns, page_template, template_name)
end end
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) def compile_and_render(template, layout, assigns, page_template, template_file)
tmpl = Liquid::Template.new tmpl = Liquid::Template.new
tmpl.assigns['page_title'] = 'Page title' tmpl.assigns['page_title'] = 'Page title'
tmpl.assigns['template'] = page_template tmpl.assigns['template'] = page_template
tmpl.registers[:file_system] = ThemeRunner::FileSystem.new(File.dirname(template_file)) tmpl.registers[:file_system] = ThemeRunner::FileSystem.new(File.dirname(template_file))
tmpl
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
end end
end end

View File

@@ -15,24 +15,24 @@ class AssignTest < Minitest::Test
def test_assigned_variable def test_assigned_variable
assert_template_result('.foo.', assert_template_result('.foo.',
'{% assign foo = values %}.{{ foo[0] }}.', '{% assign foo = values %}.{{ foo[0] }}.',
'values' => %w(foo bar baz)) 'values' => %w{foo bar baz})
assert_template_result('.bar.', assert_template_result('.bar.',
'{% assign foo = values %}.{{ foo[1] }}.', '{% assign foo = values %}.{{ foo[1] }}.',
'values' => %w(foo bar baz)) 'values' => %w{foo bar baz})
end end
def test_assign_with_filter def test_assign_with_filter
assert_template_result('.bar.', assert_template_result('.bar.',
'{% assign foo = values | split: "," %}.{{ foo[1] }}.', '{% assign foo = values | split: "," %}.{{ foo[1] }}.',
'values' => "foo,bar,baz") 'values' => "foo,bar,baz")
end end
def test_assign_syntax_error def test_assign_syntax_error
assert_match_syntax_error(/assign/, assert_match_syntax_error(/assign/,
'{% assign foo not values %}.', '{% assign foo not values %}.',
'values' => "foo,bar,baz") 'values' => "foo,bar,baz")
end end
def test_assign_uses_error_mode def test_assign_uses_error_mode

View File

@@ -9,7 +9,7 @@ class FoobarTag < Liquid::Tag
end end
class BlankTestFileSystem class BlankTestFileSystem
def read_template_file(template_path) def read_template_file(template_path, context)
template_path template_path
end end
end end
@@ -31,7 +31,7 @@ class BlankTest < Minitest::Test
end end
def test_new_tags_are_not_blank_by_default def test_new_tags_are_not_blank_by_default
assert_template_result(" " * N, wrap_in_for("{% foobar %}")) assert_template_result(" "*N, wrap_in_for("{% foobar %}"))
end end
def test_loops_are_blank def test_loops_are_blank
@@ -47,7 +47,7 @@ class BlankTest < Minitest::Test
end end
def test_mark_as_blank_only_during_parsing 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 end
def test_comments_are_blank def test_comments_are_blank
@@ -60,9 +60,9 @@ class BlankTest < Minitest::Test
def test_nested_blocks_are_blank_but_only_if_all_children_are 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), assert_template_result("\n but this is not "*(N+1),
wrap('{% if true %} {% comment %} this is blank {% endcomment %} {% endif %} wrap(%q{{% if true %} {% comment %} this is blank {% endcomment %} {% endif %}
{% if true %} but this is not {% endif %}')) {% if true %} but this is not {% endif %}}))
end end
def test_assigns_are_blank def test_assigns_are_blank
@@ -76,31 +76,31 @@ class BlankTest < Minitest::Test
def test_whitespace_is_not_blank_if_other_stuff_is_present def test_whitespace_is_not_blank_if_other_stuff_is_present
body = " x " body = " x "
assert_template_result(body * (N + 1), wrap(body)) assert_template_result(body*(N+1), wrap(body))
end end
def test_increment_is_not_blank 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 end
def test_cycle_is_not_blank def test_cycle_is_not_blank
assert_template_result(" " * ((N + 1) / 2) + " ", wrap("{% cycle ' ', ' ' %}")) assert_template_result(" "*((N+1)/2)+" ", wrap("{% cycle ' ', ' ' %}"))
end end
def test_raw_is_not_blank def test_raw_is_not_blank
assert_template_result(" " * (N + 1), wrap(" {% raw %} {% endraw %}")) assert_template_result(" "*(N+1), wrap(" {% raw %} {% endraw %}"))
end end
def test_include_is_blank def test_include_is_blank
Liquid::Template.file_system = BlankTestFileSystem.new 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 " 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 " "*(N+1), wrap(" {% include ' ' %} ")
end end
def test_case_is_blank 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 = '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("", 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(" x "*(N+1), wrap(" {% assign foo = 'else' %} {% case foo %} {% when 'bar' %} {% when 'whatever' %} {% else %} x {% endcase %} "))
end end
end end

View File

@@ -18,14 +18,14 @@ class ContextTest < Minitest::Test
with_global_filter(global) do with_global_filter(global) do
assert_equal 'Global test', Template.parse("{{'test' | notice }}").render! assert_equal 'Global test', Template.parse("{{'test' | notice }}").render!
assert_equal 'Local test', Template.parse("{{'test' | notice }}").render!({}, filters: [local]) assert_equal 'Local test', Template.parse("{{'test' | notice }}").render!({}, :filters => [local])
end end
end end
def test_has_key_will_not_add_an_error_for_missing_keys def test_has_key_will_not_add_an_error_for_missing_keys
with_error_mode :strict do with_error_mode :strict do
context = Context.new context = Context.new
context.key?('unknown') context.has_key?('unknown')
assert_empty context.errors assert_empty context.errors
end end
end end

View File

@@ -1,19 +0,0 @@
require 'test_helper'
class DocumentTest < Minitest::Test
include Liquid
def test_unexpected_outer_tag
exc = assert_raises(SyntaxError) do
Template.parse("{% else %}")
end
assert_equal exc.message, "Liquid syntax error: Unexpected outer 'else' tag"
end
def test_unknown_tag
exc = assert_raises(SyntaxError) do
Template.parse("{% foo %}")
end
assert_equal exc.message, "Liquid syntax error: Unknown tag 'foo'"
end
end

View File

@@ -13,12 +13,13 @@ class ContextDrop < Liquid::Drop
@context['forloop.index'] @context['forloop.index']
end end
def liquid_method_missing(method) def before_method(method)
@context[method] return @context[method]
end end
end end
class ProductDrop < Liquid::Drop class ProductDrop < Liquid::Drop
class TextDrop < Liquid::Drop class TextDrop < Liquid::Drop
def array def array
['text1', 'text2'] ['text1', 'text2']
@@ -30,8 +31,8 @@ class ProductDrop < Liquid::Drop
end end
class CatchallDrop < Liquid::Drop class CatchallDrop < Liquid::Drop
def liquid_method_missing(method) def before_method(method)
'catchall_method: ' << method.to_s return 'method: ' << method.to_s
end end
end end
@@ -52,14 +53,13 @@ class ProductDrop < Liquid::Drop
end end
protected protected
def callmenot
def callmenot "protected"
"protected" end
end
end end
class EnumerableDrop < Liquid::Drop class EnumerableDrop < Liquid::Drop
def liquid_method_missing(method) def before_method(method)
method method
end end
@@ -93,7 +93,7 @@ end
class RealEnumerableDrop < Liquid::Drop class RealEnumerableDrop < Liquid::Drop
include Enumerable include Enumerable
def liquid_method_missing(method) def before_method(method)
method method
end end
@@ -124,10 +124,8 @@ class DropsTest < Minitest::Test
def test_rendering_warns_on_tainted_attr def test_rendering_warns_on_tainted_attr
with_taint_mode(:warn) do with_taint_mode(:warn) do
tpl = Liquid::Template.parse('{{ product.user_input }}') tpl = Liquid::Template.parse('{{ product.user_input }}')
context = Context.new('product' => ProductDrop.new) tpl.render!('product' => ProductDrop.new)
tpl.render!(context) assert_match /tainted/, tpl.warnings.first
assert_equal [Liquid::TaintedError], context.warnings.map(&:class)
assert_equal "variable 'product.user_input' is tainted and was not escaped", context.warnings.first.to_s(false)
end end
end end
@@ -153,37 +151,37 @@ class DropsTest < Minitest::Test
end end
def test_text_drop def test_text_drop
output = Liquid::Template.parse(' {{ product.texts.text }} ').render!('product' => ProductDrop.new) output = Liquid::Template.parse( ' {{ product.texts.text }} ' ).render!('product' => ProductDrop.new)
assert_equal ' text1 ', output assert_equal ' text1 ', output
end end
def test_catchall_unknown_method def test_unknown_method
output = Liquid::Template.parse(' {{ product.catchall.unknown }} ').render!('product' => ProductDrop.new) output = Liquid::Template.parse( ' {{ product.catchall.unknown }} ' ).render!('product' => ProductDrop.new)
assert_equal ' catchall_method: unknown ', output assert_equal ' method: unknown ', output
end end
def test_catchall_integer_argument_drop def test_integer_argument_drop
output = Liquid::Template.parse(' {{ product.catchall[8] }} ').render!('product' => ProductDrop.new) output = Liquid::Template.parse( ' {{ product.catchall[8] }} ' ).render!('product' => ProductDrop.new)
assert_equal ' catchall_method: 8 ', output assert_equal ' method: 8 ', output
end end
def test_text_array_drop def test_text_array_drop
output = Liquid::Template.parse('{% for text in product.texts.array %} {{text}} {% endfor %}').render!('product' => ProductDrop.new) output = Liquid::Template.parse( '{% for text in product.texts.array %} {{text}} {% endfor %}' ).render!('product' => ProductDrop.new)
assert_equal ' text1 text2 ', output assert_equal ' text1 text2 ', output
end end
def test_context_drop 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 assert_equal ' carrot ', output
end end
def test_nested_context_drop 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 assert_equal ' monkey ', output
end end
def test_protected def test_protected
output = Liquid::Template.parse(' {{ product.callmenot }} ').render!('product' => ProductDrop.new) output = Liquid::Template.parse( ' {{ product.callmenot }} ' ).render!('product' => ProductDrop.new)
assert_equal ' ', output assert_equal ' ', output
end end
@@ -195,43 +193,43 @@ class DropsTest < Minitest::Test
end end
def test_scope def test_scope
assert_equal '1', Liquid::Template.parse('{{ context.scopes }}').render!('context' => ContextDrop.new) assert_equal '1', Liquid::Template.parse( '{{ context.scopes }}' ).render!('context' => ContextDrop.new)
assert_equal '2', Liquid::Template.parse('{%for i in dummy%}{{ context.scopes }}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1]) assert_equal '2', Liquid::Template.parse( '{%for i in dummy%}{{ context.scopes }}{%endfor%}' ).render!('context' => ContextDrop.new, 'dummy' => [1])
assert_equal '3', Liquid::Template.parse('{%for i in dummy%}{%for i in dummy%}{{ context.scopes }}{%endfor%}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1]) assert_equal '3', Liquid::Template.parse( '{%for i in dummy%}{%for i in dummy%}{{ context.scopes }}{%endfor%}{%endfor%}' ).render!('context' => ContextDrop.new, 'dummy' => [1])
end end
def test_scope_though_proc def test_scope_though_proc
assert_equal '1', Liquid::Template.parse('{{ s }}').render!('context' => ContextDrop.new, 's' => proc{ |c| c['context.scopes'] }) assert_equal '1', Liquid::Template.parse( '{{ s }}' ).render!('context' => ContextDrop.new, 's' => Proc.new{|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 '2', Liquid::Template.parse( '{%for i in dummy%}{{ s }}{%endfor%}' ).render!('context' => ContextDrop.new, 's' => Proc.new{|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 '3', Liquid::Template.parse( '{%for i in dummy%}{%for i in dummy%}{{ s }}{%endfor%}{%endfor%}' ).render!('context' => ContextDrop.new, 's' => Proc.new{|c| c['context.scopes'] }, 'dummy' => [1])
end end
def test_scope_with_assigns def test_scope_with_assigns
assert_equal 'variable', Liquid::Template.parse('{% assign a = "variable"%}{{a}}').render!('context' => ContextDrop.new) assert_equal 'variable', Liquid::Template.parse( '{% assign a = "variable"%}{{a}}' ).render!('context' => ContextDrop.new)
assert_equal 'variable', Liquid::Template.parse('{% assign a = "variable"%}{%for i in dummy%}{{a}}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1]) assert_equal 'variable', Liquid::Template.parse( '{% assign a = "variable"%}{%for i in dummy%}{{a}}{%endfor%}' ).render!('context' => ContextDrop.new, 'dummy' => [1])
assert_equal 'test', Liquid::Template.parse('{% assign header_gif = "test"%}{{header_gif}}').render!('context' => ContextDrop.new) assert_equal 'test', Liquid::Template.parse( '{% assign header_gif = "test"%}{{header_gif}}' ).render!('context' => ContextDrop.new)
assert_equal 'test', Liquid::Template.parse("{% assign header_gif = 'test'%}{{header_gif}}").render!('context' => ContextDrop.new) assert_equal 'test', Liquid::Template.parse( "{% assign header_gif = 'test'%}{{header_gif}}" ).render!('context' => ContextDrop.new)
end end
def test_scope_from_tags def test_scope_from_tags
assert_equal '1', Liquid::Template.parse('{% for i in context.scopes_as_array %}{{i}}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1]) assert_equal '1', Liquid::Template.parse( '{% for i in context.scopes_as_array %}{{i}}{% endfor %}' ).render!('context' => ContextDrop.new, 'dummy' => [1])
assert_equal '12', Liquid::Template.parse('{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1]) assert_equal '12', Liquid::Template.parse( '{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}' ).render!('context' => ContextDrop.new, 'dummy' => [1])
assert_equal '123', Liquid::Template.parse('{%for a in dummy%}{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1]) assert_equal '123', Liquid::Template.parse( '{%for a in dummy%}{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}{% endfor %}' ).render!('context' => ContextDrop.new, 'dummy' => [1])
end end
def test_access_context_from_drop def test_access_context_from_drop
assert_equal '123', Liquid::Template.parse('{%for a in dummy%}{{ context.loop_pos }}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1, 2, 3]) assert_equal '123', Liquid::Template.parse( '{%for a in dummy%}{{ context.loop_pos }}{% endfor %}' ).render!('context' => ContextDrop.new, 'dummy' => [1,2,3])
end end
def test_enumerable_drop def test_enumerable_drop
assert_equal '123', Liquid::Template.parse('{% for c in collection %}{{c}}{% endfor %}').render!('collection' => EnumerableDrop.new) assert_equal '123', Liquid::Template.parse( '{% for c in collection %}{{c}}{% endfor %}').render!('collection' => EnumerableDrop.new)
end end
def test_enumerable_drop_size def test_enumerable_drop_size
assert_equal '3', Liquid::Template.parse('{{collection.size}}').render!('collection' => EnumerableDrop.new) assert_equal '3', Liquid::Template.parse( '{{collection.size}}').render!('collection' => EnumerableDrop.new)
end end
def test_enumerable_drop_will_invoke_liquid_method_missing_for_clashing_method_names def test_enumerable_drop_will_invoke_before_method_for_clashing_method_names
["select", "each", "map", "cycle"].each do |method| ["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' => EnumerableDrop.new) assert_equal method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new)

View File

@@ -1,5 +1,24 @@
require 'test_helper' require 'test_helper'
class ErrorDrop < Liquid::Drop
def standard_error
raise Liquid::StandardError, 'standard error'
end
def argument_error
raise Liquid::ArgumentError, 'argument error'
end
def syntax_error
raise Liquid::SyntaxError, 'syntax error'
end
def exception
raise Exception, 'exception'
end
end
class ErrorHandlingTest < Minitest::Test class ErrorHandlingTest < Minitest::Test
include Liquid include Liquid
@@ -37,7 +56,7 @@ class ErrorHandlingTest < Minitest::Test
end end
def test_standard_error def test_standard_error
template = Liquid::Template.parse(' {{ errors.standard_error }} ') template = Liquid::Template.parse( ' {{ errors.standard_error }} ' )
assert_equal ' Liquid error: standard error ', template.render('errors' => ErrorDrop.new) assert_equal ' Liquid error: standard error ', template.render('errors' => ErrorDrop.new)
assert_equal 1, template.errors.size assert_equal 1, template.errors.size
@@ -45,7 +64,7 @@ class ErrorHandlingTest < Minitest::Test
end end
def test_syntax def test_syntax
template = Liquid::Template.parse(' {{ errors.syntax_error }} ') template = Liquid::Template.parse( ' {{ errors.syntax_error }} ' )
assert_equal ' Liquid syntax error: syntax error ', template.render('errors' => ErrorDrop.new) assert_equal ' Liquid syntax error: syntax error ', template.render('errors' => ErrorDrop.new)
assert_equal 1, template.errors.size assert_equal 1, template.errors.size
@@ -53,7 +72,7 @@ class ErrorHandlingTest < Minitest::Test
end end
def test_argument def test_argument
template = Liquid::Template.parse(' {{ errors.argument_error }} ') template = Liquid::Template.parse( ' {{ errors.argument_error }} ' )
assert_equal ' Liquid error: argument error ', template.render('errors' => ErrorDrop.new) assert_equal ' Liquid error: argument error ', template.render('errors' => ErrorDrop.new)
assert_equal 1, template.errors.size assert_equal 1, template.errors.size
@@ -75,7 +94,7 @@ class ErrorHandlingTest < Minitest::Test
end end
def test_lax_unrecognized_operator def test_lax_unrecognized_operator
template = Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', error_mode: :lax) template = Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', :error_mode => :lax)
assert_equal ' Liquid error: Unknown operator =! ', template.render assert_equal ' Liquid error: Unknown operator =! ', template.render
assert_equal 1, template.errors.size assert_equal 1, template.errors.size
assert_equal Liquid::ArgumentError, template.errors.first.class assert_equal Liquid::ArgumentError, template.errors.first.class
@@ -83,47 +102,31 @@ class ErrorHandlingTest < Minitest::Test
def test_with_line_numbers_adds_numbers_to_parser_errors def test_with_line_numbers_adds_numbers_to_parser_errors
err = assert_raises(SyntaxError) do err = assert_raises(SyntaxError) do
Liquid::Template.parse(%q( template = Liquid::Template.parse(%q{
foobar foobar
{% "cat" | foobar %} {% "cat" | foobar %}
bla bla
), },
line_numbers: true :line_numbers => true
) )
end 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(%q(
foobar
{%- "cat" | foobar -%}
bla
),
line_numbers: true
)
end
assert_match(/Liquid syntax error \(line 4\)/, err.message)
end end
def test_parsing_warn_with_line_numbers_adds_numbers_to_lexer_errors def test_parsing_warn_with_line_numbers_adds_numbers_to_lexer_errors
template = Liquid::Template.parse(' template = Liquid::Template.parse(%q{
foobar foobar
{% if 1 =! 2 %}ok{% endif %} {% if 1 =! 2 %}ok{% endif %}
bla bla
', },
error_mode: :warn, :error_mode => :warn,
line_numbers: true :line_numbers => true
) )
assert_equal ['Liquid syntax error (line 4): Unexpected character = in "1 =! 2"'], assert_equal ['Liquid syntax error (line 4): Unexpected character = in "1 =! 2"'],
template.warnings.map(&:message) template.warnings.map(&:message)
@@ -131,16 +134,16 @@ class ErrorHandlingTest < Minitest::Test
def test_parsing_strict_with_line_numbers_adds_numbers_to_lexer_errors def test_parsing_strict_with_line_numbers_adds_numbers_to_lexer_errors
err = assert_raises(SyntaxError) do err = assert_raises(SyntaxError) do
Liquid::Template.parse(' Liquid::Template.parse(%q{
foobar foobar
{% if 1 =! 2 %}ok{% endif %} {% if 1 =! 2 %}ok{% endif %}
bla bla
', },
error_mode: :strict, :error_mode => :strict,
line_numbers: true :line_numbers => true
) )
end end
assert_equal 'Liquid syntax error (line 4): Unexpected character = in "1 =! 2"', err.message assert_equal 'Liquid syntax error (line 4): Unexpected character = in "1 =! 2"', err.message
@@ -148,7 +151,7 @@ class ErrorHandlingTest < Minitest::Test
def test_syntax_errors_in_nested_blocks_have_correct_line_number def test_syntax_errors_in_nested_blocks_have_correct_line_number
err = assert_raises(SyntaxError) do err = assert_raises(SyntaxError) do
Liquid::Template.parse(' Liquid::Template.parse(%q{
foobar foobar
{% if 1 != 2 %} {% if 1 != 2 %}
@@ -156,9 +159,9 @@ class ErrorHandlingTest < Minitest::Test
{% endif %} {% endif %}
bla bla
', },
line_numbers: true :line_numbers => true
) )
end end
assert_equal "Liquid syntax error (line 5): Unknown tag 'foo'", err.message assert_equal "Liquid syntax error (line 5): Unknown tag 'foo'", err.message
@@ -166,18 +169,18 @@ class ErrorHandlingTest < Minitest::Test
def test_strict_error_messages def test_strict_error_messages
err = assert_raises(SyntaxError) do err = assert_raises(SyntaxError) do
Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', error_mode: :strict) Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', :error_mode => :strict)
end end
assert_equal 'Liquid syntax error: Unexpected character = in "1 =! 2"', err.message assert_equal 'Liquid syntax error: Unexpected character = in "1 =! 2"', err.message
err = assert_raises(SyntaxError) do err = assert_raises(SyntaxError) do
Liquid::Template.parse('{{%%%}}', error_mode: :strict) Liquid::Template.parse('{{%%%}}', :error_mode => :strict)
end end
assert_equal 'Liquid syntax error: Unexpected character % in "{{%%%}}"', err.message assert_equal 'Liquid syntax error: Unexpected character % in "{{%%%}}"', err.message
end end
def test_warnings def test_warnings
template = Liquid::Template.parse('{% if ~~~ %}{{%%%}}{% else %}{{ hello. }}{% endif %}', error_mode: :warn) template = Liquid::Template.parse('{% if ~~~ %}{{%%%}}{% else %}{{ hello. }}{% endif %}', :error_mode => :warn)
assert_equal 3, template.warnings.size assert_equal 3, template.warnings.size
assert_equal 'Unexpected character ~ in "~~~"', template.warnings[0].to_s(false) assert_equal 'Unexpected character ~ in "~~~"', template.warnings[0].to_s(false)
assert_equal 'Unexpected character % in "{{%%%}}"', template.warnings[1].to_s(false) assert_equal 'Unexpected character % in "{{%%%}}"', template.warnings[1].to_s(false)
@@ -186,12 +189,12 @@ class ErrorHandlingTest < Minitest::Test
end end
def test_warning_line_numbers def test_warning_line_numbers
template = Liquid::Template.parse("{% if ~~~ %}\n{{%%%}}{% else %}\n{{ hello. }}{% endif %}", error_mode: :warn, line_numbers: true) template = Liquid::Template.parse("{% if ~~~ %}\n{{%%%}}{% else %}\n{{ hello. }}{% endif %}", :error_mode => :warn, :line_numbers => true)
assert_equal 'Liquid syntax error (line 1): Unexpected character ~ in "~~~"', template.warnings[0].message assert_equal 'Liquid syntax error (line 1): Unexpected character ~ in "~~~"', template.warnings[0].message
assert_equal 'Liquid syntax error (line 2): Unexpected character % in "{{%%%}}"', template.warnings[1].message assert_equal 'Liquid syntax error (line 2): Unexpected character % in "{{%%%}}"', template.warnings[1].message
assert_equal 'Liquid syntax error (line 3): Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].message assert_equal 'Liquid syntax error (line 3): Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].message
assert_equal 3, template.warnings.size assert_equal 3, template.warnings.size
assert_equal [1, 2, 3], template.warnings.map(&:line_number) assert_equal [1,2,3], template.warnings.map(&:line_number)
end end
# Liquid should not catch Exceptions that are not subclasses of StandardError, like Interrupt and NoMemoryError # Liquid should not catch Exceptions that are not subclasses of StandardError, like Interrupt and NoMemoryError
@@ -201,60 +204,4 @@ class ErrorHandlingTest < Minitest::Test
template.render('errors' => ErrorDrop.new) template.render('errors' => ErrorDrop.new)
end end
end end
def test_default_exception_renderer_with_internal_error
template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true)
output = template.render({ 'errors' => ErrorDrop.new })
assert_equal 'This is a runtime error: Liquid error (line 1): internal', output
assert_equal [Liquid::InternalError], template.errors.map(&:class)
end
def test_setting_default_exception_renderer
old_exception_renderer = Liquid::Template.default_exception_renderer
exceptions = []
Liquid::Template.default_exception_renderer = ->(e) { exceptions << e; '' }
template = Liquid::Template.parse('This is a runtime error: {{ errors.argument_error }}')
output = template.render({ 'errors' => ErrorDrop.new })
assert_equal 'This is a runtime error: ', output
assert_equal [Liquid::ArgumentError], template.errors.map(&:class)
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 = ->(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 }}"
end
end
def test_included_template_name_with_line_numbers
old_file_system = Liquid::Template.file_system
begin
Liquid::Template.file_system = TestFileSystem.new
template = Liquid::Template.parse("Argument error:\n{% include 'product' %}", line_numbers: true)
page = template.render('errors' => ErrorDrop.new)
ensure
Liquid::Template.file_system = old_file_system
end
assert_equal "Argument error:\nLiquid error (product line 1): argument error", page
assert_equal "product", template.errors.first.template_name
end
end end

View File

@@ -17,7 +17,7 @@ module CanadianMoneyFilter
end end
module SubstituteFilter module SubstituteFilter
def substitute(input, params = {}) def substitute(input, params={})
input.gsub(/%\{(\w+)\}/) { |match| params[$1] } input.gsub(/%\{(\w+)\}/) { |match| params[$1] }
end end
end end
@@ -25,12 +25,6 @@ end
class FiltersTest < Minitest::Test class FiltersTest < Minitest::Test
include Liquid include Liquid
module OverrideObjectMethodFilter
def tap(input)
"tap overridden"
end
end
def setup def setup
@context = Context.new @context = Context.new
end end
@@ -39,13 +33,13 @@ class FiltersTest < Minitest::Test
@context['var'] = 1000 @context['var'] = 1000
@context.add_filters(MoneyFilter) @context.add_filters(MoneyFilter)
assert_equal ' 1000$ ', Template.parse("{{var | money}}").render(@context) assert_equal ' 1000$ ', Variable.new("var | money").render(@context)
end end
def test_underscore_in_filter_name def test_underscore_in_filter_name
@context['var'] = 1000 @context['var'] = 1000
@context.add_filters(MoneyFilter) @context.add_filters(MoneyFilter)
assert_equal ' 1000$ ', Template.parse("{{var | money_with_underscore}}").render(@context) assert_equal ' 1000$ ', Variable.new("var | money_with_underscore").render(@context)
end end
def test_second_filter_overwrites_first def test_second_filter_overwrites_first
@@ -53,104 +47,64 @@ class FiltersTest < Minitest::Test
@context.add_filters(MoneyFilter) @context.add_filters(MoneyFilter)
@context.add_filters(CanadianMoneyFilter) @context.add_filters(CanadianMoneyFilter)
assert_equal ' 1000$ CAD ', Template.parse("{{var | money}}").render(@context) assert_equal ' 1000$ CAD ', Variable.new("var | money").render(@context)
end end
def test_size def test_size
@context['var'] = 'abcd' @context['var'] = 'abcd'
@context.add_filters(MoneyFilter) @context.add_filters(MoneyFilter)
assert_equal '4', Template.parse("{{var | size}}").render(@context) assert_equal 4, Variable.new("var | size").render(@context)
end end
def test_join def test_join
@context['var'] = [1, 2, 3, 4] @context['var'] = [1,2,3,4]
assert_equal "1 2 3 4", Template.parse("{{var | join}}").render(@context) assert_equal "1 2 3 4", Variable.new("var | join").render(@context)
end end
def test_sort def test_sort
@context['value'] = 3 @context['value'] = 3
@context['numbers'] = [2, 1, 4, 3] @context['numbers'] = [2,1,4,3]
@context['words'] = ['expected', 'as', 'alphabetic'] @context['words'] = ['expected', 'as', 'alphabetic']
@context['arrays'] = ['flower', 'are'] @context['arrays'] = ['flower', 'are']
@context['case_sensitive'] = ['sensitive', 'Expected', 'case']
assert_equal '1 2 3 4', Template.parse("{{numbers | sort | join}}").render(@context) assert_equal [1,2,3,4], Variable.new("numbers | sort").render(@context)
assert_equal 'alphabetic as expected', Template.parse("{{words | sort | join}}").render(@context) assert_equal ['alphabetic', 'as', 'expected'], Variable.new("words | sort").render(@context)
assert_equal '3', Template.parse("{{value | sort}}").render(@context) assert_equal [3], Variable.new("value | sort").render(@context)
assert_equal 'are flower', Template.parse("{{arrays | sort | join}}").render(@context) assert_equal ['are', 'flower'], Variable.new("arrays | sort").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['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)
# Test hashes
assert_equal 'A b C', Template.parse("{{hashes | sort_natural: 'a' | map: 'a' | join}}").render(@context)
# Test objects
assert_equal 'A b C', Template.parse("{{objects | sort_natural: 'a' | map: 'a' | join}}").render(@context)
end
def test_compact
@context['words'] = ['a', nil, 'b', nil, 'c']
@context['hashes'] = [{ 'a' => 'A' }, { 'a' => nil }, { 'a' => 'C' }]
@context['objects'] = [TestObject.new('A'), TestObject.new(nil), TestObject.new('C')]
# Test strings
assert_equal 'a b c', Template.parse("{{words | compact | join}}").render(@context)
# Test hashes
assert_equal 'A C', Template.parse("{{hashes | compact: 'a' | map: 'a' | join}}").render(@context)
# Test objects
assert_equal 'A C', Template.parse("{{objects | compact: 'a' | map: 'a' | join}}").render(@context)
end end
def test_strip_html 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", Variable.new("var | strip_html").render(@context)
end end
def test_strip_html_ignore_comments_with_html 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", Variable.new("var | strip_html").render(@context)
end end
def test_capitalize def test_capitalize
@context['var'] = "blub" @context['var'] = "blub"
assert_equal "Blub", Template.parse("{{ var | capitalize }}").render(@context) assert_equal "Blub", Variable.new("var | capitalize").render(@context)
end end
def test_nonexistent_filter_is_ignored def test_nonexistent_filter_is_ignored
@context['var'] = 1000 @context['var'] = 1000
assert_equal '1000', Template.parse("{{ var | xyzzy }}").render(@context) assert_equal 1000, Variable.new("var | xyzzy").render(@context)
end end
def test_filter_with_keyword_arguments def test_filter_with_keyword_arguments
@context['surname'] = 'john' @context['surname'] = 'john'
@context['input'] = 'hello %{first_name}, %{last_name}'
@context.add_filters(SubstituteFilter) @context.add_filters(SubstituteFilter)
output = Template.parse(%({{ input | substitute: first_name: surname, last_name: 'doe' }})).render(@context) output = Variable.new(%! 'hello %{first_name}, %{last_name}' | substitute: first_name: surname, last_name: 'doe' !).render(@context)
assert_equal 'hello john, doe', output assert_equal 'hello john, doe', output
end end
def test_override_object_method_in_filter
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 })
end
end end
class FiltersInTemplate < Minitest::Test class FiltersInTemplate < Minitest::Test
@@ -159,8 +113,8 @@ class FiltersInTemplate < Minitest::Test
def test_local_global def test_local_global
with_global_filter(MoneyFilter) do with_global_filter(MoneyFilter) do
assert_equal " 1000$ ", Template.parse("{{1000 | money}}").render!(nil, nil) 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$ CAD ", Template.parse("{{1000 | money}}").render!(nil, filters: [CanadianMoneyFilter]) assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, :filters => [CanadianMoneyFilter])
end end
end end
@@ -169,10 +123,3 @@ class FiltersInTemplate < Minitest::Test
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, [CanadianMoneyFilter]) assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, [CanadianMoneyFilter])
end end
end # FiltersTest end # FiltersTest
class TestObject < Liquid::Drop
attr_accessor :a
def initialize(a)
@a = a
end
end

View File

@@ -1,18 +1,18 @@
require 'test_helper' 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 class HashOrderingTest < Minitest::Test
module MoneyFilter
def money(input)
sprintf(' %d$ ', input)
end
end
module CanadianMoneyFilter
def money(input)
sprintf(' %d$ CAD ', input)
end
end
include Liquid include Liquid
def test_global_register_order def test_global_register_order

View File

@@ -14,7 +14,7 @@ module FunnyFilter
end end
def add_tag(input, tag = "p", id = "foo") def add_tag(input, tag = "p", id = "foo")
%(<#{tag} id="#{id}">#{input}</#{tag}>) %|<#{tag} id="#{id}">#{input}</#{tag}>|
end end
def paragraph(input) def paragraph(input)
@@ -22,8 +22,9 @@ module FunnyFilter
end end
def link_to(name, url) def link_to(name, url)
%(<a href="#{url}">#{name}</a>) %|<a href="#{url}">#{name}</a>|
end end
end end
class OutputTest < Minitest::Test class OutputTest < Minitest::Test
@@ -32,92 +33,84 @@ class OutputTest < Minitest::Test
def setup def setup
@assigns = { @assigns = {
'best_cars' => 'bmw', 'best_cars' => 'bmw',
'car' => { 'bmw' => 'good', 'gm' => 'bad' } 'car' => {'bmw' => 'good', 'gm' => 'bad'}
} }
end end
def test_variable def test_variable
text = %( {{best_cars}} ) text = %| {{best_cars}} |
expected = %( bmw ) expected = %| bmw |
assert_equal expected, Template.parse(text).render!(@assigns) assert_equal expected, Template.parse(text).render!(@assigns)
end end
def test_variable_traversing_with_two_brackets
text = %({{ site.data.menu[include.menu][include.locale] }})
assert_equal "it works!", Template.parse(text).render!(
"site" => { "data" => { "menu" => { "foo" => { "bar" => "it works!" } } } },
"include" => { "menu" => "foo", "locale" => "bar" }
)
end
def test_variable_traversing def test_variable_traversing
text = %( {{car.bmw}} {{car.gm}} {{car.bmw}} ) text = %| {{car.bmw}} {{car.gm}} {{car.bmw}} |
expected = %( good bad good ) expected = %| good bad good |
assert_equal expected, Template.parse(text).render!(@assigns) assert_equal expected, Template.parse(text).render!(@assigns)
end end
def test_variable_piping def test_variable_piping
text = %( {{ car.gm | make_funny }} ) text = %( {{ car.gm | make_funny }} )
expected = %( LOL ) expected = %| LOL |
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]) assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter])
end end
def test_variable_piping_with_input def test_variable_piping_with_input
text = %( {{ car.gm | cite_funny }} ) text = %( {{ car.gm | cite_funny }} )
expected = %( LOL: bad ) expected = %| LOL: bad |
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]) assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter])
end end
def test_variable_piping_with_args def test_variable_piping_with_args
text = %! {{ car.gm | add_smiley : ':-(' }} ! text = %! {{ car.gm | add_smiley : ':-(' }} !
expected = %| bad :-( | expected = %| bad :-( |
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]) assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter])
end end
def test_variable_piping_with_no_args def test_variable_piping_with_no_args
text = %( {{ car.gm | add_smiley }} ) text = %! {{ car.gm | add_smiley }} !
expected = %| bad :-) | expected = %| bad :-) |
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]) assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter])
end end
def test_multiple_variable_piping_with_args def test_multiple_variable_piping_with_args
text = %! {{ car.gm | add_smiley : ':-(' | add_smiley : ':-('}} ! text = %! {{ car.gm | add_smiley : ':-(' | add_smiley : ':-('}} !
expected = %| bad :-( :-( | expected = %| bad :-( :-( |
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]) assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter])
end end
def test_variable_piping_with_multiple_args def test_variable_piping_with_multiple_args
text = %( {{ car.gm | add_tag : 'span', 'bar'}} ) text = %! {{ car.gm | add_tag : 'span', 'bar'}} !
expected = %( <span id="bar">bad</span> ) expected = %| <span id="bar">bad</span> |
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]) assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter])
end end
def test_variable_piping_with_variable_args def test_variable_piping_with_variable_args
text = %( {{ car.gm | add_tag : 'span', car.bmw}} ) text = %! {{ car.gm | add_tag : 'span', car.bmw}} !
expected = %( <span id="good">bad</span> ) expected = %| <span id="good">bad</span> |
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]) assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter])
end end
def test_multiple_pipings def test_multiple_pipings
text = %( {{ best_cars | cite_funny | paragraph }} ) text = %( {{ best_cars | cite_funny | paragraph }} )
expected = %( <p>LOL: bmw</p> ) expected = %| <p>LOL: bmw</p> |
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]) assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter])
end end
def test_link_to def test_link_to
text = %( {{ 'Typo' | link_to: 'http://typo.leetsoft.com' }} ) text = %( {{ 'Typo' | link_to: 'http://typo.leetsoft.com' }} )
expected = %( <a href="http://typo.leetsoft.com">Typo</a> ) expected = %| <a href="http://typo.leetsoft.com">Typo</a> |
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]) assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter])
end end
end # OutputTest end # OutputTest

View File

@@ -28,14 +28,11 @@ class ParsingQuirksTest < Minitest::Test
def test_error_on_empty_filter def test_error_on_empty_filter
assert Template.parse("{{test}}") assert Template.parse("{{test}}")
assert Template.parse("{{|test}}")
with_error_mode(:lax) do
assert Template.parse("{{|test}}")
end
with_error_mode(:strict) do with_error_mode(:strict) do
assert_raises(SyntaxError) { Template.parse("{{|test}}") } assert_raises(SyntaxError) do
assert_raises(SyntaxError) { Template.parse("{{test |a|b|}}") } Template.parse("{{test |a|b|}}")
end
end end
end end
@@ -62,25 +59,25 @@ class ParsingQuirksTest < Minitest::Test
end end
def test_no_error_on_lax_empty_filter def test_no_error_on_lax_empty_filter
assert Template.parse("{{test |a|b|}}", 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)
assert Template.parse("{{|test|}}", error_mode: :lax) assert Template.parse("{{|test|}}", :error_mode => :lax)
end end
def test_meaningless_parens_lax def test_meaningless_parens_lax
with_error_mode(:lax) do with_error_mode(:lax) do
assigns = { 'b' => 'bar', 'c' => 'baz' } assigns = {'b' => 'bar', 'c' => 'baz'}
markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false" markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false"
assert_template_result(' YES ', "{% if #{markup} %} YES {% endif %}", assigns) assert_template_result(' YES ',"{% if #{markup} %} YES {% endif %}", assigns)
end end
end end
def test_unexpected_characters_silently_eat_logic_lax def test_unexpected_characters_silently_eat_logic_lax
with_error_mode(:lax) do with_error_mode(:lax) do
markup = "true && false" markup = "true && false"
assert_template_result(' YES ', "{% if #{markup} %} YES {% endif %}") assert_template_result(' YES ',"{% if #{markup} %} YES {% endif %}")
markup = "false || true" markup = "false || true"
assert_template_result('', "{% if #{markup} %} YES {% endif %}") assert_template_result('',"{% if #{markup} %} YES {% endif %}")
end end
end end
@@ -92,7 +89,7 @@ class ParsingQuirksTest < Minitest::Test
def test_unanchored_filter_arguments def test_unanchored_filter_arguments
with_error_mode(:lax) do with_error_mode(:lax) do
assert_template_result('hi', "{{ 'hi there' | split$$$:' ' | first }}") assert_template_result('hi',"{{ 'hi there' | split$$$:' ' | first }}")
assert_template_result('x', "{{ 'X' | downcase) }}") assert_template_result('x', "{{ 'X' | downcase) }}")
@@ -116,7 +113,4 @@ class ParsingQuirksTest < Minitest::Test
end end
end end
def test_contains_in_id
assert_template_result(' YES ', '{% if containsallshipments == true %} YES {% endif %}', 'containsallshipments' => true)
end
end # ParsingQuirksTest end # ParsingQuirksTest

View File

@@ -4,7 +4,7 @@ class RenderProfilingTest < Minitest::Test
include Liquid include Liquid
class ProfilingFileSystem class ProfilingFileSystem
def read_template_file(template_path) def read_template_file(template_path, context)
"Rendering template {% assign template_name = '#{template_path}'%}\n{{ template_name }}" "Rendering template {% assign template_name = '#{template_path}'%}\n{{ template_name }}"
end end
end end
@@ -21,7 +21,7 @@ class RenderProfilingTest < Minitest::Test
end end
def test_parse_makes_available_simple_profiling def test_parse_makes_available_simple_profiling
t = Template.parse("{{ 'a string' | upcase }}", profile: true) t = Template.parse("{{ 'a string' | upcase }}", :profile => true)
t.render! t.render!
assert_equal 1, t.profiler.length assert_equal 1, t.profiler.length
@@ -31,14 +31,14 @@ class RenderProfilingTest < Minitest::Test
end end
def test_render_ignores_raw_strings_when_profiling def test_render_ignores_raw_strings_when_profiling
t = Template.parse("This is raw string\nstuff\nNewline", profile: true) t = Template.parse("This is raw string\nstuff\nNewline", :profile => true)
t.render! t.render!
assert_equal 0, t.profiler.length assert_equal 0, t.profiler.length
end end
def test_profiling_includes_line_numbers_of_liquid_nodes def test_profiling_includes_line_numbers_of_liquid_nodes
t = Template.parse("{{ 'a string' | upcase }}\n{% increment test %}", profile: true) t = Template.parse("{{ 'a string' | upcase }}\n{% increment test %}", :profile => true)
t.render! t.render!
assert_equal 2, t.profiler.length assert_equal 2, t.profiler.length
@@ -49,7 +49,7 @@ class RenderProfilingTest < Minitest::Test
end end
def test_profiling_includes_line_numbers_of_included_partials def test_profiling_includes_line_numbers_of_included_partials
t = Template.parse("{% include 'a_template' %}", profile: true) t = Template.parse("{% include 'a_template' %}", :profile => true)
t.render! t.render!
included_children = t.profiler[0].children included_children = t.profiler[0].children
@@ -61,7 +61,7 @@ class RenderProfilingTest < Minitest::Test
end end
def test_profiling_times_the_rendering_of_tokens def test_profiling_times_the_rendering_of_tokens
t = Template.parse("{% include 'a_template' %}", profile: true) t = Template.parse("{% include 'a_template' %}", :profile => true)
t.render! t.render!
node = t.profiler[0] node = t.profiler[0]
@@ -69,14 +69,14 @@ class RenderProfilingTest < Minitest::Test
end end
def test_profiling_times_the_entire_render def test_profiling_times_the_entire_render
t = Template.parse("{% include 'a_template' %}", profile: true) t = Template.parse("{% include 'a_template' %}", :profile => true)
t.render! 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 end
def test_profiling_uses_include_to_mark_children def test_profiling_uses_include_to_mark_children
t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}", profile: true) t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}", :profile => true)
t.render! t.render!
include_node = t.profiler[1] include_node = t.profiler[1]
@@ -84,47 +84,47 @@ class RenderProfilingTest < Minitest::Test
end end
def test_profiling_marks_children_with_the_name_of_included_partial def test_profiling_marks_children_with_the_name_of_included_partial
t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}", profile: true) t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}", :profile => true)
t.render! t.render!
include_node = t.profiler[1] include_node = t.profiler[1]
include_node.children.each do |child| include_node.children.each do |child|
assert_equal "a_template", child.partial assert_equal "'a_template'", child.partial
end end
end end
def test_profiling_supports_multiple_templates def test_profiling_supports_multiple_templates
t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}\n{% include 'b_template' %}", profile: true) t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}\n{% include 'b_template' %}", :profile => true)
t.render! t.render!
a_template = t.profiler[1] a_template = t.profiler[1]
a_template.children.each do |child| a_template.children.each do |child|
assert_equal "a_template", child.partial assert_equal "'a_template'", child.partial
end end
b_template = t.profiler[2] b_template = t.profiler[2]
b_template.children.each do |child| b_template.children.each do |child|
assert_equal "b_template", child.partial assert_equal "'b_template'", child.partial
end end
end end
def test_profiling_supports_rendering_the_same_partial_multiple_times def test_profiling_supports_rendering_the_same_partial_multiple_times
t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}\n{% include 'a_template' %}", profile: true) t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}\n{% include 'a_template' %}", :profile => true)
t.render! t.render!
a_template1 = t.profiler[1] a_template1 = t.profiler[1]
a_template1.children.each do |child| a_template1.children.each do |child|
assert_equal "a_template", child.partial assert_equal "'a_template'", child.partial
end end
a_template2 = t.profiler[2] a_template2 = t.profiler[2]
a_template2.children.each do |child| a_template2.children.each do |child|
assert_equal "a_template", child.partial assert_equal "'a_template'", child.partial
end end
end end
def test_can_iterate_over_each_profiling_entry def test_can_iterate_over_each_profiling_entry
t = Template.parse("{{ 'a string' | upcase }}\n{% increment test %}", profile: true) t = Template.parse("{{ 'a string' | upcase }}\n{% increment test %}", :profile => true)
t.render! t.render!
timing_count = 0 timing_count = 0
@@ -136,7 +136,7 @@ class RenderProfilingTest < Minitest::Test
end end
def test_profiling_marks_children_of_if_blocks 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! t.render!
assert_equal 1, t.profiler.length assert_equal 1, t.profiler.length
@@ -144,8 +144,8 @@ class RenderProfilingTest < Minitest::Test
end end
def test_profiling_marks_children_of_for_blocks def test_profiling_marks_children_of_for_blocks
t = Template.parse("{% for item in collection %} {{ item }} {% endfor %}", profile: true) t = Template.parse("{% for item in collection %} {{ item }} {% endfor %}", :profile => true)
t.render!({ "collection" => ["one", "two"] }) t.render!({"collection" => ["one", "two"]})
assert_equal 1, t.profiler.length assert_equal 1, t.profiler.length
# Will profile each invocation of the for block # Will profile each invocation of the for block

View File

@@ -9,36 +9,34 @@ end
class SecurityTest < Minitest::Test class SecurityTest < Minitest::Test
include Liquid include Liquid
def setup
@assigns = {}
end
def test_no_instance_eval def test_no_instance_eval
text = %( {{ '1+1' | instance_eval }} ) text = %( {{ '1+1' | instance_eval }} )
expected = %( 1+1 ) expected = %| 1+1 |
assert_equal expected, Template.parse(text).render!(@assigns) assert_equal expected, Template.parse(text).render!(@assigns)
end end
def test_no_existing_instance_eval def test_no_existing_instance_eval
text = %( {{ '1+1' | __instance_eval__ }} ) text = %( {{ '1+1' | __instance_eval__ }} )
expected = %( 1+1 ) expected = %| 1+1 |
assert_equal expected, Template.parse(text).render!(@assigns) assert_equal expected, Template.parse(text).render!(@assigns)
end end
def test_no_instance_eval_after_mixing_in_new_filter def test_no_instance_eval_after_mixing_in_new_filter
text = %( {{ '1+1' | instance_eval }} ) text = %( {{ '1+1' | instance_eval }} )
expected = %( 1+1 ) expected = %| 1+1 |
assert_equal expected, Template.parse(text).render!(@assigns) assert_equal expected, Template.parse(text).render!(@assigns)
end end
def test_no_instance_eval_later_in_chain def test_no_instance_eval_later_in_chain
text = %( {{ '1+1' | add_one | instance_eval }} ) text = %( {{ '1+1' | add_one | instance_eval }} )
expected = %( 1+1 + 1 ) expected = %| 1+1 + 1 |
assert_equal expected, Template.parse(text).render!(@assigns, filters: SecurityFilter) assert_equal expected, Template.parse(text).render!(@assigns, :filters => SecurityFilter)
end end
def test_does_not_add_filters_to_symbol_table def test_does_not_add_filters_to_symbol_table
@@ -63,18 +61,4 @@ class SecurityTest < Minitest::Test
assert_equal [], (Symbol.all_symbols - current_symbols) assert_equal [], (Symbol.all_symbols - current_symbols)
end 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 end # SecurityTest

View File

@@ -41,16 +41,6 @@ class TestEnumerable < Liquid::Drop
end end
end end
class NumberLikeThing < Liquid::Drop
def initialize(amount)
@amount = amount
end
def to_number
@amount
end
end
class StandardFiltersTest < Minitest::Test class StandardFiltersTest < Minitest::Test
include Liquid include Liquid
@@ -59,7 +49,7 @@ class StandardFiltersTest < Minitest::Test
end end
def test_size def test_size
assert_equal 3, @filters.size([1, 2, 3]) assert_equal 3, @filters.size([1,2,3])
assert_equal 0, @filters.size([]) assert_equal 0, @filters.size([])
assert_equal 0, @filters.size(nil) assert_equal 0, @filters.size(nil)
end end
@@ -86,27 +76,20 @@ class StandardFiltersTest < Minitest::Test
assert_equal '', @filters.slice(nil, 0) assert_equal '', @filters.slice(nil, 0)
assert_equal '', @filters.slice('foobar', 100, 10) assert_equal '', @filters.slice('foobar', 100, 10)
assert_equal '', @filters.slice('foobar', -100, 10) assert_equal '', @filters.slice('foobar', -100, 10)
assert_equal 'oob', @filters.slice('foobar', '1', '3')
assert_raises(Liquid::ArgumentError) do
@filters.slice('foobar', nil)
end
assert_raises(Liquid::ArgumentError) do
@filters.slice('foobar', 0, "")
end
end end
def test_slice_on_arrays def test_slice_on_arrays
input = 'foobar'.split(//) input = 'foobar'.split(//)
assert_equal %w(o o b), @filters.slice(input, 1, 3) 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{o o b a r}, @filters.slice(input, 1, 1000)
assert_equal %w(), @filters.slice(input, 1, 0) assert_equal %w{}, @filters.slice(input, 1, 0)
assert_equal %w(o), @filters.slice(input, 1, 1) assert_equal %w{o}, @filters.slice(input, 1, 1)
assert_equal %w(b a r), @filters.slice(input, 3, 3) 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, 2)
assert_equal %w(a r), @filters.slice(input, -2, 1000) assert_equal %w{a r}, @filters.slice(input, -2, 1000)
assert_equal %w(r), @filters.slice(input, -1) 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(), @filters.slice(input, -100, 10) assert_equal %w{}, @filters.slice(input, -100, 10)
end end
def test_truncate def test_truncate
@@ -115,29 +98,20 @@ class StandardFiltersTest < Minitest::Test
assert_equal '...', @filters.truncate('1234567890', 0) assert_equal '...', @filters.truncate('1234567890', 0)
assert_equal '1234567890', @filters.truncate('1234567890') 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 end
def test_split def test_split
assert_equal ['12', '34'], @filters.split('12~34', '~') assert_equal ['12','34'], @filters.split('12~34', '~')
assert_equal ['A? ', ' ,Z'], @filters.split('A? ~ ~ ~ ,Z', '~ ~ ~') assert_equal ['A? ',' ,Z'], @filters.split('A? ~ ~ ~ ,Z', '~ ~ ~')
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 [], @filters.split(nil, ' ')
assert_equal ['A', 'Z'], @filters.split('A1Z', 1)
end end
def test_escape def test_escape
assert_equal '&lt;strong&gt;', @filters.escape('<strong>') assert_equal '&lt;strong&gt;', @filters.escape('<strong>')
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 '&lt;strong&gt;', @filters.h('<strong>') assert_equal '&lt;strong&gt;', @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 end
def test_escape_once def test_escape_once
@@ -146,18 +120,7 @@ class StandardFiltersTest < Minitest::Test
def test_url_encode def test_url_encode
assert_equal 'foo%2B1%40example.com', @filters.url_encode('foo+1@example.com') assert_equal 'foo%2B1%40example.com', @filters.url_encode('foo+1@example.com')
assert_equal '1', @filters.url_encode(1) assert_equal nil, @filters.url_encode(nil)
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 '1', @filters.url_decode(1)
assert_equal '2001-02-03', @filters.url_decode(Date.new(2001, 2, 3))
assert_nil @filters.url_decode(nil)
end end
def test_truncatewords def test_truncatewords
@@ -166,7 +129,6 @@ class StandardFiltersTest < Minitest::Test
assert_equal 'one two three', @filters.truncatewords('one two three') assert_equal 'one two three', @filters.truncatewords('one two three')
assert_equal 'Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221;...', @filters.truncatewords('Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221; x 16&#8221; x 10.5&#8221; high) with cover.', 15) assert_equal 'Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221;...', @filters.truncatewords('Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221; x 16&#8221; x 10.5&#8221; 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 end
def test_strip_html def test_strip_html
@@ -180,81 +142,45 @@ class StandardFiltersTest < Minitest::Test
end end
def test_join 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 '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 end
def test_sort def test_sort
assert_equal [1, 2, 3, 4], @filters.sort([4, 3, 2, 1]) 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_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_empty_array
assert_equal [], @filters.sort([], "a")
end
def test_sort_natural_empty_array
assert_equal [], @filters.sort_natural([], "a")
end end
def test_legacy_sort_hash 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 end
def test_numerical_vs_lexicographical_sort def test_numerical_vs_lexicographical_sort
assert_equal [2, 10], @filters.sort([10, 2]) assert_equal [2, 10], @filters.sort([10, 2])
assert_equal [{ "a" => 2 }, { "a" => 10 }], @filters.sort([{ "a" => 10 }, { "a" => 2 }], "a") assert_equal [{"a" => 2}, {"a" => 10}], @filters.sort([{"a" => 10}, {"a" => 2}], "a")
assert_equal ["10", "2"], @filters.sort(["10", "2"]) assert_equal ["10", "2"], @filters.sort(["10", "2"])
assert_equal [{ "a" => "10" }, { "a" => "2" }], @filters.sort([{ "a" => "10" }, { "a" => "2" }], "a") assert_equal [{"a" => "10"}, {"a" => "2"}], @filters.sort([{"a" => "10"}, {"a" => "2"}], "a")
end end
def test_uniq def test_uniq
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 [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 testdrop = TestDrop.new
assert_equal [testdrop], @filters.uniq([testdrop, TestDrop.new], 'test') assert_equal [testdrop], @filters.uniq([testdrop, TestDrop.new], 'test')
end end
def test_uniq_empty_array
assert_equal [], @filters.uniq([], "a")
end
def test_compact_empty_array
assert_equal [], @filters.compact([], "a")
end
def test_reverse def test_reverse
assert_equal [4, 3, 2, 1], @filters.reverse([1, 2, 3, 4]) assert_equal [4,3,2,1], @filters.reverse([1,2,3,4])
end end
def test_legacy_reverse_hash def test_legacy_reverse_hash
assert_equal [{ a: 1, b: 2 }], @filters.reverse(a: 1, b: 2) assert_equal [{a:1, b:2}], @filters.reverse(a:1, b:2)
end end
def test_map 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' }}", 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 end
def test_map_doesnt_call_arbitrary_stuff def test_map_doesnt_call_arbitrary_stuff
@@ -286,24 +212,11 @@ class StandardFiltersTest < Minitest::Test
def test_map_over_proc def test_map_over_proc
drop = TestDrop.new drop = TestDrop.new
p = proc{ drop } p = Proc.new{ drop }
templ = '{{ procs | map: "test" }}' templ = '{{ procs | map: "test" }}'
assert_template_result "testfoo", templ, "procs" => [p] assert_template_result "testfoo", templ, "procs" => [p]
end end
def test_map_over_drops_returning_procs
drops = [
{
"proc" => ->{ "foo" },
},
{
"proc" => ->{ "bar" },
},
]
templ = '{{ drops | map: "proc" }}'
assert_template_result "foobar", templ, "drops" => drops
end
def test_map_works_on_enumerables 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 end
@@ -317,10 +230,6 @@ class StandardFiltersTest < Minitest::Test
assert_template_result 'foobar', '{{ foo | last }}', 'foo' => [ThingWithToLiquid.new] assert_template_result 'foobar', '{{ foo | last }}', 'foo' => [ThingWithToLiquid.new]
end end
def test_truncate_calls_to_liquid
assert_template_result "wo...", '{{ foo | truncate: 5 }}', "foo" => TestThing.new
end
def test_date def test_date
assert_equal 'May', @filters.date(Time.parse("2006-05-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 'June', @filters.date(Time.parse("2006-06-05 10:00:00"), "%B")
@@ -340,38 +249,29 @@ class StandardFiltersTest < Minitest::Test
assert_equal "07/16/2004", @filters.date("Fri Jul 16 01:00:00 2004", "%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('now', '%Y')
assert_equal "#{Date.today.year}", @filters.date('today', '%Y') assert_equal "#{Date.today.year}", @filters.date('today', '%Y')
assert_equal "#{Date.today.year}", @filters.date('Today', '%Y')
assert_nil @filters.date(nil, "%B") assert_equal nil, @filters.date(nil, "%B")
assert_equal '', @filters.date('', "%B") 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(1152098955, "%m/%d/%Y")
assert_equal "07/05/2006", @filters.date("1152098955", "%m/%d/%Y")
end
end end
def test_first_last def test_first_last
assert_equal 1, @filters.first([1, 2, 3]) assert_equal 1, @filters.first([1,2,3])
assert_equal 3, @filters.last([1, 2, 3]) assert_equal 3, @filters.last([1,2,3])
assert_nil @filters.first([]) assert_equal nil, @filters.first([])
assert_nil @filters.last([]) assert_equal nil, @filters.last([])
end end
def test_replace def test_replace
assert_equal '2 2 2 2', @filters.replace('1 1 1 1', '1', 2) assert_equal '2 2 2 2', @filters.replace('1 1 1 1', '1', 2)
assert_equal '2 2 2 2', @filters.replace('1 1 1 1', 1, 2)
assert_equal '2 1 1 1', @filters.replace_first('1 1 1 1', '1', 2) assert_equal '2 1 1 1', @filters.replace_first('1 1 1 1', '1', 2)
assert_equal '2 1 1 1', @filters.replace_first('1 1 1 1', 1, 2)
assert_template_result '2 1 1 1', "{{ '1 1 1 1' | replace_first: '1', 2 }}" assert_template_result '2 1 1 1', "{{ '1 1 1 1' | replace_first: '1', 2 }}"
end end
def test_remove def test_remove
assert_equal ' ', @filters.remove("a a a a", 'a') 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 'a a a', @filters.remove_first("a a a a", 'a ')
assert_equal ' 1 1 1', @filters.remove_first("1 1 1 1", 1)
assert_template_result 'a a a', "{{ 'a a a a' | remove_first: 'a ' }}" assert_template_result 'a a a', "{{ 'a a a a' | remove_first: 'a ' }}"
end end
@@ -406,38 +306,20 @@ class StandardFiltersTest < Minitest::Test
def test_plus def test_plus
assert_template_result "2", "{{ 1 | plus:1 }}" assert_template_result "2", "{{ 1 | plus:1 }}"
assert_template_result "2.0", "{{ '1' | plus:'1.0' }}" assert_template_result "2.0", "{{ '1' | plus:'1.0' }}"
assert_template_result "5", "{{ price | plus:'2' }}", 'price' => NumberLikeThing.new(3)
end end
def test_minus def test_minus
assert_template_result "4", "{{ input | minus:operand }}", 'input' => 5, 'operand' => 1 assert_template_result "4", "{{ input | minus:operand }}", 'input' => 5, 'operand' => 1
assert_template_result "2.3", "{{ '4.3' | minus:'2' }}" 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 end
def test_times def test_times
assert_template_result "12", "{{ 3 | times:4 }}" assert_template_result "12", "{{ 3 | times:4 }}"
assert_template_result "0", "{{ 'foo' | times:4 }}" assert_template_result "0", "{{ 'foo' | times:4 }}"
assert_template_result "6", "{{ '2.1' | times:3 | replace: '.','-' | plus:0}}" 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 "7.25", '{{ "-0.0725" | times: -100 }}'
assert_template_result "4", "{{ price | times:2 }}", 'price' => NumberLikeThing.new(2)
end end
def test_divided_by def test_divided_by
@@ -448,74 +330,38 @@ class StandardFiltersTest < Minitest::Test
assert_equal "Liquid error: divided by 0", Template.parse("{{ 5 | divided_by:0 }}").render 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 }}"
end
assert_template_result "5", "{{ price | divided_by:2 }}", 'price' => NumberLikeThing.new(10)
end end
def test_modulo 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 }}"
end
assert_template_result "1", "{{ price | modulo:2 }}", 'price' => NumberLikeThing.new(3)
end end
def test_round def test_round
assert_template_result "5", "{{ input | round }}", 'input' => 4.6 assert_template_result "5", "{{ input | round }}", 'input' => 4.6
assert_template_result "4", "{{ '4.3' | round }}" assert_template_result "4", "{{ '4.3' | round }}"
assert_template_result "4.56", "{{ input | round: 2 }}", 'input' => 4.5612 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 }}"
end
assert_template_result "5", "{{ price | round }}", 'price' => NumberLikeThing.new(4.6)
assert_template_result "4", "{{ price | round }}", 'price' => NumberLikeThing.new(4.3)
end end
def test_ceil def test_ceil
assert_template_result "5", "{{ input | ceil }}", 'input' => 4.6 assert_template_result "5", "{{ input | ceil }}", 'input' => 4.6
assert_template_result "5", "{{ '4.3' | ceil }}" assert_template_result "5", "{{ '4.3' | ceil }}"
assert_raises(Liquid::FloatDomainError) do
assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | ceil }}"
end
assert_template_result "5", "{{ price | ceil }}", 'price' => NumberLikeThing.new(4.6)
end end
def test_floor def test_floor
assert_template_result "4", "{{ input | floor }}", 'input' => 4.6 assert_template_result "4", "{{ input | floor }}", 'input' => 4.6
assert_template_result "4", "{{ '4.3' | floor }}" assert_template_result "4", "{{ '4.3' | floor }}"
assert_raises(Liquid::FloatDomainError) do
assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | floor }}"
end
assert_template_result "5", "{{ price | floor }}", 'price' => NumberLikeThing.new(5.4)
end end
def test_append def test_append
assigns = { 'a' => 'bc', 'b' => 'd' } assigns = {'a' => 'bc', 'b' => 'd' }
assert_template_result('bcd', "{{ a | append: 'd'}}", assigns) 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
assert_equal [1, 2, 3, 4], @filters.concat([1, 2], [3, 4])
assert_equal [1, 2, 'a'], @filters.concat([1, 2], ['a'])
assert_equal [1, 2, 10], @filters.concat([1, 2], [10])
assert_raises(Liquid::ArgumentError, "concat filter requires an array argument") do
@filters.concat([1, 2], 10)
end
end end
def test_prepend def test_prepend
assigns = { 'a' => 'bc', 'b' => 'a' } assigns = {'a' => 'bc', 'b' => 'a' }
assert_template_result('abc', "{{ a | prepend: 'a'}}", assigns) 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 end
def test_default def test_default
@@ -528,21 +374,6 @@ class StandardFiltersTest < Minitest::Test
end end
def test_cannot_access_private_methods def test_cannot_access_private_methods
assert_template_result('a', "{{ 'a' | to_number }}") assert_template_result('a',"{{ 'a' | to_number }}")
end
def test_date_raises_nothing
assert_template_result('', "{{ '' | date: '%D' }}")
assert_template_result('abc', "{{ 'abc' | date: '%D' }}")
end
private
def with_timezone(tz)
old_tz = ENV['TZ']
ENV['TZ'] = tz
yield
ensure
ENV['TZ'] = old_tz
end end
end # StandardFiltersTest end # StandardFiltersTest

View File

@@ -6,10 +6,11 @@ class BreakTagTest < Minitest::Test
# tests that no weird errors are raised if break is called outside of a # tests that no weird errors are raised if break is called outside of a
# block # block
def test_break_with_no_block def test_break_with_no_block
assigns = { 'i' => 1 } assigns = {'i' => 1}
markup = '{% break %}' markup = '{% break %}'
expected = '' expected = ''
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
end end
end end

View File

@@ -12,4 +12,5 @@ class ContinueTagTest < Minitest::Test
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
end end
end end

View File

@@ -10,10 +10,10 @@ class ForTagTest < Minitest::Test
include Liquid include Liquid
def test_for def test_for
assert_template_result(' yo yo yo yo ', '{%for item in array%} yo {%endfor%}', 'array' => [1, 2, 3, 4]) assert_template_result(' yo yo yo yo ','{%for item in array%} yo {%endfor%}','array' => [1,2,3,4])
assert_template_result('yoyo', '{%for item in array%}yo{%endfor%}', 'array' => [1, 2]) assert_template_result('yoyo','{%for item in array%}yo{%endfor%}','array' => [1,2])
assert_template_result(' yo ', '{%for item in array%} yo {%endfor%}', 'array' => [1]) assert_template_result(' yo ','{%for item in array%} yo {%endfor%}','array' => [1])
assert_template_result('', '{%for item in array%}{%endfor%}', 'array' => [1, 2]) assert_template_result('','{%for item in array%}{%endfor%}','array' => [1,2])
expected = <<HERE expected = <<HERE
yo yo
@@ -28,56 +28,46 @@ HERE
yo yo
{%endfor%} {%endfor%}
HERE HERE
assert_template_result(expected, template, 'array' => [1, 2, 3]) assert_template_result(expected,template,'array' => [1,2,3])
end end
def test_for_reversed 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) assert_template_result('321','{%for item in array reversed %}{{item}}{%endfor%}',assigns)
end end
def test_for_with_range def test_for_with_range
assert_template_result(' 1 2 3 ', '{%for item in (1..3) %} {{item}} {%endfor%}') 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])
end
assert_template_result(' 0 1 2 3 ', '{% for item in (a..3) %} {{item}} {% endfor %}', "a" => "invalid integer")
end end
def test_for_with_variable_range 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)
assert_template_result(' 1.0 2.0 3.0 ', '{%for item in foobar %} {{item}} {%endfor%}', "foobar" => (1..3.0))
assert_template_result(' 1.0 2.0 3.0 ', '{%for item in foobar %} {{item}} {%endfor%}', "foobar" => (1.0..3))
assert_template_result(' 1.0 2.0 3.0 ', '{%for item in foobar %} {{item}} {%endfor%}', "foobar" => (1.0..3.0))
assert_template_result(' 1.5 2.5 ', '{%for item in foobar %} {{item}} {%endfor%}', "foobar" => (1.5..3))
end end
def test_for_with_hash_value_range def test_for_with_hash_value_range
foobar = { "value" => 3 } foobar = { "value" => 3 }
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 end
def test_for_with_drop_value_range def test_for_with_drop_value_range
foobar = ThingWithValue.new 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 end
def test_for_with_variable def test_for_with_variable
assert_template_result(' 1 2 3 ', '{%for item in array%} {{item}} {%endfor%}', 'array' => [1, 2, 3]) 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('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' => ['a','b','c','d'])
assert_template_result('a b c', '{%for item in array%}{{item}}{%endfor%}', 'array' => ['a', ' ', 'b', ' ', 'c']) 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']) assert_template_result('abc','{%for item in array%}{{item}}{%endfor%}','array' => ['a','','b','','c'])
end end
def test_for_helpers def test_for_helpers
assigns = { 'array' => [1, 2, 3] } assigns = {'array' => [1,2,3] }
assert_template_result(' 1/3 2/3 3/3 ', assert_template_result(' 1/3 2/3 3/3 ',
'{%for item in array%} {{forloop.index}}/{{forloop.length}} {%endfor%}', '{%for item in array%} {{forloop.index}}/{{forloop.length}} {%endfor%}',
assigns) assigns)
assert_template_result(' 1 2 3 ', '{%for item in array%} {{forloop.index}} {%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(' 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) assert_template_result(' 2 1 0 ', '{%for item in array%} {{forloop.rindex0}} {%endfor%}', assigns)
@@ -87,20 +77,20 @@ HERE
end end
def test_for_and_if def test_for_and_if
assigns = { 'array' => [1, 2, 3] } assigns = {'array' => [1,2,3] }
assert_template_result('+--', assert_template_result('+--',
'{%for item in array%}{% if forloop.first %}+{% else %}-{% endif %}{%endfor%}', '{%for item in array%}{% if forloop.first %}+{% else %}-{% endif %}{%endfor%}',
assigns) assigns)
end end
def test_for_else def test_for_else
assert_template_result('+++', '{%for item in array%}+{%else%}-{%endfor%}', 'array' => [1, 2, 3]) assert_template_result('+++', '{%for item in array%}+{%else%}-{%endfor%}', 'array'=>[1,2,3])
assert_template_result('-', '{%for item in array%}+{%else%}-{%endfor%}', 'array' => []) assert_template_result('-', '{%for item in array%}+{%else%}-{%endfor%}', 'array'=>[])
assert_template_result('-', '{%for item in array%}+{%else%}-{%endfor%}', 'array' => nil) assert_template_result('-', '{%for item in array%}+{%else%}-{%endfor%}', 'array'=>nil)
end end
def test_limiting def test_limiting
assigns = { 'array' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } assigns = {'array' => [1,2,3,4,5,6,7,8,9,0]}
assert_template_result('12', '{%for i in array limit:2 %}{{ i }}{%endfor%}', assigns) assert_template_result('12', '{%for i in array limit:2 %}{{ i }}{%endfor%}', assigns)
assert_template_result('1234', '{%for i in array limit:4 %}{{ i }}{%endfor%}', assigns) assert_template_result('1234', '{%for i in array limit:4 %}{{ i }}{%endfor%}', assigns)
assert_template_result('3456', '{%for i in array limit:4 offset:2 %}{{ i }}{%endfor%}', assigns) assert_template_result('3456', '{%for i in array limit:4 offset:2 %}{{ i }}{%endfor%}', assigns)
@@ -108,7 +98,7 @@ HERE
end end
def test_dynamic_variable_limiting def test_dynamic_variable_limiting
assigns = { 'array' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } assigns = {'array' => [1,2,3,4,5,6,7,8,9,0]}
assigns['limit'] = 2 assigns['limit'] = 2
assigns['offset'] = 2 assigns['offset'] = 2
@@ -116,17 +106,17 @@ HERE
end end
def test_nested_for def test_nested_for
assigns = { 'array' => [[1, 2], [3, 4], [5, 6]] } assigns = {'array' => [[1,2],[3,4],[5,6]] }
assert_template_result('123456', '{%for item in array%}{%for i in item%}{{ i }}{%endfor%}{%endfor%}', assigns) assert_template_result('123456', '{%for item in array%}{%for i in item%}{{ i }}{%endfor%}{%endfor%}', assigns)
end end
def test_offset_only def test_offset_only
assigns = { 'array' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } assigns = {'array' => [1,2,3,4,5,6,7,8,9,0]}
assert_template_result('890', '{%for i in array offset:7 %}{{ i }}{%endfor%}', assigns) assert_template_result('890', '{%for i in array offset:7 %}{{ i }}{%endfor%}', assigns)
end end
def test_pause_resume def test_pause_resume
assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } } assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,0]}}
markup = <<-MKUP markup = <<-MKUP
{%for i in array.items limit: 3 %}{{i}}{%endfor%} {%for i in array.items limit: 3 %}{{i}}{%endfor%}
next next
@@ -141,11 +131,11 @@ HERE
next next
789 789
XPCTD XPCTD
assert_template_result(expected, markup, assigns) assert_template_result(expected,markup,assigns)
end end
def test_pause_resume_limit def test_pause_resume_limit
assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } } assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,0]}}
markup = <<-MKUP markup = <<-MKUP
{%for i in array.items limit:3 %}{{i}}{%endfor%} {%for i in array.items limit:3 %}{{i}}{%endfor%}
next next
@@ -160,11 +150,11 @@ HERE
next next
7 7
XPCTD XPCTD
assert_template_result(expected, markup, assigns) assert_template_result(expected,markup,assigns)
end 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] } } assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,0]}}
markup = <<-MKUP markup = <<-MKUP
{%for i in array.items limit:3 %}{{i}}{%endfor%} {%for i in array.items limit:3 %}{{i}}{%endfor%}
next next
@@ -179,102 +169,103 @@ HERE
next next
7890 7890
XPCTD XPCTD
assert_template_result(expected, markup, assigns) assert_template_result(expected,markup,assigns)
end 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] } } assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,0]}}
markup = '{%for i in array.items limit:3 %}{{i}}{%endfor%} markup = %q({%for i in array.items limit:3 %}{{i}}{%endfor%}
next next
{%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%} {%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%}
next next
{%for i in array.items offset:continue limit:3 offset:1000 %}{{i}}{%endfor%}' {%for i in array.items offset:continue limit:3 offset:1000 %}{{i}}{%endfor%})
expected = '123 expected = %q(123
next next
456 456
next next
' )
assert_template_result(expected, markup, assigns) assert_template_result(expected,markup,assigns)
end end
def test_for_with_break def test_for_with_break
assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] } } assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,10]}}
markup = '{% for i in array.items %}{% break %}{% endfor %}' markup = '{% for i in array.items %}{% break %}{% endfor %}'
expected = "" expected = ""
assert_template_result(expected, markup, assigns) assert_template_result(expected,markup,assigns)
markup = '{% for i in array.items %}{{ i }}{% break %}{% endfor %}' markup = '{% for i in array.items %}{{ i }}{% break %}{% endfor %}'
expected = "1" expected = "1"
assert_template_result(expected, markup, assigns) assert_template_result(expected,markup,assigns)
markup = '{% for i in array.items %}{% break %}{{ i }}{% endfor %}' markup = '{% for i in array.items %}{% break %}{{ i }}{% endfor %}'
expected = "" expected = ""
assert_template_result(expected, markup, assigns) assert_template_result(expected,markup,assigns)
markup = '{% for i in array.items %}{{ i }}{% if i > 3 %}{% break %}{% endif %}{% endfor %}' markup = '{% for i in array.items %}{{ i }}{% if i > 3 %}{% break %}{% endif %}{% endfor %}'
expected = "1234" expected = "1234"
assert_template_result(expected, markup, assigns) assert_template_result(expected,markup,assigns)
# tests to ensure it only breaks out of the local for loop # tests to ensure it only breaks out of the local for loop
# and not all of them. # and not all of them.
assigns = { 'array' => [[1, 2], [3, 4], [5, 6]] } assigns = {'array' => [[1,2],[3,4],[5,6]] }
markup = '{% for item in array %}' \ markup = '{% for item in array %}' +
'{% for i in item %}' \ '{% for i in item %}' +
'{% if i == 1 %}' \ '{% if i == 1 %}' +
'{% break %}' \ '{% break %}' +
'{% endif %}' \ '{% endif %}' +
'{{ i }}' \ '{{ i }}' +
'{% endfor %}' \ '{% endfor %}' +
'{% endfor %}' '{% endfor %}'
expected = '3456' expected = '3456'
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
# test break does nothing when unreached # test break does nothing when unreached
assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5] } } assigns = {'array' => {'items' => [1,2,3,4,5]}}
markup = '{% for i in array.items %}{% if i == 9999 %}{% break %}{% endif %}{{ i }}{% endfor %}' markup = '{% for i in array.items %}{% if i == 9999 %}{% break %}{% endif %}{{ i }}{% endfor %}'
expected = '12345' expected = '12345'
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
end end
def test_for_with_continue def test_for_with_continue
assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5] } } assigns = {'array' => {'items' => [1,2,3,4,5]}}
markup = '{% for i in array.items %}{% continue %}{% endfor %}' markup = '{% for i in array.items %}{% continue %}{% endfor %}'
expected = "" expected = ""
assert_template_result(expected, markup, assigns) assert_template_result(expected,markup,assigns)
markup = '{% for i in array.items %}{{ i }}{% continue %}{% endfor %}' markup = '{% for i in array.items %}{{ i }}{% continue %}{% endfor %}'
expected = "12345" expected = "12345"
assert_template_result(expected, markup, assigns) assert_template_result(expected,markup,assigns)
markup = '{% for i in array.items %}{% continue %}{{ i }}{% endfor %}' markup = '{% for i in array.items %}{% continue %}{{ i }}{% endfor %}'
expected = "" expected = ""
assert_template_result(expected, markup, assigns) assert_template_result(expected,markup,assigns)
markup = '{% for i in array.items %}{% if i > 3 %}{% continue %}{% endif %}{{ i }}{% endfor %}' markup = '{% for i in array.items %}{% if i > 3 %}{% continue %}{% endif %}{{ i }}{% endfor %}'
expected = "123" expected = "123"
assert_template_result(expected, markup, assigns) assert_template_result(expected,markup,assigns)
markup = '{% for i in array.items %}{% if i == 3 %}{% continue %}{% else %}{{ i }}{% endif %}{% endfor %}' markup = '{% for i in array.items %}{% if i == 3 %}{% continue %}{% else %}{{ i }}{% endif %}{% endfor %}'
expected = "1245" expected = "1245"
assert_template_result(expected, markup, assigns) assert_template_result(expected,markup,assigns)
# tests to ensure it only continues the local for loop and not all of them. # tests to ensure it only continues the local for loop and not all of them.
assigns = { 'array' => [[1, 2], [3, 4], [5, 6]] } assigns = {'array' => [[1,2],[3,4],[5,6]] }
markup = '{% for item in array %}' \ markup = '{% for item in array %}' +
'{% for i in item %}' \ '{% for i in item %}' +
'{% if i == 1 %}' \ '{% if i == 1 %}' +
'{% continue %}' \ '{% continue %}' +
'{% endif %}' \ '{% endif %}' +
'{{ i }}' \ '{{ i }}' +
'{% endfor %}' \ '{% endfor %}' +
'{% endfor %}' '{% endfor %}'
expected = '23456' expected = '23456'
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
# test continue does nothing when unreached # test continue does nothing when unreached
assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5] } } assigns = {'array' => {'items' => [1,2,3,4,5]}}
markup = '{% for i in array.items %}{% if i == 9999 %}{% continue %}{% endif %}{{ i }}{% endfor %}' markup = '{% for i in array.items %}{% if i == 9999 %}{% continue %}{% endif %}{{ i }}{% endfor %}'
expected = '12345' expected = '12345'
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
@@ -286,45 +277,25 @@ HERE
# the functionality for backwards compatibility # the functionality for backwards compatibility
assert_template_result('test string', assert_template_result('test string',
'{%for val in string%}{{val}}{%endfor%}', '{%for val in string%}{{val}}{%endfor%}',
'string' => "test string") 'string' => "test string")
assert_template_result('test string', assert_template_result('test string',
'{%for val in string limit:1%}{{val}}{%endfor%}', '{%for val in string limit:1%}{{val}}{%endfor%}',
'string' => "test string") 'string' => "test string")
assert_template_result('val-string-1-1-0-1-0-true-true-test string', assert_template_result('val-string-1-1-0-1-0-true-true-test string',
'{%for val in string%}' \ '{%for val in string%}' +
'{{forloop.name}}-' \ '{{forloop.name}}-' +
'{{forloop.index}}-' \ '{{forloop.index}}-' +
'{{forloop.length}}-' \ '{{forloop.length}}-' +
'{{forloop.index0}}-' \ '{{forloop.index0}}-' +
'{{forloop.rindex}}-' \ '{{forloop.rindex}}-' +
'{{forloop.rindex0}}-' \ '{{forloop.rindex0}}-' +
'{{forloop.first}}-' \ '{{forloop.first}}-' +
'{{forloop.last}}-' \ '{{forloop.last}}-' +
'{{val}}{%endfor%}', '{{val}}{%endfor%}',
'string' => "test string") '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]])
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]])
end
def test_inner_for_over_empty_input
assert_template_result 'oo', '{% for a in (1..2) %}o{% for b in empty %}{% endfor %}{% endfor %}'
end end
def test_blank_string_not_iterable def test_blank_string_not_iterable
@@ -340,7 +311,7 @@ HERE
def test_spacing_with_variable_naming_in_for_loop def test_spacing_with_variable_naming_in_for_loop
expected = '12345' expected = '12345'
template = '{% for item in items %}{{item}}{% endfor %}' template = '{% for item in items %}{{item}}{% endfor %}'
assigns = { 'items' => [1, 2, 3, 4, 5] } assigns = {'items' => [1,2,3,4,5]}
assert_template_result(expected, template, assigns) assert_template_result(expected, template, assigns)
end end
@@ -358,13 +329,13 @@ HERE
def load_slice(from, to) def load_slice(from, to)
@load_slice_called = true @load_slice_called = true
@data[(from..to - 1)] @data[(from..to-1)]
end end
end end
def test_iterate_with_each_when_no_limit_applied def test_iterate_with_each_when_no_limit_applied
loader = LoaderDrop.new([1, 2, 3, 4, 5]) loader = LoaderDrop.new([1,2,3,4,5])
assigns = { 'items' => loader } assigns = {'items' => loader}
expected = '12345' expected = '12345'
template = '{% for item in items %}{{item}}{% endfor %}' template = '{% for item in items %}{{item}}{% endfor %}'
assert_template_result(expected, template, assigns) assert_template_result(expected, template, assigns)
@@ -373,8 +344,8 @@ HERE
end end
def test_iterate_with_load_slice_when_limit_applied def test_iterate_with_load_slice_when_limit_applied
loader = LoaderDrop.new([1, 2, 3, 4, 5]) loader = LoaderDrop.new([1,2,3,4,5])
assigns = { 'items' => loader } assigns = {'items' => loader}
expected = '1' expected = '1'
template = '{% for item in items limit:1 %}{{item}}{% endfor %}' template = '{% for item in items limit:1 %}{{item}}{% endfor %}'
assert_template_result(expected, template, assigns) assert_template_result(expected, template, assigns)
@@ -383,8 +354,8 @@ HERE
end end
def test_iterate_with_load_slice_when_limit_and_offset_applied def test_iterate_with_load_slice_when_limit_and_offset_applied
loader = LoaderDrop.new([1, 2, 3, 4, 5]) loader = LoaderDrop.new([1,2,3,4,5])
assigns = { 'items' => loader } assigns = {'items' => loader}
expected = '34' expected = '34'
template = '{% for item in items offset:2 limit:2 %}{{item}}{% endfor %}' template = '{% for item in items offset:2 limit:2 %}{{item}}{% endfor %}'
assert_template_result(expected, template, assigns) assert_template_result(expected, template, assigns)
@@ -393,22 +364,12 @@ HERE
end end
def test_iterate_with_load_slice_returns_same_results_as_without def test_iterate_with_load_slice_returns_same_results_as_without
loader = LoaderDrop.new([1, 2, 3, 4, 5]) loader = LoaderDrop.new([1,2,3,4,5])
loader_assigns = { 'items' => loader } loader_assigns = {'items' => loader}
array_assigns = { 'items' => [1, 2, 3, 4, 5] } array_assigns = {'items' => [1,2,3,4,5]}
expected = '34' expected = '34'
template = '{% for item in items offset:2 limit:2 %}{{item}}{% endfor %}' template = '{% for item in items offset:2 limit:2 %}{{item}}{% endfor %}'
assert_template_result(expected, template, loader_assigns) assert_template_result(expected, template, loader_assigns)
assert_template_result(expected, template, array_assigns) assert_template_result(expected, template, array_assigns)
end end
def test_for_cleans_up_registers
context = Context.new(ErrorDrop.new)
assert_raises(StandardError) do
Liquid::Template.parse('{% for i in (1..2) %}{{ standard_error }}{% endfor %}').render!(context)
end
assert context.registers[:for_stack].empty?
end
end end

View File

@@ -4,100 +4,101 @@ class IfElseTagTest < Minitest::Test
include Liquid include Liquid
def test_if def test_if
assert_template_result(' ', ' {% if false %} this text should not go into the output {% endif %} ') assert_template_result(' ',' {% if false %} this text should not go into the output {% endif %} ')
assert_template_result(' this text should go into the output ', assert_template_result(' this text should go into the output ',
' {% if true %} this text should go into the output {% endif %} ') ' {% if true %} this text should go into the output {% endif %} ')
assert_template_result(' you rock ?', '{% if false %} you suck {% endif %} {% if true %} you rock {% endif %}?') assert_template_result(' you rock ?','{% if false %} you suck {% endif %} {% if true %} you rock {% endif %}?')
end end
def test_literal_comparisons def test_literal_comparisons
assert_template_result(' NO ', '{% assign v = false %}{% if v %} YES {% else %} NO {% endif %}') assert_template_result(' NO ','{% assign v = false %}{% if v %} YES {% else %} NO {% endif %}')
assert_template_result(' YES ', '{% assign v = nil %}{% if v == nil %} YES {% else %} NO {% endif %}') assert_template_result(' YES ','{% assign v = nil %}{% if v == nil %} YES {% else %} NO {% endif %}')
end end
def test_if_else def test_if_else
assert_template_result(' YES ', '{% if false %} NO {% else %} YES {% endif %}') assert_template_result(' YES ','{% if false %} NO {% else %} YES {% endif %}')
assert_template_result(' YES ', '{% if true %} YES {% else %} NO {% endif %}') assert_template_result(' YES ','{% if true %} YES {% else %} NO {% endif %}')
assert_template_result(' YES ', '{% if "foo" %} YES {% else %} NO {% endif %}') assert_template_result(' YES ','{% if "foo" %} YES {% else %} NO {% endif %}')
end end
def test_if_boolean def test_if_boolean
assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => true) assert_template_result(' YES ','{% if var %} YES {% endif %}', 'var' => true)
end end
def test_if_or def test_if_or
assert_template_result(' YES ', '{% if a or b %} YES {% endif %}', 'a' => true, 'b' => true) assert_template_result(' YES ','{% if a or b %} YES {% endif %}', 'a' => true, 'b' => true)
assert_template_result(' YES ', '{% if a or b %} YES {% endif %}', 'a' => true, 'b' => false) assert_template_result(' YES ','{% if a or b %} YES {% endif %}', 'a' => true, 'b' => false)
assert_template_result(' YES ', '{% if a or b %} YES {% endif %}', 'a' => false, 'b' => true) assert_template_result(' YES ','{% if a or b %} YES {% endif %}', 'a' => false, 'b' => true)
assert_template_result('', '{% if a or b %} YES {% endif %}', 'a' => false, 'b' => false) assert_template_result('', '{% if a or b %} YES {% endif %}', 'a' => false, 'b' => false)
assert_template_result(' YES ', '{% if a or b or c %} YES {% endif %}', 'a' => false, 'b' => false, 'c' => true) assert_template_result(' YES ','{% if a or b or c %} YES {% endif %}', 'a' => false, 'b' => false, 'c' => true)
assert_template_result('', '{% if a or b or c %} YES {% endif %}', 'a' => false, 'b' => false, 'c' => false) assert_template_result('', '{% if a or b or c %} YES {% endif %}', 'a' => false, 'b' => false, 'c' => false)
end end
def test_if_or_with_operators def test_if_or_with_operators
assert_template_result(' YES ', '{% if a == true or b == true %} YES {% endif %}', 'a' => true, 'b' => true) assert_template_result(' YES ','{% if a == true or b == true %} YES {% endif %}', 'a' => true, 'b' => true)
assert_template_result(' YES ', '{% if a == true or b == false %} YES {% endif %}', 'a' => true, 'b' => true) assert_template_result(' YES ','{% if a == true or b == false %} YES {% endif %}', 'a' => true, 'b' => true)
assert_template_result('', '{% if a == false or b == false %} YES {% endif %}', 'a' => true, 'b' => true) assert_template_result('','{% if a == false or b == false %} YES {% endif %}', 'a' => true, 'b' => true)
end end
def test_comparison_of_strings_containing_and_or_or def test_comparison_of_strings_containing_and_or_or
awful_markup = "a == 'and' and b == 'or' and c == 'foo and bar' and d == 'bar or baz' and e == 'foo' and foo and bar" awful_markup = "a == 'and' and b == 'or' and c == 'foo and bar' and d == 'bar or baz' and e == 'foo' and foo and bar"
assigns = { 'a' => 'and', 'b' => 'or', 'c' => 'foo and bar', 'd' => 'bar or baz', 'e' => 'foo', 'foo' => true, 'bar' => true } assigns = {'a' => 'and', 'b' => 'or', 'c' => 'foo and bar', 'd' => 'bar or baz', 'e' => 'foo', 'foo' => true, 'bar' => true}
assert_template_result(' YES ', "{% if #{awful_markup} %} YES {% endif %}", assigns) assert_template_result(' YES ',"{% if #{awful_markup} %} YES {% endif %}", assigns)
end end
def test_comparison_of_expressions_starting_with_and_or_or def test_comparison_of_expressions_starting_with_and_or_or
assigns = { 'order' => { 'items_count' => 0 }, 'android' => { 'name' => 'Roy' } } assigns = {'order' => {'items_count' => 0}, 'android' => {'name' => 'Roy'}}
assert_template_result("YES", assert_template_result( "YES",
"{% if android.name == 'Roy' %}YES{% endif %}", "{% if android.name == 'Roy' %}YES{% endif %}",
assigns) assigns)
assert_template_result("YES", assert_template_result( "YES",
"{% if order.items_count == 0 %}YES{% endif %}", "{% if order.items_count == 0 %}YES{% endif %}",
assigns) assigns)
end end
def test_if_and def test_if_and
assert_template_result(' YES ', '{% if true and true %} YES {% endif %}') assert_template_result(' YES ','{% if true and true %} YES {% endif %}')
assert_template_result('', '{% if false and true %} YES {% endif %}') assert_template_result('','{% if false and true %} YES {% endif %}')
assert_template_result('', '{% if false and true %} YES {% endif %}') assert_template_result('','{% if false and true %} YES {% endif %}')
end end
def test_hash_miss_generates_false def test_hash_miss_generates_false
assert_template_result('', '{% if foo.bar %} NO {% endif %}', 'foo' => {}) assert_template_result('','{% if foo.bar %} NO {% endif %}', 'foo' => {})
end end
def test_if_from_variable def test_if_from_variable
assert_template_result('', '{% if var %} NO {% endif %}', 'var' => false) assert_template_result('','{% if var %} NO {% endif %}', 'var' => false)
assert_template_result('', '{% if var %} NO {% endif %}', 'var' => nil) assert_template_result('','{% if var %} NO {% endif %}', 'var' => nil)
assert_template_result('', '{% if foo.bar %} NO {% endif %}', 'foo' => { 'bar' => false }) assert_template_result('','{% if foo.bar %} NO {% endif %}', 'foo' => {'bar' => false})
assert_template_result('', '{% if foo.bar %} NO {% endif %}', 'foo' => {}) assert_template_result('','{% if foo.bar %} NO {% endif %}', 'foo' => {})
assert_template_result('', '{% if foo.bar %} NO {% endif %}', 'foo' => nil) assert_template_result('','{% if foo.bar %} NO {% endif %}', 'foo' => nil)
assert_template_result('', '{% if foo.bar %} NO {% endif %}', 'foo' => true) assert_template_result('','{% if foo.bar %} NO {% endif %}', 'foo' => true)
assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => "text") assert_template_result(' YES ','{% if var %} YES {% endif %}', 'var' => "text")
assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => true) assert_template_result(' YES ','{% if var %} YES {% endif %}', 'var' => true)
assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => 1) assert_template_result(' YES ','{% if var %} YES {% endif %}', 'var' => 1)
assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => {}) assert_template_result(' YES ','{% if var %} YES {% endif %}', 'var' => {})
assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => []) assert_template_result(' YES ','{% if var %} YES {% endif %}', 'var' => [])
assert_template_result(' YES ', '{% if "foo" %} YES {% endif %}') assert_template_result(' YES ','{% if "foo" %} YES {% endif %}')
assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => true }) assert_template_result(' YES ','{% if foo.bar %} YES {% endif %}', 'foo' => {'bar' => true})
assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => "text" }) assert_template_result(' YES ','{% if foo.bar %} YES {% endif %}', 'foo' => {'bar' => "text"})
assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => 1 }) assert_template_result(' YES ','{% if foo.bar %} YES {% endif %}', 'foo' => {'bar' => 1 })
assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => {} }) assert_template_result(' YES ','{% if foo.bar %} YES {% endif %}', 'foo' => {'bar' => {} })
assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => [] }) assert_template_result(' YES ','{% if foo.bar %} YES {% endif %}', 'foo' => {'bar' => [] })
assert_template_result(' YES ', '{% if var %} NO {% else %} YES {% endif %}', 'var' => false) assert_template_result(' YES ','{% if var %} NO {% else %} YES {% endif %}', 'var' => false)
assert_template_result(' YES ', '{% if var %} NO {% else %} YES {% endif %}', 'var' => nil) assert_template_result(' YES ','{% if var %} NO {% else %} YES {% endif %}', 'var' => nil)
assert_template_result(' YES ', '{% if var %} YES {% else %} NO {% endif %}', 'var' => true) assert_template_result(' YES ','{% if var %} YES {% else %} NO {% endif %}', 'var' => true)
assert_template_result(' YES ', '{% if "foo" %} YES {% else %} NO {% endif %}', 'var' => "text") assert_template_result(' YES ','{% if "foo" %} YES {% else %} NO {% endif %}', 'var' => "text")
assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => { 'bar' => false }) assert_template_result(' YES ','{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => {'bar' => false})
assert_template_result(' YES ', '{% if foo.bar %} YES {% else %} NO {% endif %}', 'foo' => { 'bar' => true }) assert_template_result(' YES ','{% if foo.bar %} YES {% else %} NO {% endif %}', 'foo' => {'bar' => true})
assert_template_result(' YES ', '{% if foo.bar %} YES {% else %} NO {% endif %}', 'foo' => { 'bar' => "text" }) assert_template_result(' YES ','{% if foo.bar %} YES {% else %} NO {% endif %}', 'foo' => {'bar' => "text"})
assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => { 'notbar' => true }) assert_template_result(' YES ','{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => {'notbar' => true})
assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => {}) assert_template_result(' YES ','{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => {})
assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', 'notfoo' => { 'bar' => true }) assert_template_result(' YES ','{% if foo.bar %} NO {% else %} YES {% endif %}', 'notfoo' => {'bar' => true})
end end
def test_nested_if def test_nested_if
@@ -109,30 +110,31 @@ class IfElseTagTest < Minitest::Test
assert_template_result(' YES ', '{% if true %}{% if true %} YES {% else %} NO {% endif %}{% else %} NO {% endif %}') assert_template_result(' YES ', '{% if true %}{% if true %} YES {% else %} NO {% endif %}{% else %} NO {% endif %}')
assert_template_result(' YES ', '{% if true %}{% if false %} NO {% else %} YES {% endif %}{% else %} NO {% endif %}') assert_template_result(' YES ', '{% if true %}{% if false %} NO {% else %} YES {% endif %}{% else %} NO {% endif %}')
assert_template_result(' YES ', '{% if false %}{% if true %} NO {% else %} NONO {% endif %}{% else %} YES {% endif %}') assert_template_result(' YES ', '{% if false %}{% if true %} NO {% else %} NONO {% endif %}{% else %} YES {% endif %}')
end end
def test_comparisons_on_null def test_comparisons_on_null
assert_template_result('', '{% if null < 10 %} NO {% endif %}') assert_template_result('','{% if null < 10 %} NO {% endif %}')
assert_template_result('', '{% if null <= 10 %} NO {% endif %}') assert_template_result('','{% if null <= 10 %} NO {% endif %}')
assert_template_result('', '{% if null >= 10 %} NO {% endif %}') assert_template_result('','{% if null >= 10 %} NO {% endif %}')
assert_template_result('', '{% if null > 10 %} NO {% endif %}') assert_template_result('','{% if null > 10 %} NO {% endif %}')
assert_template_result('', '{% if 10 < null %} NO {% endif %}') assert_template_result('','{% if 10 < null %} NO {% endif %}')
assert_template_result('', '{% if 10 <= null %} NO {% endif %}') assert_template_result('','{% if 10 <= null %} NO {% endif %}')
assert_template_result('', '{% if 10 >= null %} NO {% endif %}') assert_template_result('','{% if 10 >= null %} NO {% endif %}')
assert_template_result('', '{% if 10 > null %} NO {% endif %}') assert_template_result('','{% if 10 > null %} NO {% endif %}')
end end
def test_else_if def test_else_if
assert_template_result('0', '{% if 0 == 0 %}0{% elsif 1 == 1%}1{% else %}2{% endif %}') assert_template_result('0','{% if 0 == 0 %}0{% elsif 1 == 1%}1{% else %}2{% endif %}')
assert_template_result('1', '{% if 0 != 0 %}0{% elsif 1 == 1%}1{% else %}2{% endif %}') assert_template_result('1','{% if 0 != 0 %}0{% elsif 1 == 1%}1{% else %}2{% endif %}')
assert_template_result('2', '{% if 0 != 0 %}0{% elsif 1 != 1%}1{% else %}2{% endif %}') assert_template_result('2','{% if 0 != 0 %}0{% elsif 1 != 1%}1{% else %}2{% endif %}')
assert_template_result('elsif', '{% if false %}if{% elsif true %}elsif{% endif %}') assert_template_result('elsif','{% if false %}if{% elsif true %}elsif{% endif %}')
end end
def test_syntax_error_no_variable def test_syntax_error_no_variable
assert_raises(SyntaxError){ assert_template_result('', '{% if jerry == 1 %}') } assert_raises(SyntaxError){ assert_template_result('', '{% if jerry == 1 %}')}
end end
def test_syntax_error_no_expression def test_syntax_error_no_expression
@@ -154,7 +156,7 @@ class IfElseTagTest < Minitest::Test
Condition.operators['contains'] = :[] Condition.operators['contains'] = :[]
assert_template_result('yes', assert_template_result('yes',
%({% if 'gnomeslab-and-or-liquid' contains 'gnomeslab-and-or-liquid' %}yes{% endif %})) %({% if 'gnomeslab-and-or-liquid' contains 'gnomeslab-and-or-liquid' %}yes{% endif %}))
ensure ensure
Condition.operators['contains'] = original_op Condition.operators['contains'] = original_op
end end
@@ -164,25 +166,4 @@ class IfElseTagTest < Minitest::Test
assert_template_result('', %({% if 1 or throw or or 1 %}yes{% endif %})) assert_template_result('', %({% if 1 or throw or or 1 %}yes{% endif %}))
end end
end end
def test_multiple_conditions
tpl = "{% if a or b and c %}true{% else %}false{% endif %}"
tests = {
[true, true, true] => true,
[true, true, false] => true,
[true, false, true] => true,
[true, false, false] => true,
[false, true, true] => true,
[false, true, false] => false,
[false, false, true] => false,
[false, false, false] => false,
}
tests.each do |vals, expected|
a, b, c = vals
assigns = { 'a' => a, 'b' => b, 'c' => c }
assert_template_result expected.to_s, tpl, assigns, assigns.to_s
end
end
end end

View File

@@ -1,7 +1,7 @@
require 'test_helper' require 'test_helper'
class TestFileSystem class TestFileSystem
def read_template_file(template_path) def read_template_file(template_path, context)
case template_path case template_path
when "product" when "product"
"Product: {{ product.title }} " "Product: {{ product.title }} "
@@ -37,14 +37,14 @@ class TestFileSystem
end end
class OtherFileSystem class OtherFileSystem
def read_template_file(template_path) def read_template_file(template_path, context)
'from OtherFileSystem' 'from OtherFileSystem'
end end
end end
class CountingFileSystem class CountingFileSystem
attr_reader :count attr_reader :count
def read_template_file(template_path) def read_template_file(template_path, context)
@count ||= 0 @count ||= 0
@count += 1 @count += 1
'from CountingFileSystem' 'from CountingFileSystem'
@@ -77,22 +77,23 @@ class IncludeTagTest < Minitest::Test
def test_include_tag_looks_for_file_system_in_registers_first def test_include_tag_looks_for_file_system_in_registers_first
assert_equal 'from OtherFileSystem', assert_equal 'from OtherFileSystem',
Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: OtherFileSystem.new }) Template.parse("{% include 'pick_a_source' %}").render!({}, :registers => {:file_system => OtherFileSystem.new})
end end
def test_include_tag_with def test_include_tag_with
assert_template_result "Product: Draft 151cm ", assert_template_result "Product: Draft 151cm ",
"{% include 'product' with products[0] %}", "products" => [ { 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' } ] "{% include 'product' with products[0] %}", "products" => [ {'title' => 'Draft 151cm'}, {'title' => 'Element 155cm'} ]
end end
def test_include_tag_with_default_name def test_include_tag_with_default_name
assert_template_result "Product: Draft 151cm ", assert_template_result "Product: Draft 151cm ",
"{% include 'product' %}", "product" => { 'title' => 'Draft 151cm' } "{% include 'product' %}", "product" => {'title' => 'Draft 151cm'}
end end
def test_include_tag_for def test_include_tag_for
assert_template_result "Product: Draft 151cm Product: Element 155cm ", assert_template_result "Product: Draft 151cm Product: Element 155cm ",
"{% include 'product' for products %}", "products" => [ { 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' } ] "{% include 'product' for products %}", "products" => [ {'title' => 'Draft 151cm'}, {'title' => 'Element 155cm'} ]
end end
def test_include_tag_with_local_variables def test_include_tag_with_local_variables
@@ -107,7 +108,7 @@ class IncludeTagTest < Minitest::Test
def test_include_tag_with_multiple_local_variables_from_context def test_include_tag_with_multiple_local_variables_from_context
assert_template_result "Locale: test123 test321", assert_template_result "Locale: test123 test321",
"{% include 'locale_variables' echo1: echo1, echo2: more_echos.echo2 %}", "{% include 'locale_variables' echo1: echo1, echo2: more_echos.echo2 %}",
'echo1' => 'test123', 'more_echos' => { "echo2" => 'test321' } 'echo1' => 'test123', 'more_echos' => { "echo2" => 'test321'}
end end
def test_included_templates_assigns_variables def test_included_templates_assigns_variables
@@ -122,24 +123,38 @@ class IncludeTagTest < Minitest::Test
def test_nested_include_with_variable def test_nested_include_with_variable
assert_template_result "Product: Draft 151cm details ", assert_template_result "Product: Draft 151cm details ",
"{% include 'nested_product_template' with product %}", "product" => { "title" => 'Draft 151cm' } "{% include 'nested_product_template' with product %}", "product" => {"title" => 'Draft 151cm'}
assert_template_result "Product: Draft 151cm details Product: Element 155cm details ", assert_template_result "Product: Draft 151cm details Product: Element 155cm details ",
"{% include 'nested_product_template' for products %}", "products" => [{ "title" => 'Draft 151cm' }, { "title" => 'Element 155cm' }] "{% include 'nested_product_template' for products %}", "products" => [{"title" => 'Draft 151cm'}, {"title" => 'Element 155cm'}]
end end
def test_recursively_included_template_does_not_produce_endless_loop def test_recursively_included_template_does_not_produce_endless_loop
infinite_file_system = Class.new do infinite_file_system = Class.new do
def read_template_file(template_path) def read_template_file(template_path, context)
"-{% include 'loop' %}" "-{% include 'loop' %}"
end end
end end
Liquid::Template.file_system = infinite_file_system.new Liquid::Template.file_system = infinite_file_system.new
assert_raises(Liquid::StackLevelError) do assert_raises(Liquid::StackLevelError, SystemStackError) do
Template.parse("{% include 'loop' %}").render! Template.parse("{% include 'loop' %}").render!
end end
end
def test_backwards_compatability_support_for_overridden_read_template_file
infinite_file_system = Class.new do
def read_template_file(template_path) # testing only one argument here.
"- hi mom"
end
end
Liquid::Template.file_system = infinite_file_system.new
Template.parse("{% include 'hi_mom' %}").render!
end end
def test_dynamically_choosen_template def test_dynamically_choosen_template
@@ -147,24 +162,24 @@ class IncludeTagTest < Minitest::Test
assert_template_result "Test321", "{% include template %}", "template" => 'Test321' assert_template_result "Test321", "{% include template %}", "template" => 'Test321'
assert_template_result "Product: Draft 151cm ", "{% include template for product %}", assert_template_result "Product: Draft 151cm ", "{% include template for product %}",
"template" => 'product', 'product' => { 'title' => 'Draft 151cm' } "template" => 'product', 'product' => { 'title' => 'Draft 151cm'}
end end
def test_include_tag_caches_second_read_of_same_partial def test_include_tag_caches_second_read_of_same_partial
file_system = CountingFileSystem.new file_system = CountingFileSystem.new
assert_equal 'from CountingFileSystemfrom CountingFileSystem', assert_equal 'from CountingFileSystemfrom CountingFileSystem',
Template.parse("{% include 'pick_a_source' %}{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system }) Template.parse("{% include 'pick_a_source' %}{% include 'pick_a_source' %}").render!({}, :registers => {:file_system => file_system})
assert_equal 1, file_system.count assert_equal 1, file_system.count
end end
def test_include_tag_doesnt_cache_partials_across_renders def test_include_tag_doesnt_cache_partials_across_renders
file_system = CountingFileSystem.new file_system = CountingFileSystem.new
assert_equal 'from CountingFileSystem', assert_equal 'from CountingFileSystem',
Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system }) Template.parse("{% include 'pick_a_source' %}").render!({}, :registers => {:file_system => file_system})
assert_equal 1, file_system.count assert_equal 1, file_system.count
assert_equal 'from CountingFileSystem', assert_equal 'from CountingFileSystem',
Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system }) Template.parse("{% include 'pick_a_source' %}").render!({}, :registers => {:file_system => file_system})
assert_equal 2, file_system.count assert_equal 2, file_system.count
end end
@@ -216,30 +231,4 @@ class IncludeTagTest < Minitest::Test
assert_equal 'x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:error_mode]).render!("template" => '{{ "X" || downcase }}') assert_equal 'x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:error_mode]).render!("template" => '{{ "X" || downcase }}')
end end
end end
def test_render_raise_argument_error_when_template_is_undefined
assert_raises(Liquid::ArgumentError) do
template = Liquid::Template.parse('{% include undefined_variable %}')
template.render!
end
assert_raises(Liquid::ArgumentError) do
template = Liquid::Template.parse('{% include nil %}')
template.render!
end
end
def test_including_via_variable_value
assert_template_result "from TestFileSystem", "{% assign page = 'pick_a_source' %}{% include page %}"
assert_template_result "Product: Draft 151cm ", "{% assign page = 'product' %}{% include page %}", "product" => { 'title' => 'Draft 151cm' }
assert_template_result "Product: Draft 151cm ", "{% assign page = 'product' %}{% include page for foo %}", "foo" => { 'title' => 'Draft 151cm' }
end
def test_including_with_strict_variables
template = Liquid::Template.parse("{% include 'simple' %}", error_mode: :warn)
template.render(nil, strict_variables: true)
assert_equal [], template.errors
end
end # IncludeTagTest end # IncludeTagTest

View File

@@ -4,20 +4,21 @@ class IncrementTagTest < Minitest::Test
include Liquid include Liquid
def test_inc def test_inc
assert_template_result('0', '{%increment port %}', {}) assert_template_result('0','{%increment port %}', {})
assert_template_result('0 1', '{%increment port %} {%increment port%}', {}) assert_template_result('0 1','{%increment port %} {%increment port%}', {})
assert_template_result('0 0 1 2 1', assert_template_result('0 0 1 2 1',
'{%increment port %} {%increment starboard%} ' \ '{%increment port %} {%increment starboard%} ' +
'{%increment port %} {%increment port%} ' \ '{%increment port %} {%increment port%} ' +
'{%increment starboard %}', {}) '{%increment starboard %}', {})
end end
def test_dec def test_dec
assert_template_result('9', '{%decrement port %}', { 'port' => 10 }) assert_template_result('9','{%decrement port %}', { 'port' => 10})
assert_template_result('-1 -2', '{%decrement port %} {%decrement port%}', {}) assert_template_result('-1 -2','{%decrement port %} {%decrement port%}', {})
assert_template_result('1 5 2 2 5', assert_template_result('1 5 2 2 5',
'{%increment port %} {%increment starboard%} ' \ '{%increment port %} {%increment starboard%} ' +
'{%increment port %} {%decrement port%} ' \ '{%increment port %} {%decrement port%} ' +
'{%decrement starboard %}', { 'port' => 1, 'starboard' => 5 }) '{%decrement starboard %}', { 'port' => 1, 'starboard' => 5 })
end end
end end

View File

@@ -5,7 +5,7 @@ class RawTagTest < Minitest::Test
def test_tag_in_raw def test_tag_in_raw
assert_template_result '{% comment %} test {% endcomment %}', assert_template_result '{% comment %} test {% endcomment %}',
'{% raw %}{% comment %} test {% endcomment %}{% endraw %}' '{% raw %}{% comment %} test {% endcomment %}{% endraw %}'
end end
def test_output_in_raw def test_output_in_raw
@@ -22,10 +22,4 @@ class RawTagTest < Minitest::Test
assert_template_result ' test {% raw %} {% endraw %}', '{% raw %} test {% raw %} {% {% endraw %}endraw %}' assert_template_result ' test {% raw %} {% endraw %}', '{% raw %} test {% raw %} {% {% endraw %}endraw %}'
assert_template_result ' Foobar {{ invalid 1', '{% raw %} Foobar {{ invalid {% endraw %}{{ 1 }}' assert_template_result ' Foobar {{ invalid 1', '{% raw %} Foobar {{ invalid {% endraw %}{{ 1 }}'
end end
def test_invalid_raw
assert_match_syntax_error(/tag was never closed/, '{% raw %} foo')
assert_match_syntax_error(/Valid syntax/, '{% raw } foo {% endraw %}')
assert_match_syntax_error(/Valid syntax/, '{% raw } foo %}{% endraw %}')
end
end end

View File

@@ -5,116 +5,116 @@ class StandardTagTest < Minitest::Test
def test_no_transform def test_no_transform
assert_template_result('this text should come out of the template without change...', assert_template_result('this text should come out of the template without change...',
'this text should come out of the template without change...') 'this text should come out of the template without change...')
assert_template_result('blah', 'blah') assert_template_result('blah','blah')
assert_template_result('<blah>', '<blah>') assert_template_result('<blah>','<blah>')
assert_template_result('|,.:', '|,.:') assert_template_result('|,.:','|,.:')
assert_template_result('', '') assert_template_result('','')
text = %(this shouldnt see any transformation either but has multiple lines text = %|this shouldnt see any transformation either but has multiple lines
as you can clearly see here ...) as you can clearly see here ...|
assert_template_result(text, text) assert_template_result(text,text)
end end
def test_has_a_block_which_does_nothing def test_has_a_block_which_does_nothing
assert_template_result(%(the comment block should be removed .. right?), assert_template_result(%|the comment block should be removed .. right?|,
%(the comment block should be removed {%comment%} be gone.. {%endcomment%} .. right?)) %|the comment block should be removed {%comment%} be gone.. {%endcomment%} .. right?|)
assert_template_result('', '{%comment%}{%endcomment%}') assert_template_result('','{%comment%}{%endcomment%}')
assert_template_result('', '{%comment%}{% endcomment %}') assert_template_result('','{%comment%}{% endcomment %}')
assert_template_result('', '{% comment %}{%endcomment%}') assert_template_result('','{% comment %}{%endcomment%}')
assert_template_result('', '{% comment %}{% endcomment %}') assert_template_result('','{% comment %}{% endcomment %}')
assert_template_result('', '{%comment%}comment{%endcomment%}') assert_template_result('','{%comment%}comment{%endcomment%}')
assert_template_result('', '{% comment %}comment{% endcomment %}') assert_template_result('','{% comment %}comment{% endcomment %}')
assert_template_result('', '{% comment %} 1 {% comment %} 2 {% endcomment %} 3 {% endcomment %}') assert_template_result('','{% comment %} 1 {% comment %} 2 {% endcomment %} 3 {% endcomment %}')
assert_template_result('', '{%comment%}{%blabla%}{%endcomment%}') assert_template_result('','{%comment%}{%blabla%}{%endcomment%}')
assert_template_result('', '{% comment %}{% blabla %}{% endcomment %}') assert_template_result('','{% comment %}{% blabla %}{% endcomment %}')
assert_template_result('', '{%comment%}{% endif %}{%endcomment%}') assert_template_result('','{%comment%}{% endif %}{%endcomment%}')
assert_template_result('', '{% comment %}{% endwhatever %}{% endcomment %}') assert_template_result('','{% comment %}{% endwhatever %}{% endcomment %}')
assert_template_result('', '{% comment %}{% raw %} {{%%%%}} }} { {% endcomment %} {% comment {% endraw %} {% endcomment %}') assert_template_result('','{% comment %}{% raw %} {{%%%%}} }} { {% endcomment %} {% comment {% endraw %} {% endcomment %}')
assert_template_result('foobar', 'foo{%comment%}comment{%endcomment%}bar') assert_template_result('foobar','foo{%comment%}comment{%endcomment%}bar')
assert_template_result('foobar', 'foo{% comment %}comment{% endcomment %}bar') assert_template_result('foobar','foo{% comment %}comment{% endcomment %}bar')
assert_template_result('foobar', 'foo{%comment%} comment {%endcomment%}bar') assert_template_result('foobar','foo{%comment%} comment {%endcomment%}bar')
assert_template_result('foobar', 'foo{% comment %} comment {% endcomment %}bar') assert_template_result('foobar','foo{% comment %} comment {% endcomment %}bar')
assert_template_result('foo bar', 'foo {%comment%} {%endcomment%} bar') assert_template_result('foo bar','foo {%comment%} {%endcomment%} bar')
assert_template_result('foo bar', 'foo {%comment%}comment{%endcomment%} bar') assert_template_result('foo bar','foo {%comment%}comment{%endcomment%} bar')
assert_template_result('foo bar', 'foo {%comment%} comment {%endcomment%} bar') assert_template_result('foo bar','foo {%comment%} comment {%endcomment%} bar')
assert_template_result('foobar', 'foo{%comment%} assert_template_result('foobar','foo{%comment%}
{%endcomment%}bar') {%endcomment%}bar')
end end
def test_hyphenated_assign def test_hyphenated_assign
assigns = { 'a-b' => '1' } assigns = {'a-b' => '1' }
assert_template_result('a-b:1 a-b:2', 'a-b:{{a-b}} {%assign a-b = 2 %}a-b:{{a-b}}', assigns) assert_template_result('a-b:1 a-b:2', 'a-b:{{a-b}} {%assign a-b = 2 %}a-b:{{a-b}}', assigns)
end end
def test_assign_with_colon_and_spaces def test_assign_with_colon_and_spaces
assigns = { 'var' => { 'a:b c' => { 'paged' => '1' } } } assigns = {'var' => {'a:b c' => {'paged' => '1' }}}
assert_template_result('var2: 1', '{%assign var2 = var["a:b c"].paged %}var2: {{var2}}', assigns) assert_template_result('var2: 1', '{%assign var2 = var["a:b c"].paged %}var2: {{var2}}', assigns)
end end
def test_capture def test_capture
assigns = { 'var' => 'content' } assigns = {'var' => 'content' }
assert_template_result('content foo content foo ', assert_template_result('content foo content foo ',
'{{ var2 }}{% capture var2 %}{{ var }} foo {% endcapture %}{{ var2 }}{{ var2 }}', '{{ var2 }}{% capture var2 %}{{ var }} foo {% endcapture %}{{ var2 }}{{ var2 }}',
assigns) assigns)
end end
def test_capture_detects_bad_syntax def test_capture_detects_bad_syntax
assert_raises(SyntaxError) do assert_raises(SyntaxError) do
assert_template_result('content foo content foo ', assert_template_result('content foo content foo ',
'{{ var2 }}{% capture %}{{ var }} foo {% endcapture %}{{ var2 }}{{ var2 }}', '{{ var2 }}{% capture %}{{ var }} foo {% endcapture %}{{ var2 }}{{ var2 }}',
{ 'var' => 'content' }) {'var' => 'content' })
end end
end end
def test_case def test_case
assigns = { 'condition' => 2 } assigns = {'condition' => 2 }
assert_template_result(' its 2 ', assert_template_result(' its 2 ',
'{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}', '{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}',
assigns) assigns)
assigns = { 'condition' => 1 } assigns = {'condition' => 1 }
assert_template_result(' its 1 ', assert_template_result(' its 1 ',
'{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}', '{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}',
assigns) assigns)
assigns = { 'condition' => 3 } assigns = {'condition' => 3 }
assert_template_result('', assert_template_result('',
'{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}', '{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}',
assigns) assigns)
assigns = { 'condition' => "string here" } assigns = {'condition' => "string here" }
assert_template_result(' hit ', assert_template_result(' hit ',
'{% case condition %}{% when "string here" %} hit {% endcase %}', '{% case condition %}{% when "string here" %} hit {% endcase %}',
assigns) assigns)
assigns = { 'condition' => "bad string here" } assigns = {'condition' => "bad string here" }
assert_template_result('', assert_template_result('',
'{% case condition %}{% when "string here" %} hit {% endcase %}',\ '{% case condition %}{% when "string here" %} hit {% endcase %}',\
assigns) assigns)
end end
def test_case_with_else def test_case_with_else
assigns = { 'condition' => 5 } assigns = {'condition' => 5 }
assert_template_result(' hit ', assert_template_result(' hit ',
'{% case condition %}{% when 5 %} hit {% else %} else {% endcase %}', '{% case condition %}{% when 5 %} hit {% else %} else {% endcase %}',
assigns) assigns)
assigns = { 'condition' => 6 } assigns = {'condition' => 6 }
assert_template_result(' else ', assert_template_result(' else ',
'{% case condition %}{% when 5 %} hit {% else %} else {% endcase %}', '{% case condition %}{% when 5 %} hit {% else %} else {% endcase %}',
assigns) assigns)
assigns = { 'condition' => 6 } assigns = {'condition' => 6 }
assert_template_result(' else ', assert_template_result(' else ',
'{% case condition %} {% when 5 %} hit {% else %} else {% endcase %}', '{% case condition %} {% when 5 %} hit {% else %} else {% endcase %}',
assigns) assigns)
end end
def test_case_on_size def test_case_on_size
@@ -128,87 +128,87 @@ class StandardTagTest < Minitest::Test
def test_case_on_size_with_else def test_case_on_size_with_else
assert_template_result('else', assert_template_result('else',
'{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}',
'a' => []) 'a' => [])
assert_template_result('1', assert_template_result('1',
'{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}',
'a' => [1]) 'a' => [1])
assert_template_result('2', assert_template_result('2',
'{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}',
'a' => [1, 1]) 'a' => [1, 1])
assert_template_result('else', assert_template_result('else',
'{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}',
'a' => [1, 1, 1]) 'a' => [1, 1, 1])
assert_template_result('else', assert_template_result('else',
'{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}',
'a' => [1, 1, 1, 1]) 'a' => [1, 1, 1, 1])
assert_template_result('else', assert_template_result('else',
'{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}',
'a' => [1, 1, 1, 1, 1]) 'a' => [1, 1, 1, 1, 1])
end end
def test_case_on_length_with_else def test_case_on_length_with_else
assert_template_result('else', assert_template_result('else',
'{% case a.empty? %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', '{% case a.empty? %}{% when true %}true{% when false %}false{% else %}else{% endcase %}',
{}) {})
assert_template_result('false', assert_template_result('false',
'{% case false %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', '{% case false %}{% when true %}true{% when false %}false{% else %}else{% endcase %}',
{}) {})
assert_template_result('true', assert_template_result('true',
'{% case true %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', '{% case true %}{% when true %}true{% when false %}false{% else %}else{% endcase %}',
{}) {})
assert_template_result('else', assert_template_result('else',
'{% case NULL %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', '{% case NULL %}{% when true %}true{% when false %}false{% else %}else{% endcase %}',
{}) {})
end end
def test_assign_from_case def test_assign_from_case
# Example from the shopify forums # Example from the shopify forums
code = "{% case collection.handle %}{% when 'menswear-jackets' %}{% assign ptitle = 'menswear' %}{% when 'menswear-t-shirts' %}{% assign ptitle = 'menswear' %}{% else %}{% assign ptitle = 'womenswear' %}{% endcase %}{{ ptitle }}" code = %q({% case collection.handle %}{% when 'menswear-jackets' %}{% assign ptitle = 'menswear' %}{% when 'menswear-t-shirts' %}{% assign ptitle = 'menswear' %}{% else %}{% assign ptitle = 'womenswear' %}{% endcase %}{{ ptitle }})
template = Liquid::Template.parse(code) template = Liquid::Template.parse(code)
assert_equal "menswear", template.render!("collection" => { 'handle' => 'menswear-jackets' }) assert_equal "menswear", template.render!("collection" => {'handle' => 'menswear-jackets'})
assert_equal "menswear", template.render!("collection" => { 'handle' => 'menswear-t-shirts' }) assert_equal "menswear", template.render!("collection" => {'handle' => 'menswear-t-shirts'})
assert_equal "womenswear", template.render!("collection" => { 'handle' => 'x' }) assert_equal "womenswear", template.render!("collection" => {'handle' => 'x'})
assert_equal "womenswear", template.render!("collection" => { 'handle' => 'y' }) assert_equal "womenswear", template.render!("collection" => {'handle' => 'y'})
assert_equal "womenswear", template.render!("collection" => { 'handle' => 'z' }) assert_equal "womenswear", template.render!("collection" => {'handle' => 'z'})
end end
def test_case_when_or def test_case_when_or
code = '{% case condition %}{% when 1 or 2 or 3 %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' code = '{% case condition %}{% when 1 or 2 or 3 %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}'
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 1 }) assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 1 })
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 2 }) assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 2 })
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 3 }) assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 3 })
assert_template_result(' its 4 ', code, { 'condition' => 4 }) assert_template_result(' its 4 ', code, {'condition' => 4 })
assert_template_result('', code, { 'condition' => 5 }) assert_template_result('', code, {'condition' => 5 })
code = '{% case condition %}{% when 1 or "string" or null %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' code = '{% case condition %}{% when 1 or "string" or null %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}'
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 1 }) assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 1 })
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 'string' }) assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 'string' })
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => nil }) assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => nil })
assert_template_result('', code, { 'condition' => 'something else' }) assert_template_result('', code, {'condition' => 'something else' })
end end
def test_case_when_comma def test_case_when_comma
code = '{% case condition %}{% when 1, 2, 3 %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' code = '{% case condition %}{% when 1, 2, 3 %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}'
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 1 }) assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 1 })
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 2 }) assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 2 })
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 3 }) assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 3 })
assert_template_result(' its 4 ', code, { 'condition' => 4 }) assert_template_result(' its 4 ', code, {'condition' => 4 })
assert_template_result('', code, { 'condition' => 5 }) assert_template_result('', code, {'condition' => 5 })
code = '{% case condition %}{% when 1, "string", null %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' code = '{% case condition %}{% when 1, "string", null %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}'
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 1 }) assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 1 })
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 'string' }) assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 'string' })
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => nil }) assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => nil })
assert_template_result('', code, { 'condition' => 'something else' }) assert_template_result('', code, {'condition' => 'something else' })
end end
def test_assign def test_assign
@@ -236,14 +236,15 @@ class StandardTagTest < Minitest::Test
assert_raises(SyntaxError) do assert_raises(SyntaxError) do
assert_template_result('', '{% case false %}{% huh %}true{% endcase %}', {}) assert_template_result('', '{% case false %}{% huh %}true{% endcase %}', {})
end end
end end
def test_cycle def test_cycle
assert_template_result('one', '{%cycle "one", "two"%}') assert_template_result('one','{%cycle "one", "two"%}')
assert_template_result('one two', '{%cycle "one", "two"%} {%cycle "one", "two"%}') assert_template_result('one two','{%cycle "one", "two"%} {%cycle "one", "two"%}')
assert_template_result(' two', '{%cycle "", "two"%} {%cycle "", "two"%}') assert_template_result(' two','{%cycle "", "two"%} {%cycle "", "two"%}')
assert_template_result('one two one', '{%cycle "one", "two"%} {%cycle "one", "two"%} {%cycle "one", "two"%}') assert_template_result('one two one','{%cycle "one", "two"%} {%cycle "one", "two"%} {%cycle "one", "two"%}')
assert_template_result('text-align: left text-align: right', assert_template_result('text-align: left text-align: right',
'{%cycle "text-align: left", "text-align: right" %} {%cycle "text-align: left", "text-align: right"%}') '{%cycle "text-align: left", "text-align: right" %} {%cycle "text-align: left", "text-align: right"%}')
@@ -260,18 +261,18 @@ class StandardTagTest < Minitest::Test
end end
def test_multiple_named_cycles_with_names_from_context def test_multiple_named_cycles_with_names_from_context
assigns = { "var1" => 1, "var2" => 2 } assigns = {"var1" => 1, "var2" => 2 }
assert_template_result('one one two two one one', assert_template_result('one one two two one one',
'{%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %} {%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %} {%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %}', assigns) '{%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %} {%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %} {%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %}', assigns)
end end
def test_size_of_array def test_size_of_array
assigns = { "array" => [1, 2, 3, 4] } assigns = {"array" => [1,2,3,4]}
assert_template_result('array has 4 elements', "array has {{ array.size }} elements", assigns) assert_template_result('array has 4 elements', "array has {{ array.size }} elements", assigns)
end end
def test_size_of_hash def test_size_of_hash
assigns = { "hash" => { a: 1, b: 2, c: 3, d: 4 } } assigns = {"hash" => {:a => 1, :b => 2, :c=> 3, :d => 4}}
assert_template_result('hash has 4 elements', "hash has {{ hash.size }} elements", assigns) assert_template_result('hash has 4 elements', "hash has {{ hash.size }} elements", assigns)
end end
@@ -283,11 +284,11 @@ class StandardTagTest < Minitest::Test
end end
def test_ifchanged def test_ifchanged
assigns = { 'array' => [ 1, 1, 2, 2, 3, 3] } assigns = {'array' => [ 1, 1, 2, 2, 3, 3] }
assert_template_result('123', '{%for item in array%}{%ifchanged%}{{item}}{% endifchanged %}{%endfor%}', assigns) assert_template_result('123','{%for item in array%}{%ifchanged%}{{item}}{% endifchanged %}{%endfor%}',assigns)
assigns = { 'array' => [ 1, 1, 1, 1] } assigns = {'array' => [ 1, 1, 1, 1] }
assert_template_result('1', '{%for item in array%}{%ifchanged%}{{item}}{% endifchanged %}{%endfor%}', assigns) assert_template_result('1','{%for item in array%}{%ifchanged%}{{item}}{% endifchanged %}{%endfor%}',assigns)
end end
def test_multiline_tag def test_multiline_tag

View File

@@ -37,6 +37,7 @@ class StatementsTest < Minitest::Test
text = ' {% if null <= 0 %} true {% else %} false {% endif %} ' text = ' {% if null <= 0 %} true {% else %} false {% endif %} '
assert_template_result ' false ', text assert_template_result ' false ', text
text = ' {% if 0 <= null %} true {% else %} false {% endif %} ' text = ' {% if 0 <= null %} true {% else %} false {% endif %} '
assert_template_result ' false ', text assert_template_result ' false ', text
end end
@@ -71,17 +72,18 @@ class StatementsTest < Minitest::Test
assert_template_result ' true ', text, 'var' => 'hello there!' assert_template_result ' true ', text, 'var' => 'hello there!'
end end
def test_var_and_long_string_are_equal_backwards def test_var_and_long_string_are_equal_backwards
text = " {% if 'hello there!' == var %} true {% else %} false {% endif %} " text = " {% if 'hello there!' == var %} true {% else %} false {% endif %} "
assert_template_result ' true ', text, 'var' => 'hello there!' assert_template_result ' true ', text, 'var' => 'hello there!'
end end
# def test_is_nil #def test_is_nil
# text = %| {% if var != nil %} true {% else %} false {% end %} | # text = %| {% if var != nil %} true {% else %} false {% end %} |
# @template.assigns = { 'var' => 'hello there!'} # @template.assigns = { 'var' => 'hello there!'}
# expected = %| true | # expected = %| true |
# assert_equal expected, @template.parse(text) # assert_equal expected, @template.parse(text)
# end #end
def test_is_collection_empty def test_is_collection_empty
text = ' {% if array == empty %} true {% else %} false {% endif %} ' text = ' {% if array == empty %} true {% else %} false {% endif %} '
@@ -90,7 +92,7 @@ class StatementsTest < Minitest::Test
def test_is_not_collection_empty def test_is_not_collection_empty
text = ' {% if array == empty %} true {% else %} false {% endif %} ' text = ' {% if array == empty %} true {% else %} false {% endif %} '
assert_template_result ' false ', text, 'array' => [1, 2, 3] assert_template_result ' false ', text, 'array' => [1,2,3]
end end
def test_nil def test_nil

View File

@@ -16,49 +16,48 @@ class TableRowTest < Minitest::Test
end end
def test_table_row def test_table_row
assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n", assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
'{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}', '{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}',
'numbers' => [1, 2, 3, 4, 5, 6]) 'numbers' => [1,2,3,4,5,6])
assert_template_result("<tr class=\"row1\">\n</tr>\n", assert_template_result("<tr class=\"row1\">\n</tr>\n",
'{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}', '{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}',
'numbers' => []) 'numbers' => [])
end end
def test_table_row_with_different_cols def test_table_row_with_different_cols
assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td><td class=\"col4\"> 4 </td><td class=\"col5\"> 5 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 6 </td></tr>\n", assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td><td class=\"col4\"> 4 </td><td class=\"col5\"> 5 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 6 </td></tr>\n",
'{% tablerow n in numbers cols:5%} {{n}} {% endtablerow %}', '{% tablerow n in numbers cols:5%} {{n}} {% endtablerow %}',
'numbers' => [1, 2, 3, 4, 5, 6]) 'numbers' => [1,2,3,4,5,6])
end end
def test_table_col_counter def test_table_col_counter
assert_template_result("<tr class=\"row1\">\n<td class=\"col1\">1</td><td class=\"col2\">2</td></tr>\n<tr class=\"row2\"><td class=\"col1\">1</td><td class=\"col2\">2</td></tr>\n<tr class=\"row3\"><td class=\"col1\">1</td><td class=\"col2\">2</td></tr>\n", assert_template_result("<tr class=\"row1\">\n<td class=\"col1\">1</td><td class=\"col2\">2</td></tr>\n<tr class=\"row2\"><td class=\"col1\">1</td><td class=\"col2\">2</td></tr>\n<tr class=\"row3\"><td class=\"col1\">1</td><td class=\"col2\">2</td></tr>\n",
'{% tablerow n in numbers cols:2%}{{tablerowloop.col}}{% endtablerow %}', '{% tablerow n in numbers cols:2%}{{tablerowloop.col}}{% endtablerow %}',
'numbers' => [1, 2, 3, 4, 5, 6]) 'numbers' => [1,2,3,4,5,6])
end end
def test_quoted_fragment def test_quoted_fragment
assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n", assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
"{% tablerow n in collections.frontpage cols:3%} {{n}} {% endtablerow %}", "{% tablerow n in collections.frontpage cols:3%} {{n}} {% endtablerow %}",
'collections' => { 'frontpage' => [1, 2, 3, 4, 5, 6] }) 'collections' => {'frontpage' => [1,2,3,4,5,6]})
assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n", assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
"{% tablerow n in collections['frontpage'] cols:3%} {{n}} {% endtablerow %}", "{% tablerow n in collections['frontpage'] cols:3%} {{n}} {% endtablerow %}",
'collections' => { 'frontpage' => [1, 2, 3, 4, 5, 6] }) 'collections' => {'frontpage' => [1,2,3,4,5,6]})
end end
def test_enumerable_drop def test_enumerable_drop
assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n", assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
'{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}', '{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}',
'numbers' => ArrayDrop.new([1, 2, 3, 4, 5, 6])) 'numbers' => ArrayDrop.new([1,2,3,4,5,6]))
end end
def test_offset_and_limit def test_offset_and_limit
assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n", assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
'{% tablerow n in numbers cols:3 offset:1 limit:6%} {{n}} {% endtablerow %}', '{% tablerow n in numbers cols:3 offset:1 limit:6%} {{n}} {% endtablerow %}',
'numbers' => [0, 1, 2, 3, 4, 5, 6, 7]) 'numbers' => [0,1,2,3,4,5,6,7])
end
def test_blank_string_not_iterable
assert_template_result("<tr class=\"row1\">\n</tr>\n", "{% tablerow char in characters cols:3 %}I WILL NOT BE OUTPUT{% endtablerow %}", 'characters' => '')
end end
end end

Some files were not shown because too many files have changed in this diff Show More