Compare commits

..

77 Commits

Author SHA1 Message Date
Maarten van Grootel
d7e7285fcb Fix leaking error when comparison leads to TypeError 2017-01-16 12:26:19 -05:00
Justin Li
ffb0ace303 Update changelog for 4.0.0 2016-12-16 13:11:22 -05:00
Florian Weingarten
ad00998ef8 bump to v4 2016-12-14 11:58:42 -05:00
Dylan Thacker-Smith
869dbc7ebf feature: Allow a default exception renderer to be specified (#837)
This could be used to preserve the old default of rendering
non-Liquid::Error messages or for providing default behaviour like error
reporting which could be missed if the exception renderer needed to be
specified on each render.
2016-12-12 10:29:09 -05:00
Dylan Thacker-Smith
fae3a2de7b Add version constraint to rake to fix CI (#836) 2016-12-09 14:01:15 -05:00
Dylan Thacker-Smith
f27bd619b9 change: Render an opaque internal error by default for non-Liquid::Error (#835)
These errors may contain sensitive information, so is safer to
render a more vague message by default.

This is done by replacing non-Liquid::Error exceptions with a
Liquid::InternalError exception with the non-Liquid::Error accessible on
through the cause method. This also allows the template name and line
number to be attached to the template errors.

The exception_handler render option has been changed to exception_renderer
since now it should raise an exception to re-raise on a liquid rendering
error or return a string to be rendered where the error occurred.
2016-12-07 17:34:29 -05:00
Dylan Thacker-Smith
a9b84b7806 test: Use ruby 2.1 in Circle CI 2016-12-05 15:36:42 -05:00
Dylan Thacker-Smith
6cc2c567c5 Merge pull request #832 from Shopify/drop-ruby-2.0
Drop support for ruby 2.0
2016-12-05 13:56:15 -05:00
Dylan Thacker-Smith
812e3c51b9 test: Add ruby 2.3.3 to CI
Travis doesn't have a ruby 2.3 alias, so the latest 2.3.x version is
specified instead.
2016-12-05 13:53:02 -05:00
Dylan Thacker-Smith
9dd0801f5c Drop support for ruby 2.0
It is no longer maintained upstream
2016-12-05 13:51:49 -05:00
Dylan Thacker-Smith
b146b49f46 fix: Clear the strainer cache when a global filter is added (#826) 2016-11-24 10:32:11 -05:00
Richard Monette
86944fe7b7 Merge pull request #809 from Shopify/introduce-unhandled-liquid-exception
introduce unhandled liquid exception
2016-10-31 10:20:06 -04:00
Richard Monette
a549d289d7 introduce unhandled liquid exception
check arity
2016-10-28 09:40:44 -04:00
Richard Monette
b2feeacbce Merge pull request #812 from Shopify/allow-split-to-accept-numeric
allow split to accept numeric
2016-10-26 10:59:44 -04:00
Richard Monette
143ba39a08 allow split to accept numeric 2016-10-26 10:43:04 -04:00
Richard Monette
43e59796f6 Merge pull request #805 from Shopify/dont-explode-when-sorting-nil-property
dont explode when sorting nil property
2016-10-05 10:18:16 -04:00
Richard Monette
bb3624b799 dont explode when sorting nil property 2016-10-04 13:22:29 -04:00
Konstantin Tennhard
64fca66ef5 Merge pull request #797 from Shopify/truncatewords-resiliency
Standard filter truncate / truncatewords: force truncate_string to string
2016-09-13 10:43:55 -04:00
Florian Weingarten
e9d7486758 4.0.0.rc3 2016-09-13 06:33:20 -04:00
Philibert Dugas
2bb98c1431 Merge pull request #798 from PhilibertDugas/bugfix-#697
Fixing #697 with better exception
2016-09-12 13:53:22 -04:00
Konstantin Tennhard
95d5c24bfc Standard filter truncate: truncate_string string coercion
The argument `truncate_string` is now coerced into a string to avoid
`NoMethodError`s. This is mostly for added resiliency. It is doubtful
that someone would actually intent to use a number as truncate string,
but accidentally supplying one is entirely possible.
2016-09-12 12:13:12 -04:00
Philibert Dugas
b7ee1a2176 Fixing #697 with better exception
When including a template which is not defined, the exception raised is
*undefined method `split` for nil:NilClass*

This occurs for a scenario like the following:
`{% include nil %}`
or
`{% include undefined-var %}`

Making the code raise an argument error to allow better understanding of
the include error
2016-09-12 09:31:59 -04:00
Florian Weingarten
0eca61a977 Merge pull request #799 from kainjow/patch-1
Update liquid-c
2016-09-12 08:12:14 -04:00
Kevin Wojniak
9bfd04da2d Update liquid-c 2016-09-10 09:23:15 -07:00
Konstantin Tennhard
302185a7fc Standard filter truncatewords: force truncate_string to string
Currently, `truncatewords` raises a TypeError when the argument
`truncate_string` is an interger. This PR forces string coercion for any
value provided for this argument. Thus,

```ruby
assert_equal 'one two1', @filters.truncatewords("one two three", 2, 1)
```

holds true. Another option would be to raise a `Liquid::ArgumentError`.

What is preferred?
2016-09-09 16:50:50 -04:00
Florian Weingarten
50c85afc35 Merge pull request #786 from Shopify/bump-liquid-c
Bump LiquidC for whitespace changes
2016-08-11 13:38:42 -04:00
Florian Weingarten
5876dff326 Bump LiquidC for whitespace changes 2016-08-11 13:21:39 -04:00
Florian Weingarten
f25185631d Merge pull request #773 from evulse/whitespace-trim
Add whitespace control character and associated tests
2016-08-11 13:20:12 -04:00
Michael Angell
283f1bad18 Use .last instead of pop push method for updating last node in nodelist 2016-07-08 20:49:30 +10:00
Michael Angell
e1d40c7d89 Add whitespace control character and associated tests 2016-06-28 09:15:45 +10:00
Justin Li
19c6eb426a Merge pull request #769 from zacstewart/patch-1
Fix doc formatting of code examples in file_system
2016-06-15 17:10:11 -04:00
Zac Stewart
f87b06095d Fix doc formatting of code examples in file_system
These code examples are being rendered as paragraph text in the docs.
2016-06-15 15:34:14 -04:00
Gaurav Chande
b81d54e789 Merge pull request #761 from Shopify/range-to_liquid
Support Range Type
2016-06-02 16:48:30 -04:00
Gaurav Chande
00f53b16e8 Prevent Range usage in templates from blowing up 2016-06-02 16:38:44 -04:00
Gaurav Chande
e4cf55b112 Merge pull request #748 from Shopify/expose-tags
Make Template.tags loop-able
2016-04-25 11:59:37 -04:00
Gaurav Chande
5bb211d933 Ensure no tag leakage since registry is global 2016-04-25 11:50:46 -04:00
Gaurav Chande
6adc431a19 Make tag registry enumerable 2016-04-25 11:38:42 -04:00
Justin Li
23d2beed41 Merge pull request #744 from Shopify/raw-syntax-method
Make markup validation a method on Liquid::Raw
2016-04-13 17:08:02 -04:00
Drew Martin
a80ecb7678 make markup validation a method on Liquid::Raw 2016-04-13 14:52:30 -04:00
Florian Weingarten
361c695264 Merge pull request #736 from Shopify/abs-filter
Abs filter
2016-04-05 09:13:56 -04:00
Florian Weingarten
f93243cc1a abs filter 2016-04-04 09:32:31 -04:00
Florian Weingarten
1e533a52e7 Merge pull request #735 from Shopify/fix-to-number-for-negative-float-strings
Fix to_number filter for negative float strings
2016-03-31 15:52:51 -04:00
Dylan Thacker-Smith
3ea84f095f Merge pull request #734 from Shopify/concat-liquid-error
Raise a Liquid::Error when a non-array is passed into the concat filter.
2016-03-31 15:47:43 -04:00
Dylan Thacker-Smith
4239c899a4 Raise a Liquid::Error when a non-array is passed into the concat filter. 2016-03-31 15:47:06 -04:00
Florian Weingarten
1597f8859f Fix to_number filter for negative float strings 2016-03-31 09:18:55 -04:00
Florian Weingarten
b3dda384c9 Merge pull request #733 from Shopify/fix-some-ruby-warnings
Fix a bunch of Ruby warnings
2016-03-30 17:09:00 -04:00
Florian Weingarten
6828670bfe Merge pull request #732 from Shopify/v400rc2
Release v4.0.0rc2
2016-03-30 17:02:34 -04:00
Florian Weingarten
d2f16d92d6 Fix a bunch of Ruby warnings 2016-03-30 20:42:30 +00:00
Justin Li
d233acb483 Update history to reflect merge of #731 2016-03-30 16:36:57 -04:00
Florian Weingarten
8920e2a2a2 Release v4.0.0rc2 2016-03-30 20:13:21 +00:00
Justin Li
bfee507005 Merge pull request #731 from ismasan/duck_typed_maths_filters
Duck typed maths filters
2016-03-30 16:09:16 -04:00
Ismael Celis
929c89789f Test that all maths filters work with duck-typed #to_number 2016-03-30 13:35:09 -03:00
Ismael Celis
d03c4ae8e8 Allow Utils.to_number to work with anything that responds to #to_number 2016-03-30 01:57:21 -03:00
Justin Li
021bafd260 Merge pull request #725 from jeroenvisser101/performance-start-with-vs-regex
Use start_with? instead of Regex
2016-03-21 10:34:28 -04:00
Jeroen Visser
04c393ab07 Use start_with? instead of Regex
Performance is increased by doing this:

  require 'benchmark'
  require 'tempfile'

  n = 50000
  test = File.expand_path(Tempfile.new('foo'))
  Benchmark.bm(20) do |x|
    x.report("Regex:") do
      n.times { test =~ /\A#{test}/ }
    end
    x.report("String#start_with?:") do
      n.times { test =~ test.start_with?(test) }
    end
  end

Benchmark result:
                             user     system      total        real
  Regex:                 0.440000   0.010000   0.450000 (  0.447357)
  String#start_with?:    0.000000   0.000000   0.000000 (  0.006313)
2016-03-21 14:23:35 +01:00
Gaurav Chande
9a7778e52c Merge pull request #707 from Shopify/drop-without-context
@context not always present on a Drop
2016-03-01 18:07:16 -05:00
Gaurav Chande
dde00253f9 context is not always present on a Drop 2016-03-01 21:22:11 +00:00
Gaurav Chande
18d1644980 Merge pull request #705 from Shopify/register-filter-warn
Strainer#add_filter Raises on Private Override
2016-02-24 15:31:41 -05:00
Gaurav Chande
c424d47274 Add changelog entry for Strainer method override change 2016-02-24 20:23:57 +00:00
Gaurav Chande
8e6b9d503d Make Strainer also raise when registered method is overriden as protected 2016-02-24 20:23:49 +00:00
Gaurav Chande
8be38d1795 Strainer#add_filter should raise when registered method is overriden as private 2016-02-24 20:03:17 +00:00
Justin Li
3146d5c3f2 Grammatic and other fixes to CONTRIBUTING.md 2016-02-02 23:45:37 -05:00
Justin Li
0cc8b68a97 Make logic in Context#lookup_and_evaluate more understandable 2016-02-02 23:22:46 -05:00
Justin Li
5a50c12953 Update history to reflect merge of #691
[ci skip]
2016-02-02 23:14:41 -05:00
Justin Li
a6fa4c5c38 Merge pull request #691 from urbandictionary/missing_variables_and_filters
Merge pull request 691
2016-02-02 23:13:44 -05:00
Ivan Kuznetsov
dadd9b4dd2 Add strict_variables/strict_filters render options to check for undefined variables and filters 2016-02-03 10:49:33 +07:00
Justin Li
6434b8d2bb Merge pull request #696 from Shopify/no-send
Remove possibility for arbitrary sends
2016-02-02 11:01:46 -05:00
Justin Li
2d891ddd8f Merge pull request #695 from Shopify/assign-score
Take nested values into account for assign score
2016-02-01 13:14:40 -05:00
Justin Li
60b508b151 Use #each to avoid extra allocations 2016-02-01 13:01:25 -05:00
Justin Li
3891f14a1a Take nested values into account for assign score 2016-02-01 13:01:25 -05:00
Justin Li
198f0aa366 Add test for nested assign score bookkeeping 2016-02-01 13:01:23 -05:00
Justin Li
f2e6adf566 Remove arbitrary send vector 2016-02-01 11:38:40 -05:00
Justin Li
08de6ed2c5 Merge pull request #687 from pathawks/default
Performance improvement: `default` filter
2016-01-24 11:34:05 -05:00
Pat Hawks
7e322f5cf8 Performance improvement: default filter 2016-01-23 23:18:51 -08:00
Justin Li
bf86a5a069 Merge pull request #688 from Shopify/gmp
Install libgmp3-dev in travis
2016-01-23 21:46:37 -05:00
Justin Li
0141444814 Install libgmp3-dev in travis 2016-01-23 21:41:14 -05:00
Justin Li
6d30226768 Update changelog for 4.0.0rc1 2016-01-08 15:08:06 -05:00
40 changed files with 1150 additions and 204 deletions

View File

@@ -1,15 +1,20 @@
language: ruby
rvm:
- 2.0
- 2.1
- 2.2
- 2.3.3
- ruby-head
- jruby-head
# - rbx-2
sudo: false
addons:
apt:
packages:
- libgmp3-dev
matrix:
allow_failures:
- rvm: jruby-head

View File

@@ -4,23 +4,22 @@
* Bugfixes
* Performance improvements
* Features which are likely to be useful to the majority of Liquid users
* Features that are likely to be useful to the majority of Liquid users
## Things we won't merge
* Code which introduces considerable performance degrations
* Code which touches performance critical parts of Liquid and comes without benchmarks
* Features which are not important for most people (we want to keep the core Liquid code small and tidy)
* Features which can easily be implemented on top of Liquid (for example as a custom filter or custom filesystem)
* Code which comes without tests
* Code which breaks existing tests
* Code that introduces considerable performance degrations
* Code that touches performance-critical parts of Liquid and comes without benchmarks
* Features that are not important for most people (we want to keep the core Liquid code small and tidy)
* Features that can easily be implemented on top of Liquid (for example as a custom filter or custom filesystem)
* Code that does not include tests
* Code that breaks existing tests
## Workflow
* Fork the Liquid repository
* Create a new branch in your fork
* If it makes sense, add tests for your code and run a performance benchmark
* Make sure all tests pass
* If it makes sense, add tests for your code and/or run a performance benchmark
* Make sure all tests pass (`bundle exec rake`)
* Create a pull request
* In the description, ping one of [@boourns](https://github.com/boourns), [@fw42](https://github.com/fw42), [@camilo](https://github.com/camilo), [@dylanahsmith](https://github.com/dylanahsmith), or [@arthurnn](https://github.com/arthurnn) and ask for a code review.

View File

@@ -9,6 +9,6 @@ group :test do
gem 'rubocop', '0.34.2'
platform :mri do
gem 'liquid-c', github: 'Shopify/liquid-c', ref: '2570693d8d03faa0df9160ec74348a7149436df3'
gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'bd53db95de3d44d631e7c5a267c3d934e66107dd'
end
end

View File

@@ -1,8 +1,14 @@
# Liquid Change Log
## 4.0.0 / not yet released / branch "master"
## 4.0.0 / 2016-12-14 / branch "4-0-stable"
### Changed
* Render an opaque internal error by default for non-Liquid::Error (#835) [Dylan Thacker-Smith]
* Ruby 2.0 support dropped (#832) [Dylan Thacker-Smith]
* Add to_number Drop method to allow custom drops to work with number filters (#731)
* Add strict_variables and strict_filters options to detect undefined references (#691)
* Improve loop performance (#681) [Florian Weingarten]
* Rename Drop method `before_method` to `liquid_method_missing` (#661) [Thierry Joyal]
* Add url_decode filter to invert url_encode (#645) [Larry Archer]
* Add global_filter to apply a filter to all output (#610) [Loren Hale]
* Add compact filter (#600) [Carson Reinke]
@@ -15,9 +21,12 @@
* Ruby 1.9 support dropped (#491) [Justin Li]
* Liquid::Template.file_system's read_template_file method is no longer passed the context. (#441) [James Reid-Smith]
* Remove support for `liquid_methods`
* Rename Drop method `before_method` as `liquid_method_missing` (#661) [Thierry Joyal]
* Liquid::Template.register_filter raises when the module overrides registered public methods as private or protected (#705) [Gaurav Chande]
### Fixed
* 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]

View File

@@ -73,3 +73,34 @@ This is useful for doing things like enabling strict mode only in the theme edit
It is recommended that you enable `:strict` or `:warn` mode on new apps to stop invalid templates from being created.
It is also recommended that you use it in the template editors of existing apps to give editors better error messages.
### Undefined variables and filters
By default, the renderer doesn't raise or in any other way notify you if some variables or filters are missing, i.e. not passed to the `render` method.
You can improve this situation by passing `strict_variables: true` and/or `strict_filters: true` options to the `render` method.
When one of these options is set to true, all errors about undefined variables and undefined filters will be stored in `errors` array of a `Liquid::Template` instance.
Here are some examples:
```ruby
template = Liquid::Template.parse("{{x}} {{y}} {{z.a}} {{z.b}}")
template.render({ 'x' => 1, 'z' => { 'a' => 2 } }, { strict_variables: true })
#=> '1 2 ' # when a variable is undefined, it's rendered as nil
template.errors
#=> [#<Liquid::UndefinedVariable: Liquid error: undefined variable y>, #<Liquid::UndefinedVariable: Liquid error: undefined variable b>]
```
```ruby
template = Liquid::Template.parse("{{x | filter1 | upcase}}")
template.render({ 'x' => 'foo' }, { strict_filters: true })
#=> '' # when at least one filter in the filter chain is undefined, a whole expression is rendered as nil
template.errors
#=> [#<Liquid::UndefinedFilter: Liquid error: undefined filter filter1>]
```
If you want to raise on a first exception instead of pushing all of them in `errors`, you can use `render!` method:
```ruby
template = Liquid::Template.parse("{{x}} {{y}}")
template.render!({ 'x' => 1}, { strict_variables: true })
#=> Liquid::UndefinedVariable: Liquid error: undefined variable y
```

View File

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

View File

@@ -24,6 +24,7 @@ module Liquid
ArgumentSeparator = ','.freeze
FilterArgumentSeparator = ':'.freeze
VariableAttributeSeparator = '.'.freeze
WhitespaceControl = '-'.freeze
TagStart = /\{\%/
TagEnd = /\%\}/
VariableSignature = /\(?[\w\-\.\[\]]\)?/
@@ -34,7 +35,7 @@ module Liquid
QuotedString = /"[^"]*"|'[^']*'/
QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o
TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/o
AnyStartingTag = /\{\{|\{\%/
AnyStartingTag = /#{TagStart}|#{VariableStart}/o
PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om
VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o

View File

@@ -1,7 +1,7 @@
module Liquid
class BlockBody
FullToken = /\A#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
ContentOfVariable = /\A#{VariableStart}(.*)#{VariableEnd}\z/om
FullToken = /\A#{TagStart}#{WhitespaceControl}?\s*(\w+)\s*(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
TAGSTART = "{%".freeze
VARSTART = "{{".freeze
@@ -18,6 +18,7 @@ module Liquid
unless token.empty?
case
when token.start_with?(TAGSTART)
whitespace_handler(token, parse_context)
if token =~ FullToken
tag_name = $1
markup = $2
@@ -35,9 +36,14 @@ module Liquid
raise_missing_tag_terminator(token, parse_context)
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
@@ -48,6 +54,16 @@ module Liquid
yield nil, nil
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?
@blank
end
@@ -76,8 +92,11 @@ module Liquid
end
rescue MemoryError => e
raise e
rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
context.handle_error(e, token.line_number, token.raw)
output << nil
rescue ::StandardError => e
output << context.handle_error(e, token.line_number)
output << context.handle_error(e, token.line_number, token.raw)
end
end

View File

@@ -113,7 +113,7 @@ module Liquid
elsif left.respond_to?(operation) && right.respond_to?(operation)
begin
left.send(operation, right)
rescue ::ArgumentError => e
rescue ::ArgumentError, TypeError => e
raise Liquid::ArgumentError.new(e.message)
end
end

View File

@@ -13,7 +13,7 @@ module Liquid
# context['bob'] #=> nil class Context
class Context
attr_reader :scopes, :errors, :registers, :environments, :resource_limits
attr_accessor :exception_handler, :template_name, :partial, :global_filter
attr_accessor :exception_renderer, :template_name, :partial, :global_filter, :strict_variables, :strict_filters
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil)
@environments = [environments].flatten
@@ -21,13 +21,15 @@ module Liquid
@registers = registers
@errors = []
@partial = false
@strict_variables = false
@resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
squash_instance_assigns_with_environments
@this_stack_used = false
self.exception_renderer = Template.default_exception_renderer
if rethrow_errors
self.exception_handler = ->(e) { raise }
self.exception_renderer = ->(e) { raise }
end
@interrupts = []
@@ -72,31 +74,12 @@ module Liquid
@interrupts.pop
end
def handle_error(e, line_number = nil)
if e.is_a?(Liquid::Error)
e.template_name ||= template_name
e.line_number ||= line_number
end
output = nil
if exception_handler
result = exception_handler.call(e)
case result
when Exception
e = result
if e.is_a?(Liquid::Error)
e.template_name ||= template_name
e.line_number ||= line_number
end
when String
output = result
else
raise if result
end
end
def handle_error(e, line_number = nil, raw_token = nil)
e = internal_error unless e.is_a?(Liquid::Error)
e.template_name ||= template_name
e.line_number ||= line_number
errors.push(e)
output || Liquid::Error.render(e)
exception_renderer.call(e).to_s
end
def invoke(method, *args)
@@ -205,7 +188,13 @@ module Liquid
end
def lookup_and_evaluate(obj, key)
if (value = obj[key]).is_a?(Proc) && obj.respond_to?(:[]=)
if @strict_variables && obj.respond_to?(:key?) && !obj.key?(key)
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)
else
value
@@ -214,6 +203,13 @@ module Liquid
private
def internal_error
# raise and catch to set backtrace and cause on exception
raise Liquid::InternalError, 'internal'
rescue Liquid::InternalError => exc
exc
end
def squash_instance_assigns_with_environments
@scopes.last.each_key do |k|
@environments.each do |env|

View File

@@ -24,8 +24,9 @@ module Liquid
attr_writer :context
# Catch all for the method
def liquid_method_missing(_method)
nil
def liquid_method_missing(method)
return nil unless @context && @context.strict_variables
raise Liquid::UndefinedDropMethod, "undefined method #{method}"
end
# called by liquid to invoke a drop
@@ -61,16 +62,17 @@ module Liquid
end
def self.invokable_methods
unless @invokable_methods
@invokable_methods ||= begin
blacklist = Liquid::Drop.public_instance_methods + [:each]
if include?(Enumerable)
blacklist += Enumerable.public_instance_methods
blacklist -= [:sort, :count, :first, :min, :max, :include?]
end
whitelist = [:to_liquid] + (public_instance_methods - blacklist)
@invokable_methods = Set.new(whitelist.map(&:to_s))
Set.new(whitelist.map(&:to_s))
end
@invokable_methods
end
end
end

View File

@@ -17,14 +17,6 @@ module Liquid
str
end
def self.render(e)
if e.is_a?(Liquid::Error)
e.to_s
else
"Liquid error: #{e}"
end
end
private
def message_prefix
@@ -56,4 +48,9 @@ module Liquid
MemoryError = Class.new(Error)
ZeroDivisionError = Class.new(Error)
FloatDomainError = Class.new(Error)
UndefinedVariable = Class.new(Error)
UndefinedDropMethod = Class.new(Error)
UndefinedFilter = Class.new(Error)
MethodOverrideError = Class.new(Error)
InternalError = Class.new(Error)
end

View File

@@ -25,6 +25,12 @@ class Numeric # :nodoc:
end
end
class Range # :nodoc:
def to_liquid
self
end
end
class Time # :nodoc:
def to_liquid
self

View File

@@ -8,8 +8,8 @@ module Liquid
#
# Example:
#
# Liquid::Template.file_system = Liquid::LocalFileSystem.new(template_path)
# liquid = Liquid::Template.parse(template)
# Liquid::Template.file_system = Liquid::LocalFileSystem.new(template_path)
# liquid = Liquid::Template.parse(template)
#
# This will parse the template with a LocalFileSystem implementation rooted at 'template_path'.
class BlankFileSystem
@@ -26,10 +26,10 @@ module Liquid
#
# Example:
#
# file_system = Liquid::LocalFileSystem.new("/some/path")
# file_system = Liquid::LocalFileSystem.new("/some/path")
#
# file_system.full_path("mypartial") # => "/some/path/_mypartial.liquid"
# file_system.full_path("dir/mypartial") # => "/some/path/dir/_mypartial.liquid"
# file_system.full_path("mypartial") # => "/some/path/_mypartial.liquid"
# file_system.full_path("dir/mypartial") # => "/some/path/dir/_mypartial.liquid"
#
# Optionally in the second argument you can specify a custom pattern for template filenames.
# The Kernel::sprintf format specification is used.
@@ -37,9 +37,9 @@ module Liquid
#
# Example:
#
# file_system = Liquid::LocalFileSystem.new("/some/path", "%s.html")
# file_system = Liquid::LocalFileSystem.new("/some/path", "%s.html")
#
# file_system.full_path("index") # => "/some/path/index.html"
# file_system.full_path("index") # => "/some/path/index.html"
#
class LocalFileSystem
attr_accessor :root
@@ -65,7 +65,7 @@ module Liquid
File.join(root, @pattern % template_path)
end
raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path) =~ /\A#{File.expand_path(root)}/
raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path).start_with?(File.expand_path(root))
full_path
end

View File

@@ -22,3 +22,5 @@
tag_never_closed: "'%{block_name}' tag was never closed"
meta_syntax_error: "Liquid syntax error: #{e.message}"
table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
argument:
include: "Argument error in tag 'include' - Illegal template name"

View File

@@ -1,7 +1,7 @@
module Liquid
class ParseContext
attr_accessor :partial, :locale, :line_number
attr_reader :warnings, :error_mode
attr_accessor :locale, :line_number, :trim_whitespace
attr_reader :partial, :warnings, :error_mode
def initialize(options = {})
@template_options = options ? options.dup : {}

View File

@@ -65,9 +65,10 @@ module Liquid
return if input.nil?
input_str = input.to_s
length = Utils.to_integer(length)
l = length - truncate_string.length
truncate_string_str = truncate_string.to_s
l = length - truncate_string_str.length
l = 0 if l < 0
input_str.length > length ? input_str[0...l] + truncate_string : input_str
input_str.length > length ? input_str[0...l] + truncate_string_str : input_str
end
def truncatewords(input, words = 15, truncate_string = "...".freeze)
@@ -76,7 +77,7 @@ module Liquid
words = Utils.to_integer(words)
l = words - 1
l = 0 if l < 0
wordlist.length > l ? wordlist[0..l].join(" ".freeze) + truncate_string : input
wordlist.length > l ? wordlist[0..l].join(" ".freeze) + truncate_string.to_s : input
end
# Split input string into an array of substrings separated by given pattern.
@@ -85,7 +86,7 @@ module Liquid
# <div class="summary">{{ post | split '//' | first }}</div>
#
def split(input, pattern)
input.to_s.split(pattern)
input.to_s.split(pattern.to_s)
end
def strip(input)
@@ -124,9 +125,15 @@ module Liquid
elsif ary.empty? # The next two cases assume a non-empty array.
[]
elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
ary.sort { |a, b| a[property] <=> b[property] }
elsif ary.first.respond_to?(property)
ary.sort { |a, b| a.send(property) <=> b.send(property) }
ary.sort do |a, b|
a = a[property]
b = b[property]
if a && b
a <=> b
else
a ? -1 : 1
end
end
end
end
@@ -141,8 +148,6 @@ module Liquid
[]
elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
ary.sort { |a, b| a[property].casecmp(b[property]) }
elsif ary.first.respond_to?(property)
ary.sort { |a, b| a.send(property).casecmp(b.send(property)) }
end
end
@@ -191,8 +196,6 @@ module Liquid
[]
elsif ary.first.respond_to?(:[])
ary.reject{ |a| a[property].nil? }
elsif ary.first.respond_to?(property)
ary.reject { |a| a.send(property).nil? }
end
end
@@ -222,6 +225,9 @@ module Liquid
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
@@ -292,6 +298,12 @@ module Liquid
array.last if array.respond_to?(:last)
end
# absolute value
def abs(input)
result = Utils.to_number(input).abs
result.is_a?(BigDecimal) ? result.to_f : result
end
# addition
def plus(input, operand)
apply_operation(input, operand, :+)
@@ -341,9 +353,12 @@ module Liquid
raise Liquid::FloatDomainError, e.message
end
def default(input, default_value = "".freeze)
is_blank = input.respond_to?(:empty?) ? input.empty? : !input
is_blank ? default_value : input
def default(input, default_value = ''.freeze)
if !input || input.respond_to?(:empty?) && input.empty?
default_value
else
input
end
end
private
@@ -373,7 +388,7 @@ module Liquid
end
def concat(args)
to_a.concat args
to_a.concat(args)
end
def reverse

View File

@@ -26,14 +26,20 @@ module Liquid
end
def self.add_filter(filter)
raise ArgumentError, "Expected module but got: #{f.class}" unless filter.is_a?(Module)
raise ArgumentError, "Expected module but got: #{filter.class}" unless filter.is_a?(Module)
unless self.class.include?(filter)
send(:include, filter)
@filter_methods.merge(filter.public_instance_methods.map(&:to_s))
invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) }
if invokable_non_public_methods.any?
raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}"
else
send(:include, filter)
@filter_methods.merge(filter.public_instance_methods.map(&:to_s))
end
end
end
def self.global_filter(filter)
@@strainer_class_cache.clear
@@global_strainer.add_filter(filter)
end
@@ -48,6 +54,8 @@ module Liquid
def invoke(method, *args)
if self.class.invokable?(method)
send(method, *args)
elsif @context && @context.strict_filters
raise Liquid::UndefinedFilter, "undefined filter #{method}"
else
args.first
end

View File

@@ -23,16 +23,28 @@ module Liquid
def render(context)
val = @from.render(context)
context.scopes.last[@to] = val
inc = val.instance_of?(String) || val.instance_of?(Array) || val.instance_of?(Hash) ? val.length : 1
context.resource_limits.assign_score += inc
context.resource_limits.assign_score += assign_score_of(val)
''.freeze
end
def blank?
true
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
Template.register_tag('assign'.freeze, Assign)

View File

@@ -48,8 +48,10 @@ module Liquid
def initialize(tag_name, markup, options)
super
@from = @limit = nil
parse_with_selected_parser(markup)
@for_block = BlockBody.new
@else_block = nil
end
def parse(tokens)

View File

@@ -42,8 +42,9 @@ module Liquid
def render(context)
template_name = context.evaluate(@template_name_expr)
partial = load_cached_partial(template_name, context)
raise ArgumentError.new(options[:locale].t("errors.argument.include")) unless template_name
partial = load_cached_partial(template_name, context)
context_variable_name = template_name.split('/'.freeze).last
variable = if @variable_name_expr

View File

@@ -6,9 +6,7 @@ module Liquid
def initialize(tag_name, markup, parse_context)
super
unless markup =~ Syntax
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_unexpected_args".freeze, tag: tag_name))
end
ensure_valid_markup(tag_name, markup, parse_context)
end
def parse(tokens)
@@ -35,6 +33,14 @@ module Liquid
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
Template.register_tag('raw'.freeze, Raw)

View File

@@ -19,8 +19,10 @@ module Liquid
@@file_system = BlankFileSystem.new
class TagRegistry
include Enumerable
def initialize
@tags = {}
@tags = {}
@cache = {}
end
@@ -41,6 +43,10 @@ module Liquid
@cache.delete(tag_name)
end
def each(&block)
@tags.each(&block)
end
private
def lookup_class(name)
@@ -63,6 +69,11 @@ module Liquid
# :error raises an error when tainted output is used
attr_writer :taint_mode
attr_accessor :default_exception_renderer
Template.default_exception_renderer = lambda do |exception|
exception
end
def file_system
@@file_system
end
@@ -80,11 +91,11 @@ module Liquid
end
def error_mode
@error_mode || :lax
@error_mode ||= :lax
end
def taint_mode
@taint_mode || :lax
@taint_mode ||= :lax
end
# Pass a module with filter methods which should be available
@@ -107,6 +118,7 @@ module Liquid
end
def initialize
@rethrow_errors = false
@resource_limits = ResourceLimits.new(self.class.default_resource_limits)
end
@@ -160,7 +172,7 @@ module Liquid
c = args.shift
if @rethrow_errors
c.exception_handler = ->(e) { raise }
c.exception_renderer = ->(e) { raise }
end
c
@@ -181,12 +193,7 @@ module Liquid
registers.merge!(options[:registers]) if options[:registers].is_a?(Hash)
context.add_filters(options[:filters]) if options[:filters]
context.global_filter = options[:global_filter] if options[:global_filter]
context.exception_handler = options[:exception_handler] if options[:exception_handler]
apply_options_to_context(context, options)
when Module, Array
context.add_filters(args.pop)
end
@@ -235,5 +242,13 @@ module Liquid
yield
end
end
def apply_options_to_context(context, options)
context.add_filters(options[:filters]) if options[:filters]
context.global_filter = options[:global_filter] if options[:global_filter]
context.exception_renderer = options[:exception_renderer] if options[:exception_renderer]
context.strict_variables = options[:strict_variables] if options[:strict_variables]
context.strict_filters = options[:strict_filters] if options[:strict_filters]
end
end
end

View File

@@ -4,7 +4,7 @@ module Liquid
def initialize(source, line_numbers = false)
@source = source
@line_number = 1 if line_numbers
@line_number = line_numbers ? 1 : nil
@tokens = tokenize
end

View File

@@ -50,9 +50,13 @@ module Liquid
when Numeric
obj
when String
(obj.strip =~ /\A\d+\.\d+\z/) ? BigDecimal.new(obj) : obj.to_i
(obj.strip =~ /\A-?\d+\.\d+\z/) ? BigDecimal.new(obj) : obj.to_i
else
0
if obj.respond_to?(:to_number)
obj.to_number
else
0
end
end
end

View File

@@ -55,9 +55,11 @@ module Liquid
object = object.send(key).to_liquid
# No key was present with the desired value and it wasn't one of the directly supported
# keywords either. The only thing we got left is to return nil
# keywords either. The only thing we got left is to return nil or
# raise an exception if `strict_variables` option is set to true
else
return nil
return nil unless context.strict_variables
raise Liquid::UndefinedVariable, "undefined variable #{key}"
end
# If we are dealing with a drop here we have to

View File

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

View File

@@ -15,6 +15,7 @@ Gem::Specification.new do |s|
s.license = "MIT"
# s.description = "A secure, non-evaling end user template engine with aesthetic markup."
s.required_ruby_version = ">= 2.1.0"
s.required_rubygems_version = ">= 1.3.7"
s.test_files = Dir.glob("{test}/**/*")
@@ -24,6 +25,6 @@ Gem::Specification.new do |s|
s.require_path = "lib"
s.add_development_dependency 'rake'
s.add_development_dependency 'rake', '~> 11.3'
s.add_development_dependency 'minitest'
end

View File

@@ -94,7 +94,23 @@ class ErrorHandlingTest < Minitest::Test
)
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
def test_parsing_warn_with_line_numbers_adds_numbers_to_lexer_errors
@@ -186,22 +202,40 @@ class ErrorHandlingTest < Minitest::Test
end
end
def test_exception_handler_with_string_result
template = Liquid::Template.parse('This is an argument error: {{ errors.argument_error }}')
output = template.render({ 'errors' => ErrorDrop.new }, exception_handler: ->(e) { '' })
assert_equal 'This is an argument error: ', output
assert_equal [ArgumentError], template.errors.map(&:class)
end
class InternalError < Liquid::Error
end
def test_exception_handler_with_exception_result
def test_default_exception_renderer_with_internal_error
template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true)
handler = ->(e) { e.is_a?(Liquid::Error) ? e : InternalError.new('internal') }
output = template.render({ 'errors' => ErrorDrop.new }, exception_handler: handler)
output = template.render({ 'errors' => ErrorDrop.new })
assert_equal 'This is a runtime error: Liquid error (line 1): internal', output
assert_equal [InternalError], template.errors.map(&:class)
assert_equal [Liquid::InternalError], template.errors.map(&:class)
end
def test_setting_default_exception_renderer
old_exception_renderer = Liquid::Template.default_exception_renderer
exceptions = []
Liquid::Template.default_exception_renderer = ->(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

View File

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

View File

@@ -9,6 +9,10 @@ end
class SecurityTest < Minitest::Test
include Liquid
def setup
@assigns = {}
end
def test_no_instance_eval
text = %( {{ '1+1' | instance_eval }} )
expected = %( 1+1 )

View File

@@ -41,6 +41,16 @@ class TestEnumerable < Liquid::Drop
end
end
class NumberLikeThing < Liquid::Drop
def initialize(amount)
@amount = amount
end
def to_number
@amount
end
end
class StandardFiltersTest < Minitest::Test
include Liquid
@@ -105,15 +115,15 @@ class StandardFiltersTest < Minitest::Test
assert_equal '...', @filters.truncate('1234567890', 0)
assert_equal '1234567890', @filters.truncate('1234567890')
assert_equal "测试...", @filters.truncate("测试测试测试测试", 5)
assert_equal '12341', @filters.truncate("1234567890", 5, 1)
end
def test_split
assert_equal ['12', '34'], @filters.split('12~34', '~')
assert_equal ['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 ['A', 'Z'], @filters.split('A1Z', 1)
end
def test_escape
@@ -144,6 +154,7 @@ class StandardFiltersTest < Minitest::Test
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 "测试测试测试测试", @filters.truncatewords('测试测试测试测试', 5)
assert_equal 'one two1', @filters.truncatewords("one two three", 2, 1)
end
def test_strip_html
@@ -166,6 +177,24 @@ class StandardFiltersTest < Minitest::Test
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
@@ -364,20 +393,38 @@ class StandardFiltersTest < Minitest::Test
def test_plus
assert_template_result "2", "{{ 1 | plus:1 }}"
assert_template_result "2.0", "{{ '1' | plus:'1.0' }}"
assert_template_result "5", "{{ price | plus:'2' }}", 'price' => NumberLikeThing.new(3)
end
def test_minus
assert_template_result "4", "{{ input | minus:operand }}", 'input' => 5, 'operand' => 1
assert_template_result "2.3", "{{ '4.3' | minus:'2' }}"
assert_template_result "5", "{{ price | minus:'2' }}", 'price' => NumberLikeThing.new(7)
end
def test_abs
assert_template_result "17", "{{ 17 | abs }}"
assert_template_result "17", "{{ -17 | abs }}"
assert_template_result "17", "{{ '17' | abs }}"
assert_template_result "17", "{{ '-17' | abs }}"
assert_template_result "0", "{{ 0 | abs }}"
assert_template_result "0", "{{ '0' | abs }}"
assert_template_result "17.42", "{{ 17.42 | abs }}"
assert_template_result "17.42", "{{ -17.42 | abs }}"
assert_template_result "17.42", "{{ '17.42' | abs }}"
assert_template_result "17.42", "{{ '-17.42' | abs }}"
end
def test_times
assert_template_result "12", "{{ 3 | times:4 }}"
assert_template_result "0", "{{ 'foo' | times:4 }}"
assert_template_result "6", "{{ '2.1' | times:3 | replace: '.','-' | plus:0}}"
assert_template_result "7.25", "{{ 0.0725 | times:100 }}"
assert_template_result "-7.25", '{{ "-0.0725" | times:100 }}'
assert_template_result "7.25", '{{ "-0.0725" | times: -100 }}'
assert_template_result "4", "{{ price | times:2 }}", 'price' => NumberLikeThing.new(2)
end
def test_divided_by
@@ -391,6 +438,8 @@ class StandardFiltersTest < Minitest::Test
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
def test_modulo
@@ -398,6 +447,8 @@ class StandardFiltersTest < Minitest::Test
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
def test_round
@@ -407,6 +458,9 @@ class StandardFiltersTest < Minitest::Test
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
def test_ceil
@@ -415,6 +469,8 @@ class StandardFiltersTest < Minitest::Test
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
def test_floor
@@ -423,6 +479,8 @@ class StandardFiltersTest < Minitest::Test
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
def test_append
@@ -436,8 +494,7 @@ class StandardFiltersTest < Minitest::Test
assert_equal [1, 2, 'a'], @filters.concat([1, 2], ['a'])
assert_equal [1, 2, 10], @filters.concat([1, 2], [10])
assert_raises(TypeError) do
# no implicit conversion of Fixnum into Array
assert_raises(Liquid::ArgumentError, "concat filter requires an array argument") do
@filters.concat([1, 2], 10)
end
end

View File

@@ -217,6 +217,17 @@ class IncludeTagTest < Minitest::Test
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 %}"

View File

@@ -24,8 +24,8 @@ class RawTagTest < Minitest::Test
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 %}'
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

View File

@@ -27,6 +27,12 @@ class ErroneousDrop < Liquid::Drop
end
end
class DropWithUndefinedMethod < Liquid::Drop
def foo
'foo'
end
end
class TemplateTest < Minitest::Test
include Liquid
@@ -133,6 +139,17 @@ class TemplateTest < Minitest::Test
refute_nil t.resource_limits.assign_score
end
def test_resource_limits_assign_score_nested
t = Template.parse("{% assign foo = 'aaaa' | reverse %}")
t.resource_limits.assign_score_limit = 3
assert_equal "Liquid error: Memory limits exceeded", t.render
assert t.resource_limits.reached?
t.resource_limits.assign_score_limit = 5
assert_equal "", t.render!
end
def test_resource_limits_aborts_rendering_after_first_error
t = Template.parse("{% for a in (1..100) %} foo1 {% endfor %} bar {% for a in (1..100) %} foo2 {% endfor %}")
t.resource_limits.render_score_limit = 50
@@ -198,16 +215,20 @@ class TemplateTest < Minitest::Test
assert_equal 'ruby error in drop', e.message
end
def test_exception_handler_doesnt_reraise_if_it_returns_false
def test_exception_renderer_that_returns_string
exception = nil
Template.parse("{{ 1 | divided_by: 0 }}").render({}, exception_handler: ->(e) { exception = e; false })
handler = ->(e) { exception = e; '<!-- error -->' }
output = Template.parse("{{ 1 | divided_by: 0 }}").render({}, exception_renderer: handler)
assert exception.is_a?(Liquid::ZeroDivisionError)
assert_equal '<!-- error -->', output
end
def test_exception_handler_does_reraise_if_it_returns_true
def test_exception_renderer_that_raises
exception = nil
assert_raises(Liquid::ZeroDivisionError) do
Template.parse("{{ 1 | divided_by: 0 }}").render({}, exception_handler: ->(e) { exception = e; true })
Template.parse("{{ 1 | divided_by: 0 }}").render({}, exception_renderer: ->(e) { exception = e; raise })
end
assert exception.is_a?(Liquid::ZeroDivisionError)
end
@@ -225,4 +246,78 @@ class TemplateTest < Minitest::Test
assert_equal 'BOB filtered', rendered_template
end
def test_undefined_variables
t = Template.parse("{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}")
result = t.render({ 'x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } } }, { strict_variables: true })
assert_equal '33 32 ', result
assert_equal 3, t.errors.count
assert_instance_of Liquid::UndefinedVariable, t.errors[0]
assert_equal 'Liquid error: undefined variable y', t.errors[0].message
assert_instance_of Liquid::UndefinedVariable, t.errors[1]
assert_equal 'Liquid error: undefined variable b', t.errors[1].message
assert_instance_of Liquid::UndefinedVariable, t.errors[2]
assert_equal 'Liquid error: undefined variable d', t.errors[2].message
end
def test_undefined_variables_raise
t = Template.parse("{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}")
assert_raises UndefinedVariable do
t.render!({ 'x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } } }, { strict_variables: true })
end
end
def test_undefined_drop_methods
d = DropWithUndefinedMethod.new
t = Template.new.parse('{{ foo }} {{ woot }}')
result = t.render(d, { strict_variables: true })
assert_equal 'foo ', result
assert_equal 1, t.errors.count
assert_instance_of Liquid::UndefinedDropMethod, t.errors[0]
end
def test_undefined_drop_methods_raise
d = DropWithUndefinedMethod.new
t = Template.new.parse('{{ foo }} {{ woot }}')
assert_raises UndefinedDropMethod do
t.render!(d, { strict_variables: true })
end
end
def test_undefined_filters
t = Template.parse("{{a}} {{x | upcase | somefilter1 | somefilter2 | somefilter3}}")
filters = Module.new do
def somefilter3(v)
"-#{v}-"
end
end
result = t.render({ 'a' => 123, 'x' => 'foo' }, { filters: [filters], strict_filters: true })
assert_equal '123 ', result
assert_equal 1, t.errors.count
assert_instance_of Liquid::UndefinedFilter, t.errors[0]
assert_equal 'Liquid error: undefined filter somefilter1', t.errors[0].message
end
def test_undefined_filters_raise
t = Template.parse("{{x | somefilter1 | upcase | somefilter2}}")
assert_raises UndefinedFilter do
t.render!({ 'x' => 'foo' }, { strict_filters: true })
end
end
def test_using_range_literal_works_as_expected
t = Template.parse("{% assign foo = (x..y) %}{{ foo }}")
result = t.render({ 'x' => 1, 'y' => 5 })
assert_equal '1..5', result
t = Template.parse("{% assign nums = (x..y) %}{% for num in nums %}{{ num }}{% endfor %}")
result = t.render({ 'x' => 1, 'y' => 5 })
assert_equal '12345', result
end
end

View File

@@ -0,0 +1,525 @@
require 'test_helper'
class TrimModeTest < Minitest::Test
include Liquid
# Make sure the trim isn't applied to standard output
def test_standard_output
text = <<-END_TEMPLATE
<div>
<p>
{{ 'John' }}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>
John
</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
def test_variable_output_with_multiple_blank_lines
text = <<-END_TEMPLATE
<div>
<p>
{{- 'John' -}}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>John</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
def test_tag_output_with_multiple_blank_lines
text = <<-END_TEMPLATE
<div>
<p>
{%- if true -%}
yes
{%- endif -%}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>yes</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
# Make sure the trim isn't applied to standard tags
def test_standard_tags
whitespace = ' '
text = <<-END_TEMPLATE
<div>
<p>
{% if true %}
yes
{% endif %}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>
#{whitespace}
yes
#{whitespace}
</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
text = <<-END_TEMPLATE
<div>
<p>
{% if false %}
no
{% endif %}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>
#{whitespace}
</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
# Make sure the trim isn't too agressive
def test_no_trim_output
text = '<p>{{- \'John\' -}}</p>'
expected = '<p>John</p>'
assert_template_result(expected, text)
end
# Make sure the trim isn't too agressive
def test_no_trim_tags
text = '<p>{%- if true -%}yes{%- endif -%}</p>'
expected = '<p>yes</p>'
assert_template_result(expected, text)
text = '<p>{%- if false -%}no{%- endif -%}</p>'
expected = '<p></p>'
assert_template_result(expected, text)
end
def test_single_line_outer_tag
text = '<p> {%- if true %} yes {% endif -%} </p>'
expected = '<p> yes </p>'
assert_template_result(expected, text)
text = '<p> {%- if false %} no {% endif -%} </p>'
expected = '<p></p>'
assert_template_result(expected, text)
end
def test_single_line_inner_tag
text = '<p> {% if true -%} yes {%- endif %} </p>'
expected = '<p> yes </p>'
assert_template_result(expected, text)
text = '<p> {% if false -%} no {%- endif %} </p>'
expected = '<p> </p>'
assert_template_result(expected, text)
end
def test_single_line_post_tag
text = '<p> {% if true -%} yes {% endif -%} </p>'
expected = '<p> yes </p>'
assert_template_result(expected, text)
text = '<p> {% if false -%} no {% endif -%} </p>'
expected = '<p> </p>'
assert_template_result(expected, text)
end
def test_single_line_pre_tag
text = '<p> {%- if true %} yes {%- endif %} </p>'
expected = '<p> yes </p>'
assert_template_result(expected, text)
text = '<p> {%- if false %} no {%- endif %} </p>'
expected = '<p> </p>'
assert_template_result(expected, text)
end
def test_pre_trim_output
text = <<-END_TEMPLATE
<div>
<p>
{{- 'John' }}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>John
</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
def test_pre_trim_tags
text = <<-END_TEMPLATE
<div>
<p>
{%- if true %}
yes
{%- endif %}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>
yes
</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
text = <<-END_TEMPLATE
<div>
<p>
{%- if false %}
no
{%- endif %}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>
</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
def test_post_trim_output
text = <<-END_TEMPLATE
<div>
<p>
{{ 'John' -}}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>
John</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
def test_post_trim_tags
text = <<-END_TEMPLATE
<div>
<p>
{% if true -%}
yes
{% endif -%}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>
yes
</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
text = <<-END_TEMPLATE
<div>
<p>
{% if false -%}
no
{% endif -%}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>
</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
def test_pre_and_post_trim_tags
text = <<-END_TEMPLATE
<div>
<p>
{%- if true %}
yes
{% endif -%}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>
yes
</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
text = <<-END_TEMPLATE
<div>
<p>
{%- if false %}
no
{% endif -%}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p></p>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
def test_post_and_pre_trim_tags
text = <<-END_TEMPLATE
<div>
<p>
{% if true -%}
yes
{%- endif %}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>
yes
</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
whitespace = ' '
text = <<-END_TEMPLATE
<div>
<p>
{% if false -%}
no
{%- endif %}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>
#{whitespace}
</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
def test_trim_output
text = <<-END_TEMPLATE
<div>
<p>
{{- 'John' -}}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>John</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
def test_trim_tags
text = <<-END_TEMPLATE
<div>
<p>
{%- if true -%}
yes
{%- endif -%}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>yes</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
text = <<-END_TEMPLATE
<div>
<p>
{%- if false -%}
no
{%- endif -%}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p></p>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
def test_whitespace_trim_output
text = <<-END_TEMPLATE
<div>
<p>
{{- 'John' -}},
{{- '30' -}}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>John,30</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
def test_whitespace_trim_tags
text = <<-END_TEMPLATE
<div>
<p>
{%- if true -%}
yes
{%- endif -%}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>yes</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
text = <<-END_TEMPLATE
<div>
<p>
{%- if false -%}
no
{%- endif -%}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p></p>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
def test_complex_trim_output
text = <<-END_TEMPLATE
<div>
<p>
{{- 'John' -}}
{{- '30' -}}
</p>
<b>
{{ 'John' -}}
{{- '30' }}
</b>
<i>
{{- 'John' }}
{{ '30' -}}
</i>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>John30</p>
<b>
John30
</b>
<i>John
30</i>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
def test_complex_trim
text = <<-END_TEMPLATE
<div>
{%- if true -%}
{%- if true -%}
<p>
{{- 'John' -}}
</p>
{%- endif -%}
{%- endif -%}
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div><p>John</p></div>
END_EXPECTED
assert_template_result(expected, text)
end
def test_raw_output
whitespace = ' '
text = <<-END_TEMPLATE
<div>
{% raw %}
{%- if true -%}
<p>
{{- 'John' -}}
</p>
{%- endif -%}
{% endraw %}
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
#{whitespace}
{%- if true -%}
<p>
{{- 'John' -}}
</p>
{%- endif -%}
#{whitespace}
</div>
END_EXPECTED
assert_template_result(expected, text)
end
end # TrimModeTest

View File

@@ -46,6 +46,8 @@ class BlockUnitTest < Minitest::Test
def test_with_custom_tag
Liquid::Template.register_tag("testtag", Block)
assert Liquid::Template.parse("{% testtag %} {% endtesttag %}")
ensure
Liquid::Template.tags.delete('testtag')
end
private

View File

@@ -3,56 +3,65 @@ require 'test_helper'
class ConditionUnitTest < Minitest::Test
include Liquid
def setup
@context = Liquid::Context.new
end
def test_basic_condition
assert_equal false, Condition.new(1, '==', 2).evaluate
assert_equal true, Condition.new(1, '==', 1).evaluate
end
def test_default_operators_evalute_true
assert_evalutes_true 1, '==', 1
assert_evalutes_true 1, '!=', 2
assert_evalutes_true 1, '<>', 2
assert_evalutes_true 1, '<', 2
assert_evalutes_true 2, '>', 1
assert_evalutes_true 1, '>=', 1
assert_evalutes_true 2, '>=', 1
assert_evalutes_true 1, '<=', 2
assert_evalutes_true 1, '<=', 1
assert_evaluates_true 1, '==', 1
assert_evaluates_true 1, '!=', 2
assert_evaluates_true 1, '<>', 2
assert_evaluates_true 1, '<', 2
assert_evaluates_true 2, '>', 1
assert_evaluates_true 1, '>=', 1
assert_evaluates_true 2, '>=', 1
assert_evaluates_true 1, '<=', 2
assert_evaluates_true 1, '<=', 1
# negative numbers
assert_evalutes_true 1, '>', -1
assert_evalutes_true -1, '<', 1
assert_evalutes_true 1.0, '>', -1.0
assert_evalutes_true -1.0, '<', 1.0
assert_evaluates_true 1, '>', -1
assert_evaluates_true (-1), '<', 1
assert_evaluates_true 1.0, '>', -1.0
assert_evaluates_true (-1.0), '<', 1.0
end
def test_default_operators_evalute_false
assert_evalutes_false 1, '==', 2
assert_evalutes_false 1, '!=', 1
assert_evalutes_false 1, '<>', 1
assert_evalutes_false 1, '<', 0
assert_evalutes_false 2, '>', 4
assert_evalutes_false 1, '>=', 3
assert_evalutes_false 2, '>=', 4
assert_evalutes_false 1, '<=', 0
assert_evalutes_false 1, '<=', 0
assert_evaluates_false 1, '==', 2
assert_evaluates_false 1, '!=', 1
assert_evaluates_false 1, '<>', 1
assert_evaluates_false 1, '<', 0
assert_evaluates_false 2, '>', 4
assert_evaluates_false 1, '>=', 3
assert_evaluates_false 2, '>=', 4
assert_evaluates_false 1, '<=', 0
assert_evaluates_false 1, '<=', 0
end
def test_contains_works_on_strings
assert_evalutes_true 'bob', 'contains', 'o'
assert_evalutes_true 'bob', 'contains', 'b'
assert_evalutes_true 'bob', 'contains', 'bo'
assert_evalutes_true 'bob', 'contains', 'ob'
assert_evalutes_true 'bob', 'contains', 'bob'
assert_evaluates_true 'bob', 'contains', 'o'
assert_evaluates_true 'bob', 'contains', 'b'
assert_evaluates_true 'bob', 'contains', 'bo'
assert_evaluates_true 'bob', 'contains', 'ob'
assert_evaluates_true 'bob', 'contains', 'bob'
assert_evalutes_false 'bob', 'contains', 'bob2'
assert_evalutes_false 'bob', 'contains', 'a'
assert_evalutes_false 'bob', 'contains', '---'
assert_evaluates_false 'bob', 'contains', 'bob2'
assert_evaluates_false 'bob', 'contains', 'a'
assert_evaluates_false 'bob', 'contains', '---'
end
def test_invalid_comparation_operator
assert_evaluates_argument_error 1, '~~', 0
end
def test_comparing_hash_and_integer
assert_evaluates_argument_error({a: 1}, '>', 1)
assert_evaluates_argument_error(1, '>', {a: 1})
end
def test_comparation_of_int_and_str
assert_evaluates_argument_error '1', '>', 0
assert_evaluates_argument_error '1', '<', 0
@@ -65,29 +74,29 @@ class ConditionUnitTest < Minitest::Test
@context['array'] = [1, 2, 3, 4, 5]
array_expr = VariableLookup.new("array")
assert_evalutes_false array_expr, 'contains', 0
assert_evalutes_true array_expr, 'contains', 1
assert_evalutes_true array_expr, 'contains', 2
assert_evalutes_true array_expr, 'contains', 3
assert_evalutes_true array_expr, 'contains', 4
assert_evalutes_true array_expr, 'contains', 5
assert_evalutes_false array_expr, 'contains', 6
assert_evalutes_false array_expr, 'contains', "1"
assert_evaluates_false array_expr, 'contains', 0
assert_evaluates_true array_expr, 'contains', 1
assert_evaluates_true array_expr, 'contains', 2
assert_evaluates_true array_expr, 'contains', 3
assert_evaluates_true array_expr, 'contains', 4
assert_evaluates_true array_expr, 'contains', 5
assert_evaluates_false array_expr, 'contains', 6
assert_evaluates_false array_expr, 'contains', "1"
end
def test_contains_returns_false_for_nil_operands
@context = Liquid::Context.new
assert_evalutes_false VariableLookup.new('not_assigned'), 'contains', '0'
assert_evalutes_false 0, 'contains', VariableLookup.new('not_assigned')
assert_evaluates_false VariableLookup.new('not_assigned'), 'contains', '0'
assert_evaluates_false 0, 'contains', VariableLookup.new('not_assigned')
end
def test_contains_return_false_on_wrong_data_type
assert_evalutes_false 1, 'contains', 0
assert_evaluates_false 1, 'contains', 0
end
def test_contains_with_string_left_operand_coerces_right_operand_to_string
assert_evalutes_true ' 1 ', 'contains', 1
assert_evalutes_false ' 1 ', 'contains', 2
assert_evaluates_true ' 1 ', 'contains', 1
assert_evaluates_false ' 1 ', 'contains', 2
end
def test_or_condition
@@ -121,8 +130,8 @@ class ConditionUnitTest < Minitest::Test
def test_should_allow_custom_proc_operator
Condition.operators['starts_with'] = proc { |cond, left, right| left =~ %r{^#{right}} }
assert_evalutes_true 'bob', 'starts_with', 'b'
assert_evalutes_false 'bob', 'starts_with', 'o'
assert_evaluates_true 'bob', 'starts_with', 'b'
assert_evaluates_false 'bob', 'starts_with', 'o'
ensure
Condition.operators.delete 'starts_with'
end
@@ -131,24 +140,24 @@ class ConditionUnitTest < Minitest::Test
@context = Liquid::Context.new
@context['one'] = @context['another'] = "gnomeslab-and-or-liquid"
assert_evalutes_true VariableLookup.new("one"), '==', VariableLookup.new("another")
assert_evaluates_true VariableLookup.new("one"), '==', VariableLookup.new("another")
end
private
def assert_evalutes_true(left, op, right)
assert Condition.new(left, op, right).evaluate(@context || Liquid::Context.new),
def assert_evaluates_true(left, op, right)
assert Condition.new(left, op, right).evaluate(@context),
"Evaluated false: #{left} #{op} #{right}"
end
def assert_evalutes_false(left, op, right)
assert !Condition.new(left, op, right).evaluate(@context || Liquid::Context.new),
def assert_evaluates_false(left, op, right)
assert !Condition.new(left, op, right).evaluate(@context),
"Evaluated true: #{left} #{op} #{right}"
end
def assert_evaluates_argument_error(left, op, right)
assert_raises(Liquid::ArgumentError) do
Condition.new(left, op, right).evaluate(@context || Liquid::Context.new)
Condition.new(left, op, right).evaluate(@context)
end
end
end # ConditionTest

View File

@@ -77,4 +77,72 @@ class StrainerUnitTest < Minitest::Test
assert_kind_of b, strainer
assert_kind_of Liquid::StandardFilters, strainer
end
def test_add_filter_when_wrong_filter_class
c = Context.new
s = c.strainer
wrong_filter = ->(v) { v.reverse }
assert_raises ArgumentError do
s.class.add_filter(wrong_filter)
end
end
module PrivateMethodOverrideFilter
private
def public_filter
"overriden as private"
end
end
def test_add_filter_raises_when_module_privately_overrides_registered_public_methods
strainer = Context.new.strainer
error = assert_raises(Liquid::MethodOverrideError) do
strainer.class.add_filter(PrivateMethodOverrideFilter)
end
assert_equal 'Liquid error: Filter overrides registered public methods as non public: public_filter', error.message
end
module ProtectedMethodOverrideFilter
protected
def public_filter
"overriden as protected"
end
end
def test_add_filter_raises_when_module_overrides_registered_public_method_as_protected
strainer = Context.new.strainer
error = assert_raises(Liquid::MethodOverrideError) do
strainer.class.add_filter(ProtectedMethodOverrideFilter)
end
assert_equal 'Liquid error: Filter overrides registered public methods as non public: public_filter', error.message
end
module PublicMethodOverrideFilter
def public_filter
"public"
end
end
def test_add_filter_does_not_raise_when_module_overrides_previously_registered_method
strainer = Context.new.strainer
strainer.class.add_filter(PublicMethodOverrideFilter)
assert strainer.class.filter_methods.include?('public_filter')
end
module LateAddedFilter
def late_added_filter(input)
"filtered"
end
end
def test_global_filter_clears_cache
assert_equal 'input', Strainer.create(nil).invoke('late_added_filter', 'input')
Strainer.global_filter(LateAddedFilter)
assert_equal 'filtered', Strainer.create(nil).invoke('late_added_filter', 'input')
end
end # StrainerTest

View File

@@ -67,4 +67,12 @@ class TemplateUnitTest < Minitest::Test
Template.tags.delete('fake')
assert_nil Template.tags['fake']
end
def test_tags_can_be_looped_over
Template.register_tag('fake', FakeTag)
result = Template.tags.map { |name, klass| [name, klass] }
assert result.include?(["fake", "TemplateUnitTest::FakeTag"])
ensure
Template.tags.delete('fake')
end
end