Compare commits

..

1 Commits

Author SHA1 Message Date
Justin Li
e99c7e2eec Force #to_liquid call in InputIterator#each 2016-02-01 11:44:54 -05:00
63 changed files with 440 additions and 2341 deletions

2
.github/probots.yml vendored
View File

@@ -1,2 +0,0 @@
enabled:
- cla

View File

@@ -7,6 +7,8 @@ AllCops:
Metrics/BlockNesting:
Max: 3
Exclude:
- 'lib/liquid/block_body.rb'
Metrics/ModuleLength:
Enabled: false
@@ -30,7 +32,7 @@ Lint/UnusedBlockArgument:
Enabled: false
Lint/EndAlignment:
EnforcedStyleAlignWith: variable
AlignWith: variable
Lint/UnusedMethodArgument:
Enabled: false
@@ -59,10 +61,10 @@ Style/BracesAroundHashParameters:
Style/NumericLiterals:
Enabled: false
Layout/SpaceInsideBrackets:
Style/SpaceInsideBrackets:
Enabled: false
Layout/SpaceBeforeBlockBraces:
Style/SpaceBeforeBlockBraces:
Enabled: false
Style/Documentation:
@@ -71,22 +73,19 @@ Style/Documentation:
Style/ClassAndModuleChildren:
Enabled: false
Style/TrailingCommaInArrayLiteral:
Style/TrailingComma:
Enabled: false
Style/TrailingCommaInHashLiteral:
Enabled: false
Layout/IndentHash:
Style/IndentHash:
EnforcedStyle: consistent
Style/FormatString:
Enabled: false
Layout/AlignParameters:
Style/AlignParameters:
EnforcedStyle: with_fixed_indentation
Layout/MultilineOperationIndentation:
Style/MultilineOperationIndentation:
EnforcedStyle: indented
Style/IfUnlessModifier:
@@ -95,7 +94,7 @@ Style/IfUnlessModifier:
Style/RaiseArgs:
Enabled: false
Style/PreferredHashMethods:
Style/DeprecatedHashMethods:
Enabled: false
Style/RegexpLiteral:
@@ -110,7 +109,7 @@ Performance/Count:
Style/ConstantName:
Enabled: false
Layout/CaseIndentation:
Style/CaseIndentation:
Enabled: false
Style/ClassVars:
@@ -124,7 +123,3 @@ Style/TrivialAccessors:
Style/WordArray:
Enabled: false
Naming/MethodName:
Exclude:
- 'example/server/liquid_servlet.rb'

View File

@@ -1,248 +1,72 @@
# This configuration was generated by
# `rubocop --auto-gen-config`
# on 2017-11-22 11:35:55 -0500 using RuboCop version 0.49.1.
# 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: 3
# Cop supports --auto-correct.
Layout/ClosingParenthesisIndentation:
Exclude:
- 'test/integration/error_handling_test.rb'
# Offense count: 1
# Cop supports --auto-correct.
Layout/EmptyLineAfterMagicComment:
Exclude:
- 'lib/liquid/version.rb'
# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: AllowForAlignment, ForceEqualSignAlignment.
Layout/ExtraSpacing:
Exclude:
- 'test/integration/parsing_quirks_test.rb'
# Offense count: 5
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: auto_detection, squiggly, active_support, powerpack, unindent
Layout/IndentHeredoc:
Exclude:
- 'test/integration/tags/for_tag_test.rb'
- 'test/integration/trim_mode_test.rb'
Lint/NestedMethodDefinition:
Enabled: false
# Offense count: 6
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: symmetrical, new_line, same_line
Layout/MultilineMethodCallBraceLayout:
Exclude:
- 'test/integration/error_handling_test.rb'
- 'test/unit/strainer_unit_test.rb'
# Offense count: 2
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: runtime_error, standard_error
Lint/InheritException:
Exclude:
- 'lib/liquid/interrupts.rb'
# Offense count: 1
Lint/ScriptPermission:
Exclude:
- 'test/test_helper.rb'
# Offense count: 52
# Offense count: 53
Metrics/AbcSize:
Max: 56
Max: 58
# Offense count: 13
# Offense count: 12
Metrics/CyclomaticComplexity:
Max: 12
Max: 15
# Offense count: 620
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
# URISchemes: http, https
# Offense count: 553
# Configuration parameters: AllowURI, URISchemes.
Metrics/LineLength:
Max: 294
# Offense count: 102
# Offense count: 77
# Configuration parameters: CountComments.
Metrics/MethodLength:
Max: 37
# Offense count: 9
Metrics/PerceivedComplexity:
Max: 11
# Offense count: 10
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: prefer_alias, prefer_alias_method
Style/Alias:
Exclude:
- 'lib/liquid/drop.rb'
- 'lib/liquid/i18n.rb'
- 'lib/liquid/profiler/hooks.rb'
- 'lib/liquid/standardfilters.rb'
- 'lib/liquid/tag.rb'
- 'lib/liquid/tags/include.rb'
- 'lib/liquid/variable.rb'
# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, SingleLineConditionsOnly, IncludeTernaryExpressions.
# SupportedStyles: assign_to_condition, assign_inside_condition
Style/ConditionalAssignment:
Exclude:
- 'lib/liquid/errors.rb'
# Offense count: 2
# Cop supports --auto-correct.
Style/EmptyCaseCondition:
Exclude:
- 'lib/liquid/block_body.rb'
- 'lib/liquid/lexer.rb'
# Offense count: 5
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: compact, expanded
Style/EmptyMethod:
Exclude:
- 'lib/liquid/tag.rb'
- 'lib/liquid/tags/comment.rb'
- 'lib/liquid/tags/include.rb'
- 'test/integration/tags/include_tag_test.rb'
- 'test/unit/context_unit_test.rb'
# Offense count: 2
# Configuration parameters: SupportedStyles.
# SupportedStyles: annotated, template
Style/FormatStringToken:
EnforcedStyle: template
# Offense count: 14
# Configuration parameters: MinBodyLength.
Style/GuardClause:
Exclude:
- 'lib/liquid/condition.rb'
- 'lib/liquid/lexer.rb'
- 'lib/liquid/strainer.rb'
- 'lib/liquid/tags/assign.rb'
- 'lib/liquid/tags/capture.rb'
- 'lib/liquid/tags/case.rb'
- 'lib/liquid/tags/for.rb'
- 'lib/liquid/tags/include.rb'
- 'lib/liquid/tags/raw.rb'
- 'lib/liquid/tags/table_row.rb'
- 'lib/liquid/variable.rb'
- 'test/unit/tokenizer_unit_test.rb'
# Offense count: 4
# Configuration parameters: SupportedStyles.
# SupportedStyles: snake_case, camelCase
Style/MethodName:
EnforcedStyle: snake_case
Max: 46
# Offense count: 6
# Cop supports --auto-correct.
Style/MutableConstant:
Exclude:
- 'lib/liquid/expression.rb'
- 'lib/liquid/lexer.rb'
- 'lib/liquid/standardfilters.rb'
- 'lib/liquid/tags/if.rb'
- 'lib/liquid/variable_lookup.rb'
- 'lib/liquid/version.rb'
Metrics/PerceivedComplexity:
Max: 13
# Offense count: 1
Style/AccessorMethodName:
Enabled: false
# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, MinBodyLength, SupportedStyles.
# SupportedStyles: skip_modifier_ifs, always
Style/Next:
Exclude:
- 'lib/liquid/tags/for.rb'
Style/ClosingParenthesisIndentation:
Enabled: false
# Offense count: 3
# Configuration parameters: MinBodyLength.
Style/GuardClause:
Enabled: false
# Offense count: 4
# Cop supports --auto-correct.
# Configuration parameters: AutoCorrect, EnforcedStyle, SupportedStyles.
# SupportedStyles: predicate, comparison
Style/NumericPredicate:
Exclude:
- 'spec/**/*'
- 'lib/liquid/context.rb'
- 'lib/liquid/forloop_drop.rb'
- 'lib/liquid/standardfilters.rb'
- 'lib/liquid/tablerowloop_drop.rb'
# Offense count: 14
# Cop supports --auto-correct.
# Configuration parameters: PreferredDelimiters.
Style/PercentLiteralDelimiters:
Exclude:
- 'lib/liquid/tags/if.rb'
- 'liquid.gemspec'
- 'test/integration/assign_test.rb'
- 'test/integration/standard_filter_test.rb'
# Offense count: 2
# Cop supports --auto-correct.
Style/RedundantParentheses:
Exclude:
- 'test/unit/condition_unit_test.rb'
# Configuration parameters: EnforcedStyle, SupportedStyles.
Style/MethodName:
Enabled: false
# Offense count: 1
# Cop supports --auto-correct.
Style/RedundantSelf:
Exclude:
- 'lib/liquid/strainer.rb'
Style/MultilineBlockChain:
Enabled: false
# Offense count: 9
# Cop supports --auto-correct.
# Configuration parameters: AllowAsExpressionSeparator.
Style/Semicolon:
Exclude:
- 'test/integration/error_handling_test.rb'
- 'test/integration/template_test.rb'
- 'test/unit/context_unit_test.rb'
# Offense count: 2
# Configuration parameters: EnforcedStyle, MinBodyLength, SupportedStyles.
Style/Next:
Enabled: false
# Offense count: 7
# Cop supports --auto-correct.
# Configuration parameters: MinSize, SupportedStyles.
# SupportedStyles: percent, brackets
Style/SymbolArray:
EnforcedStyle: brackets
# Offense count: 2
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, AllowSafeAssignment.
# SupportedStyles: require_parentheses, require_no_parentheses, require_parentheses_when_complex
Style/TernaryParentheses:
Exclude:
- 'lib/liquid/context.rb'
- 'lib/liquid/utils.rb'
# Offense count: 4
# Cop supports --auto-correct.
Style/UnneededInterpolation:
Exclude:
- 'lib/liquid/i18n.rb'
- 'test/integration/standard_filter_test.rb'
# Offense count: 2
# Cop supports --auto-correct.
Style/UnneededPercentQ:
Exclude:
- 'test/integration/error_handling_test.rb'
# Configuration parameters: AllowAsExpressionSeparator.
Style/Semicolon:
Enabled: false
# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: MaxLineLength.
Style/WhileUntilModifier:
Exclude:
- 'lib/liquid/tags/case.rb'
Enabled: false

View File

@@ -1,11 +1,9 @@
language: ruby
rvm:
- 2.0
- 2.1
- 2.2
- 2.3
- 2.4
- 2.5
- ruby-head
- jruby-head
# - rbx-2
@@ -19,14 +17,9 @@ addons:
matrix:
allow_failures:
- rvm: ruby-head
- rvm: jruby-head
install:
- gem install rainbow -v 2.2.1
- bundle install
script: bundle exec rake
script: "bundle exec rake"
notifications:
disable: true

View File

@@ -4,22 +4,23 @@
* Bugfixes
* 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
* 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
* 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
## Workflow
* Fork the Liquid repository
* Create a new branch in your fork
* If it makes sense, add tests for your code and/or run a performance benchmark
* Make sure all tests pass (`bundle exec rake`)
* If it makes sense, add tests for your code and run a performance benchmark
* Make sure all tests pass
* 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.

16
Gemfile
View File

@@ -1,20 +1,14 @@
source 'https://rubygems.org'
git_source(:github) do |repo_name|
"https://github.com/#{repo_name}.git"
end
gemspec
gem 'stackprof', platforms: :mri
group :benchmark, :test do
gem 'benchmark-ips'
end
gem 'stackprof', platforms: :mri_21
group :test do
gem 'rubocop', '~> 0.49.0'
gem 'spy', '0.4.1'
gem 'benchmark-ips'
gem 'rubocop', '0.34.2'
platform :mri do
gem 'liquid-c', github: 'Shopify/liquid-c', ref: '9168659de45d6d576fce30c735f857e597fa26f6'
gem 'liquid-c', github: 'Shopify/liquid-c', ref: '2570693d8d03faa0df9160ec74348a7149436df3'
end
end

View File

@@ -1,12 +1,8 @@
# Liquid Change Log
## 4.0.0 / 2016-12-14 / branch "4-0-stable"
## 4.0.0 / not yet released / branch "master"
### 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]
@@ -20,13 +16,9 @@
* Add concat filter to concatenate arrays (#429) [Diogo Beato]
* Ruby 1.9 support dropped (#491) [Justin Li]
* Liquid::Template.file_system's read_template_file method is no longer passed the context. (#441) [James Reid-Smith]
* Remove `liquid_methods` (See https://github.com/Shopify/liquid/pull/568 for replacement)
* Liquid::Template.register_filter raises when the module overrides registered public methods as private or protected (#705) [Gaurav Chande]
* Remove support for `liquid_methods`
### Fixed
* Fix variable names being detected as an operator when starting with contains (#788) [Michael Angell]
* Fix include tag used with strict_variables (#828) [QuickPay]
* Fix map filter when value is a Proc (#672) [Guillaume Malette]
* Fix truncate filter when value is not a string (#672) [Guillaume Malette]
* Fix behaviour of escape filter when input is nil (#665) [Tanel Jakobsoo]

View File

@@ -42,8 +42,6 @@ Liquid is a template engine which was written with very specific requirements:
## How to use Liquid
Install Liquid by adding `gem 'liquid'` to your gemfile.
Liquid supports a very simple API based around the Liquid::Template class.
For standard use you can just pass it the content of a file and call render with a parameters hash.
@@ -75,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 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
```

3
circle.yml Normal file
View File

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

View File

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

View File

@@ -1,7 +1,5 @@
module Liquid
class Block < Tag
MAX_DEPTH = 100
def initialize(tag_name, markup, options)
super
@blank = true
@@ -26,12 +24,12 @@ module Liquid
end
def unknown_tag(tag, _params, _tokens)
if tag == 'else'.freeze
case tag
when 'else'.freeze
raise SyntaxError.new(parse_context.locale.t("errors.syntax.unexpected_else".freeze,
block_name: block_name))
elsif tag.start_with?('end'.freeze)
when 'end'.freeze
raise SyntaxError.new(parse_context.locale.t("errors.syntax.invalid_delimiter".freeze,
tag: tag,
block_name: block_name,
block_delimiter: block_delimiter))
else
@@ -50,25 +48,17 @@ module Liquid
protected
def parse_body(body, tokens)
if parse_context.depth >= MAX_DEPTH
raise StackLevelError, "Nesting too deep".freeze
end
parse_context.depth += 1
begin
body.parse(tokens, parse_context) do |end_tag_name, end_tag_params|
@blank &&= body.blank?
body.parse(tokens, parse_context) do |end_tag_name, end_tag_params|
@blank &&= body.blank?
return false if end_tag_name == block_delimiter
unless end_tag_name
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name))
end
# this tag is not registered with the system
# pass it to the current block for special handling or error reporting
unknown_tag(end_tag_name, end_tag_params, tokens)
return false if end_tag_name == block_delimiter
unless end_tag_name
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name))
end
ensure
parse_context.depth -= 1
# this tag is not registered with the system
# pass it to the current block for special handling or error reporting
unknown_tag(end_tag_name, end_tag_params, tokens)
end
true

View File

@@ -1,8 +1,7 @@
module Liquid
class BlockBody
FullToken = /\A#{TagStart}#{WhitespaceControl}?\s*(\w+)\s*(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
WhitespaceOrNothing = /\A\s*\z/
FullToken = /\A#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
ContentOfVariable = /\A#{VariableStart}(.*)#{VariableEnd}\z/om
TAGSTART = "{%".freeze
VARSTART = "{{".freeze
@@ -16,35 +15,32 @@ module Liquid
def parse(tokenizer, parse_context)
parse_context.line_number = tokenizer.line_number
while token = tokenizer.shift
next if token.empty?
case
when token.start_with?(TAGSTART)
whitespace_handler(token, parse_context)
unless token =~ FullToken
raise_missing_tag_terminator(token, parse_context)
unless token.empty?
case
when token.start_with?(TAGSTART)
if token =~ FullToken
tag_name = $1
markup = $2
# fetch the tag from registered blocks
if tag = registered_tags[tag_name]
new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
@blank &&= new_tag.blank?
@nodelist << new_tag
else
# end parsing if we reach an unknown tag and let the caller decide
# determine how to proceed
return yield tag_name, markup
end
else
raise_missing_tag_terminator(token, parse_context)
end
when token.start_with?(VARSTART)
@nodelist << create_variable(token, parse_context)
@blank = false
else
@nodelist << token
@blank &&= !!(token =~ /\A\s*\z/)
end
tag_name = $1
markup = $2
# fetch the tag from registered blocks
unless tag = registered_tags[tag_name]
# end parsing if we reach an unknown tag and let the caller decide
# determine how to proceed
return yield tag_name, markup
end
new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
@blank &&= new_tag.blank?
@nodelist << new_tag
when token.start_with?(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 =~ WhitespaceOrNothing)
end
parse_context.line_number = tokenizer.line_number
end
@@ -52,16 +48,6 @@ 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
@@ -70,27 +56,29 @@ module Liquid
output = []
context.resource_limits.render_score += @nodelist.length
idx = 0
while node = @nodelist[idx]
case node
when String
check_resources(context, node)
output << node
when Variable
render_node_to_output(node, output, context)
when Block
render_node_to_output(node, output, context, node.blank?)
break if context.interrupt? # might have happened in a for-block
when Continue, Break
@nodelist.each do |token|
# Break out if we have any unhanded interrupts.
break if context.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 %}
context.push_interrupt(node.interrupt)
break
else # Other non-Block tags
render_node_to_output(node, output, context)
if token.is_a?(Continue) || token.is_a?(Break)
context.push_interrupt(token.interrupt)
break
end
node_output = render_node(token, context)
unless token.is_a?(Block) && token.blank?
output << node_output
end
rescue MemoryError => e
raise e
rescue ::StandardError => e
output << context.handle_error(e, token.line_number)
end
idx += 1
end
output.join
@@ -98,25 +86,15 @@ module Liquid
private
def render_node_to_output(node, output, context, skip_output = false)
node_output = node.render(context)
def render_node(node, context)
node_output = (node.respond_to?(:render) ? node.render(context) : node)
node_output = node_output.is_a?(Array) ? node_output.join : node_output.to_s
check_resources(context, node_output)
output << node_output unless skip_output
rescue MemoryError => e
raise e
rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
context.handle_error(e, node.line_number)
output << nil
rescue ::StandardError => e
line_number = node.is_a?(String) ? nil : node.line_number
output << context.handle_error(e, line_number)
end
def check_resources(context, node_output)
context.resource_limits.render_length += node_output.length
return unless context.resource_limits.reached?
raise MemoryError.new("Memory limits exceeded".freeze)
if context.resource_limits.reached?
raise MemoryError.new("Memory limits exceeded".freeze)
end
node_output
end
def create_variable(token, parse_context)

View File

@@ -29,7 +29,7 @@ module Liquid
@@operators
end
attr_reader :attachment, :child_condition
attr_reader :attachment
attr_accessor :left, :operator, :right
def initialize(left = nil, operator = nil, right = nil)
@@ -41,22 +41,16 @@ module Liquid
end
def evaluate(context = Context.new)
condition = self
result = nil
loop do
result = interpret_condition(condition.left, condition.right, condition.operator, context)
result = interpret_condition(left, right, operator, context)
case condition.child_relation
when :or
break if result
when :and
break unless result
else
break
end
condition = condition.child_condition
case @child_relation
when :or
result || @child_condition.evaluate(context)
when :and
result && @child_condition.evaluate(context)
else
result
end
result
end
def or(condition)
@@ -81,10 +75,6 @@ module Liquid
"#<Condition #{[@left, @operator, @right].compact.join(' '.freeze)}>"
end
protected
attr_reader :child_relation
private
def equal_variables(left, right)
@@ -120,7 +110,7 @@ module Liquid
if operation.respond_to?(:call)
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) && right.respond_to?(operation)
begin
left.send(operation, right)
rescue ::ArgumentError => e
@@ -128,15 +118,6 @@ module Liquid
end
end
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[
@node.left, @node.right,
@node.child_condition, @node.attachment
].compact
end
end
end
class ElseCondition < Condition

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_renderer, :template_name, :partial, :global_filter, :strict_variables, :strict_filters
attr_accessor :exception_handler, :template_name, :partial, :global_filter
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil)
@environments = [environments].flatten
@@ -21,15 +21,13 @@ 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_renderer = ->(e) { raise }
self.exception_handler = ->(e) { raise }
end
@interrupts = []
@@ -75,11 +73,30 @@ module Liquid
end
def handle_error(e, line_number = nil)
e = internal_error unless e.is_a?(Liquid::Error)
e.template_name ||= template_name
e.line_number ||= line_number
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
errors.push(e)
exception_renderer.call(e).to_s
output || Liquid::Error.render(e)
end
def invoke(method, *args)
@@ -89,7 +106,7 @@ module Liquid
# Push new local scope on the stack. use <tt>Context#stack</tt> instead
def push(new_scope = {})
@scopes.unshift(new_scope)
raise StackLevelError, "Nesting too deep".freeze if @scopes.length > Block::MAX_DEPTH
raise StackLevelError, "Nesting too deep".freeze if @scopes.length > 100
end
# Merge a hash of variables in the current local scope
@@ -160,7 +177,7 @@ module Liquid
end
# 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
# path and find_index() is optimized in MRI to reduce object allocation
index = @scopes.find_index { |s| s.key?(key) }
@@ -170,10 +187,8 @@ module Liquid
if scope.nil?
@environments.each do |e|
variable = lookup_and_evaluate(e, key, raise_on_not_found: raise_on_not_found)
# When lookup returned a value OR there is no value but the lookup also did not raise
# then it is the value we are looking for.
if !variable.nil? || @strict_variables && raise_on_not_found
variable = lookup_and_evaluate(e, key)
unless variable.nil?
scope = e
break
end
@@ -181,7 +196,7 @@ module Liquid
end
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.context = self if variable.respond_to?(:context=)
@@ -189,14 +204,8 @@ module Liquid
variable
end
def lookup_and_evaluate(obj, key, raise_on_not_found: true)
if @strict_variables && raise_on_not_found && 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?(:[]=)
def lookup_and_evaluate(obj, key)
if (value = obj[key]).is_a?(Proc) && obj.respond_to?(:[]=)
obj[key] = (value.arity == 0) ? value.call : value.call(self)
else
value
@@ -205,13 +214,6 @@ 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,9 +24,8 @@ module Liquid
attr_writer :context
# Catch all for the method
def liquid_method_missing(method)
return nil unless @context && @context.strict_variables
raise Liquid::UndefinedDropMethod, "undefined method #{method}"
def liquid_method_missing(_method)
nil
end
# called by liquid to invoke a drop
@@ -62,17 +61,16 @@ module Liquid
end
def self.invokable_methods
@invokable_methods ||= begin
unless @invokable_methods
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)
Set.new(whitelist.map(&:to_s))
@invokable_methods = Set.new(whitelist.map(&:to_s))
end
@invokable_methods
end
end
end

View File

@@ -17,6 +17,14 @@ 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
@@ -48,9 +56,4 @@ 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

@@ -21,24 +21,20 @@ module Liquid
'empty'.freeze => MethodLiteral.new(:empty?, '').freeze
}
SINGLE_QUOTED_STRING = /\A'(.*)'\z/m
DOUBLE_QUOTED_STRING = /\A"(.*)"\z/m
INTEGERS_REGEX = /\A(-?\d+)\z/
FLOATS_REGEX = /\A(-?\d[\d\.]+)\z/
RANGES_REGEX = /\A\((\S+)\.\.(\S+)\)\z/
def self.parse(markup)
if LITERALS.key?(markup)
LITERALS[markup]
else
case markup
when SINGLE_QUOTED_STRING, DOUBLE_QUOTED_STRING
when /\A'(.*)'\z/m # Single quoted strings
$1
when INTEGERS_REGEX
when /\A"(.*)"\z/m # Double quoted strings
$1
when /\A(-?\d+)\z/ # Integer and floats
$1.to_i
when RANGES_REGEX
when /\A\((\S+)\.\.(\S+)\)\z/ # Ranges
RangeLookup.parse($1, $2)
when FLOATS_REGEX
when /\A(-?\d[\d\.]+)\z/ # Floats
$1.to_f
else
VariableLookup.parse(markup)

View File

@@ -7,12 +7,6 @@ class String # :nodoc:
end
end
class Symbol # :nodoc:
def to_liquid
to_s
end
end
class Array # :nodoc:
def to_liquid
self
@@ -31,12 +25,6 @@ class Numeric # :nodoc:
end
end
class Range # :nodoc:
def to_liquid
self
end
end
class Time # :nodoc:
def to_liquid
self
@@ -72,3 +60,9 @@ class NilClass
self
end
end
class Proc
def to_liquid # :nodoc:
self
end
end

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).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
end

View File

@@ -18,19 +18,17 @@ module Liquid
DOUBLE_STRING_LITERAL = /"[^\"]*"/
NUMBER_LITERAL = /-?\d+(\.\d+)?/
DOTDOT = /\.\./
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/
WHITESPACE_OR_NOTHING = /\s*/
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains/
def initialize(input)
@ss = StringScanner.new(input)
@ss = StringScanner.new(input.rstrip)
end
def tokenize
@output = []
until @ss.eos?
@ss.skip(WHITESPACE_OR_NOTHING)
break if @ss.eos?
@ss.skip(/\s*/)
tok = case
when t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t]
when t = @ss.scan(SINGLE_STRING_LITERAL) then [:string, t]

View File

@@ -14,7 +14,7 @@
if: "Syntax Error in tag 'if' - Valid syntax: if [expression]"
include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]"
unknown_tag: "Unknown tag '%{tag}'"
invalid_delimiter: "'%{tag}' 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_outer_tag: "Unexpected outer '%{tag}' tag"
tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"
@@ -22,5 +22,3 @@
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,13 +1,12 @@
module Liquid
class ParseContext
attr_accessor :locale, :line_number, :trim_whitespace, :depth
attr_reader :partial, :warnings, :error_mode
attr_accessor :partial, :locale, :line_number
attr_reader :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

View File

@@ -1,42 +0,0 @@
# frozen_string_literal: true
module Liquid
class ParseTreeVisitor
def self.for(node, callbacks = Hash.new(proc {}))
if defined?(node.class::ParseTreeVisitor)
node.class::ParseTreeVisitor
else
self
end.new(node, callbacks)
end
def initialize(node, callbacks)
@node = node
@callbacks = callbacks
end
def add_callback_for(*classes, &block)
callback = block
callback = ->(node, _) { yield node } if block.arity.abs == 1
callback = ->(_, _) { yield } if block.arity.zero?
classes.each { |klass| @callbacks[klass] = callback }
self
end
def visit(context = nil)
children.map do |node|
item, new_context = @callbacks[node.class].call(node, context)
[
item,
ParseTreeVisitor.for(node, @callbacks).visit(new_context || context)
]
end
end
protected
def children
@node.respond_to?(:nodelist) ? Array(@node.nodelist) : []
end
end
end

View File

@@ -1,13 +1,13 @@
module Liquid
class BlockBody
def render_node_with_profiling(node, output, context, skip_output = false)
def render_node_with_profiling(node, context)
Profiler.profile_node_render(node) do
render_node_without_profiling(node, output, context, skip_output)
render_node_without_profiling(node, context)
end
end
alias_method :render_node_without_profiling, :render_node_to_output
alias_method :render_node_to_output, :render_node_with_profiling
alias_method :render_node_without_profiling, :render_node
alias_method :render_node, :render_node_with_profiling
end
class Include < Tag

View File

@@ -33,7 +33,7 @@ module Liquid
end
def escape(input)
CGI.escapeHTML(input.to_s).untaint unless input.nil?
CGI.escapeHTML(input).untaint unless input.nil?
end
alias_method :h, :escape
@@ -42,11 +42,11 @@ module Liquid
end
def url_encode(input)
CGI.escape(input.to_s) unless input.nil?
CGI.escape(input) unless input.nil?
end
def url_decode(input)
CGI.unescape(input.to_s) unless input.nil?
CGI.unescape(input) unless input.nil?
end
def slice(input, offset, length = nil)
@@ -65,10 +65,9 @@ module Liquid
return if input.nil?
input_str = input.to_s
length = Utils.to_integer(length)
truncate_string_str = truncate_string.to_s
l = length - truncate_string_str.length
l = length - truncate_string.length
l = 0 if l < 0
input_str.length > length ? input_str[0...l] + truncate_string_str : input_str
input_str.length > length ? input_str[0...l] + truncate_string : input_str
end
def truncatewords(input, words = 15, truncate_string = "...".freeze)
@@ -77,7 +76,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.to_s : input
wordlist.length > l ? wordlist[0..l].join(" ".freeze) + truncate_string : input
end
# Split input string into an array of substrings separated by given pattern.
@@ -86,7 +85,7 @@ module Liquid
# <div class="summary">{{ post | split '//' | first }}</div>
#
def split(input, pattern)
input.to_s.split(pattern.to_s)
input.to_s.split(pattern)
end
def strip(input)
@@ -121,25 +120,13 @@ module Liquid
def sort(input, property = nil)
ary = InputIterator.new(input)
if property.nil?
ary.sort do |a, b|
if !a.nil? && !b.nil?
a <=> b
else
a.nil? ? 1 : -1
end
end
ary.sort
elsif ary.empty? # The next two cases assume a non-empty array.
[]
elsif ary.all? { |el| el.respond_to?(:[]) }
ary.sort do |a, b|
a = a[property]
b = b[property]
if !a.nil? && !b.nil?
a <=> b
else
a.nil? ? 1 : -1
end
end
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) }
end
end
@@ -149,39 +136,13 @@ module Liquid
ary = InputIterator.new(input)
if property.nil?
ary.sort do |a, b|
if !a.nil? && !b.nil?
a.to_s.casecmp(b.to_s)
else
a.nil? ? 1 : -1
end
end
ary.sort { |a, b| a.casecmp(b) }
elsif ary.empty? # The next two cases assume a non-empty array.
[]
elsif ary.all? { |el| el.respond_to?(:[]) }
ary.sort do |a, b|
a = a[property]
b = b[property]
if !a.nil? && !b.nil?
a.to_s.casecmp(b.to_s)
else
a.nil? ? 1 : -1
end
end
end
end
# Filter the elements of an array to those with a certain property value.
# By default the target is any truthy value.
def where(input, property, target_value = nil)
ary = InputIterator.new(input)
if ary.empty?
[]
elsif ary.first.respond_to?(:[]) && target_value.nil?
ary.where_present(property)
elsif ary.first.respond_to?(:[])
ary.where(property, target_value)
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
@@ -230,6 +191,8 @@ 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
@@ -259,9 +222,6 @@ 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
@@ -332,12 +292,6 @@ 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, :+)
@@ -387,22 +341,6 @@ module Liquid
raise Liquid::FloatDomainError, e.message
end
def at_least(input, n)
min_value = Utils.to_number(n)
result = Utils.to_number(input)
result = min_value if min_value > result
result.is_a?(BigDecimal) ? result.to_f : result
end
def at_most(input, n)
max_value = Utils.to_number(n)
result = Utils.to_number(input)
result = max_value if max_value < result
result.is_a?(BigDecimal) ? result.to_f : result
end
def default(input, default_value = ''.freeze)
if !input || input.respond_to?(:empty?) && input.empty?
default_value
@@ -434,11 +372,11 @@ module Liquid
end
def join(glue)
to_a.join(glue.to_s)
to_a.join(glue)
end
def concat(args)
to_a.concat(args)
to_a.concat args
end
def reverse
@@ -460,27 +398,9 @@ module Liquid
def each
@input.each do |e|
yield(e.respond_to?(:to_liquid) ? e.to_liquid : e)
yield(e.to_liquid)
end
end
def where(property, target_value)
select do |item|
item[property] == target_value
end
rescue TypeError
# Cannot index with the given property type (eg. indexing integers with strings
# which are only allowed to be indexed by other integers).
raise ArgumentError.new("cannot select the property `#{property}`")
end
def where_present(property)
select { |item| item[property] }
rescue TypeError
# Cannot index with the given property type (eg. indexing integers with strings
# which are only allowed to be indexed by other integers).
raise ArgumentError.new("cannot select the property `#{property}`")
end
end
end

View File

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

@@ -10,8 +10,6 @@ module Liquid
class Assign < Tag
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
attr_reader :to, :from
def initialize(tag_name, markup, options)
super
if markup =~ Syntax
@@ -25,34 +23,16 @@ module Liquid
def render(context)
val = @from.render(context)
context.scopes.last[@to] = val
context.resource_limits.assign_score += assign_score_of(val)
inc = val.instance_of?(String) || val.instance_of?(Array) || val.instance_of?(Hash) ? val.length : 1
context.resource_limits.assign_score += inc
''.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
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[@node.from]
end
end
end
Template.register_tag('assign'.freeze, Assign)

View File

@@ -3,8 +3,6 @@ module Liquid
Syntax = /(#{QuotedFragment})/o
WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om
attr_reader :blocks, :left
def initialize(tag_name, markup, options)
super
@blocks = []
@@ -82,12 +80,6 @@ module Liquid
block.attach(BlockBody.new)
@blocks << block
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[@node.left] + @node.blocks
end
end
end
Template.register_tag('case'.freeze, Case)

View File

@@ -15,8 +15,6 @@ module Liquid
SimpleSyntax = /\A#{QuotedFragment}+/o
NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om
attr_reader :variables
def initialize(tag_name, markup, options)
super
case markup
@@ -32,11 +30,11 @@ module Liquid
end
def render(context)
context.registers[:cycle] ||= {}
context.registers[:cycle] ||= Hash.new(0)
context.stack do
key = context.evaluate(@name)
iteration = context.registers[:cycle][key].to_i
iteration = context.registers[:cycle][key]
result = context.evaluate(@variables[iteration])
iteration += 1
iteration = 0 if iteration >= @variables.size
@@ -53,12 +51,6 @@ module Liquid
$1 ? Expression.parse($1) : nil
end.compact
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
Array(@node.variables)
end
end
end
Template.register_tag('cycle', Cycle)

View File

@@ -23,7 +23,7 @@ module Liquid
# {{ item.name }}
# {% 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:
#
@@ -46,14 +46,10 @@ module Liquid
class For < Block
Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
attr_reader :collection_name, :variable_name, :limit, :from
def initialize(tag_name, markup, options)
super
@from = @limit = nil
parse_with_selected_parser(markup)
@for_block = BlockBody.new
@else_block = nil
end
def parse(tokens)
@@ -119,7 +115,7 @@ module Liquid
private
def collection_segment(context)
offsets = context.registers[:for] ||= {}
offsets = context.registers[:for] ||= Hash.new(0)
from = if @from == :continue
offsets[@name].to_i
@@ -155,7 +151,7 @@ module Liquid
begin
context['forloop'.freeze] = loop_vars
segment.each do |item|
segment.each_with_index do |item, index|
context[@variable_name] = item
result << @for_block.render(context)
loop_vars.send(:increment!)
@@ -191,12 +187,6 @@ module Liquid
def render_else(context)
@else_block ? @else_block.render(context) : ''.freeze
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
(super + [@node.limit, @node.from, @node.collection_name]).compact
end
end
end
Template.register_tag('for'.freeze, For)

View File

@@ -14,23 +14,21 @@ module Liquid
ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o
BOOLEAN_OPERATORS = %w(and or)
attr_reader :blocks
def initialize(tag_name, markup, options)
super
@blocks = []
push_block('if'.freeze, markup)
end
def nodelist
@blocks.map(&:attachment)
end
def parse(tokens)
while parse_body(@blocks.last.attachment, tokens)
end
end
def nodelist
@blocks.map(&:attachment)
end
def unknown_tag(tag, markup, tokens)
if ['elsif'.freeze, 'else'.freeze].include?(tag)
push_block(tag, markup)
@@ -85,20 +83,17 @@ module Liquid
def strict_parse(markup)
p = Parser.new(markup)
condition = parse_binary_comparisons(p)
condition = parse_binary_comparison(p)
p.consume(:end_of_string)
condition
end
def parse_binary_comparisons(p)
def parse_binary_comparison(p)
condition = parse_comparison(p)
first_condition = condition
while op = (p.id?('and'.freeze) || p.id?('or'.freeze))
child_condition = parse_comparison(p)
condition.send(op, child_condition)
condition = child_condition
if op = (p.id?('and'.freeze) || p.id?('or'.freeze))
condition.send(op, parse_binary_comparison(p))
end
first_condition
condition
end
def parse_comparison(p)
@@ -110,12 +105,6 @@ module Liquid
Condition.new(a)
end
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
@node.blocks
end
end
end
Template.register_tag('if'.freeze, If)

View File

@@ -16,8 +16,6 @@ module Liquid
class Include < Tag
Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/o
attr_reader :template_name_expr, :variable_name_expr, :attributes
def initialize(tag_name, markup, options)
super
@@ -44,15 +42,14 @@ module Liquid
def render(context)
template_name = context.evaluate(@template_name_expr)
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
context.evaluate(@variable_name_expr)
else
context.find_variable(template_name, raise_on_not_found: false)
context.find_variable(template_name)
end
old_template_name = context.template_name
@@ -109,15 +106,6 @@ module Liquid
file_system.read_template_file(context.evaluate(@template_name_expr))
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[
@node.template_name_expr,
@node.variable_name_expr
] + @node.attributes.values
end
end
end
Template.register_tag('include'.freeze, Include)

View File

@@ -6,7 +6,9 @@ module Liquid
def initialize(tag_name, markup, parse_context)
super
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
def parse(tokens)
@@ -33,14 +35,6 @@ 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

@@ -2,8 +2,6 @@ module Liquid
class TableRow < Block
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
attr_reader :variable_name, :collection_name, :attributes
def initialize(tag_name, markup, options)
super
if markup =~ Syntax
@@ -35,7 +33,7 @@ module Liquid
tablerowloop = Liquid::TablerowloopDrop.new(length, cols)
context['tablerowloop'.freeze] = tablerowloop
collection.each do |item|
collection.each_with_index do |item, index|
context[@variable_name] = item
result << "<td class=\"col#{tablerowloop.col}\">" << super << '</td>'
@@ -50,12 +48,6 @@ module Liquid
result << "</tr>\n"
result
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
super + @node.attributes.values + [@node.collection_name]
end
end
end
Template.register_tag('tablerow'.freeze, TableRow)

View File

@@ -19,10 +19,8 @@ module Liquid
@@file_system = BlankFileSystem.new
class TagRegistry
include Enumerable
def initialize
@tags = {}
@tags = {}
@cache = {}
end
@@ -43,10 +41,6 @@ module Liquid
@cache.delete(tag_name)
end
def each(&block)
@tags.each(&block)
end
private
def lookup_class(name)
@@ -69,11 +63,6 @@ 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
@@ -91,11 +80,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
@@ -118,7 +107,6 @@ module Liquid
end
def initialize
@rethrow_errors = false
@resource_limits = ResourceLimits.new(self.class.default_resource_limits)
end
@@ -172,7 +160,7 @@ module Liquid
c = args.shift
if @rethrow_errors
c.exception_renderer = ->(e) { raise }
c.exception_handler = ->(e) { raise }
end
c
@@ -193,7 +181,12 @@ module Liquid
registers.merge!(options[:registers]) if options[:registers].is_a?(Hash)
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_handler = options[:exception_handler] if options[:exception_handler]
when Module, Array
context.add_filters(args.pop)
end
@@ -242,13 +235,5 @@ 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 = line_numbers ? 1 : nil
@line_number = 1 if line_numbers
@tokens = tokenize
end

View File

@@ -46,17 +46,13 @@ module Liquid
def self.to_number(obj)
case obj
when Float
BigDecimal(obj.to_s)
BigDecimal.new(obj.to_s)
when Numeric
obj
when String
(obj.strip =~ /\A-?\d+\.\d+\z/) ? BigDecimal(obj) : obj.to_i
(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
0
end
end

View File

@@ -10,16 +10,10 @@ module Liquid
# {{ user | link }}
#
class Variable
FilterMarkupRegex = /#{FilterSeparator}\s*(.*)/om
FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o
FilterArgsRegex = /(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o
JustTagAttributes = /\A#{TagAttributes}\z/o
MarkupWithQuotedFragment = /(#{QuotedFragment})(.*)/om
attr_accessor :filters, :name, :line_number
attr_reader :parse_context
alias_method :options, :parse_context
include ParserSwitching
def initialize(markup, parse_context)
@@ -41,17 +35,17 @@ module Liquid
def lax_parse(markup)
@filters = []
return unless markup =~ MarkupWithQuotedFragment
return unless markup =~ /(#{QuotedFragment})(.*)/om
name_markup = $1
filter_markup = $2
@name = Expression.parse(name_markup)
if filter_markup =~ FilterMarkupRegex
if filter_markup =~ /#{FilterSeparator}\s*(.*)/om
filters = $1.scan(FilterParser)
filters.each do |f|
next unless f =~ /\w+/
filtername = Regexp.last_match(0)
filterargs = f.scan(FilterArgsRegex).flatten
filterargs = f.scan(/(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o).flatten
@filters << parse_filter_expressions(filtername, filterargs)
end
end
@@ -97,7 +91,7 @@ module Liquid
filter_args = []
keyword_args = {}
unparsed_args.each do |a|
if matches = a.match(JustTagAttributes)
if matches = a.match(/\A#{TagAttributes}\z/o)
keyword_args[matches[1]] = Expression.parse(matches[2])
else
filter_args << Expression.parse(a)
@@ -138,11 +132,5 @@ module Liquid
raise error
end
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[@node.name] + @node.filters.flatten
end
end
end
end

View File

@@ -55,11 +55,9 @@ 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 or
# raise an exception if `strict_variables` option is set to true
# keywords either. The only thing we got left is to return nil
else
return nil unless context.strict_variables
raise Liquid::UndefinedVariable, "undefined variable #{key}"
return nil
end
# If we are dealing with a drop here we have to
@@ -78,11 +76,5 @@ module Liquid
def state
[@name, @lookups, @command_flags]
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
@node.lookups
end
end
end
end

View File

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

View File

@@ -1,5 +1,4 @@
# encoding: utf-8
lib = File.expand_path('../lib/', __FILE__)
$LOAD_PATH.unshift lib unless $LOAD_PATH.include?(lib)
@@ -16,7 +15,6 @@ 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}/**/*")
@@ -26,6 +24,6 @@ Gem::Specification.new do |s|
s.require_path = "lib"
s.add_development_dependency 'rake', '~> 11.3'
s.add_development_dependency 'rake'
s.add_development_dependency 'minitest'
end

View File

@@ -5,7 +5,7 @@ Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new
Benchmark.ips do |x|
x.time = 10
x.time = 60
x.warmup = 5
puts
@@ -13,6 +13,5 @@ Benchmark.ips do |x|
puts
x.report("parse:") { profiler.compile }
x.report("render:") { profiler.render }
x.report("parse & render:") { profiler.run }
x.report("parse & run:") { profiler.run }
end

View File

@@ -21,100 +21,53 @@ class ThemeRunner
end
end
# Initialize a new liquid ThemeRunner instance
# Will load all templates into memory, do this now so that we don't profile IO.
# Load all templates into memory, do this now so that
# we don't profile IO.
def initialize
@tests = Dir[__dir__ + '/tests/**/*.liquid'].collect do |test|
next if File.basename(test) == 'theme.liquid'
theme_path = File.dirname(test) + '/theme.liquid'
{
liquid: File.read(test),
layout: (File.file?(theme_path) ? File.read(theme_path) : nil),
template_name: test
}
end.compact
compile_all_tests
[File.read(test), (File.file?(theme_path) ? File.read(theme_path) : nil), test]
end.compact
end
# `compile` will test just the compilation portion of liquid without any templates
def compile
@tests.each do |test_hash|
Liquid::Template.new.parse(test_hash[:liquid])
Liquid::Template.new.parse(test_hash[:layout])
# Dup assigns because will make some changes to them
@tests.each do |liquid, layout, template_name|
tmpl = Liquid::Template.new
tmpl.parse(liquid)
tmpl = Liquid::Template.new
tmpl.parse(layout)
end
end
# `run` is called to benchmark rendering and compiling at the same time
def run
each_test do |liquid, layout, assigns, page_template, template_name|
# Dup assigns because will make some changes to them
assigns = Database.tables.dup
@tests.each do |liquid, layout, template_name|
# Compute page_tempalte outside of profiler run, uninteresting to profiler
page_template = File.basename(template_name, File.extname(template_name))
compile_and_render(liquid, layout, assigns, page_template, template_name)
end
end
# `render` is called to benchmark just the render portion of liquid
def render
@compiled_tests.each do |test|
tmpl = test[:tmpl]
assigns = test[:assigns]
layout = test[:layout]
if layout
assigns['content_for_layout'] = tmpl.render!(assigns)
layout.render!(assigns)
else
tmpl.render!(assigns)
end
end
end
private
def compile_and_render(template, layout, assigns, page_template, template_file)
compiled_test = compile_test(template, layout, assigns, page_template, template_file)
assigns['content_for_layout'] = compiled_test[:tmpl].render!(assigns)
compiled_test[:layout].render!(assigns) if layout
end
def compile_all_tests
@compiled_tests = []
each_test do |liquid, layout, assigns, page_template, template_name|
@compiled_tests << compile_test(liquid, layout, assigns, page_template, template_name)
end
@compiled_tests
end
def compile_test(template, layout, assigns, page_template, template_file)
tmpl = init_template(page_template, template_file)
parsed_template = tmpl.parse(template).dup
if layout
parsed_layout = tmpl.parse(layout)
{ tmpl: parsed_template, assigns: assigns, layout: parsed_layout }
else
{ tmpl: parsed_template, assigns: assigns }
end
end
# utility method with similar functionality needed in `compile_all_tests` and `run`
def each_test
# Dup assigns because will make some changes to them
assigns = Database.tables.dup
@tests.each do |test_hash|
# Compute page_template outside of profiler run, uninteresting to profiler
page_template = File.basename(test_hash[:template_name], File.extname(test_hash[:template_name]))
yield(test_hash[:liquid], test_hash[:layout], assigns, page_template, test_hash[:template_name])
end
end
# set up a new Liquid::Template object for use in `compile_and_render` and `compile_test`
def init_template(page_template, template_file)
tmpl = Liquid::Template.new
tmpl.assigns['page_title'] = 'Page title'
tmpl.assigns['template'] = page_template
tmpl.registers[:file_system] = ThemeRunner::FileSystem.new(File.dirname(template_file))
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

View File

@@ -1,12 +0,0 @@
require 'test_helper'
class BlockTest < Minitest::Test
include Liquid
def test_unexpected_end_tag
exc = assert_raises(SyntaxError) do
Template.parse("{% if true %}{% endunless %}")
end
assert_equal exc.message, "Liquid syntax error: 'endunless' is not a valid delimiter for if tags. use endif"
end
end

View File

@@ -94,23 +94,7 @@ class ErrorHandlingTest < Minitest::Test
)
end
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)
assert_match /Liquid syntax error \(line 4\)/, err.message
end
def test_parsing_warn_with_line_numbers_adds_numbers_to_lexer_errors
@@ -202,40 +186,22 @@ class ErrorHandlingTest < Minitest::Test
end
end
def test_default_exception_renderer_with_internal_error
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
template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true)
output = template.render({ 'errors' => ErrorDrop.new })
handler = ->(e) { e.is_a?(Liquid::Error) ? e : InternalError.new('internal') }
output = template.render({ 'errors' => ErrorDrop.new }, exception_handler: handler)
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
assert_equal [InternalError], template.errors.map(&:class)
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

@@ -1,247 +0,0 @@
# frozen_string_literal: true
require 'test_helper'
class ParseTreeVisitorTest < Minitest::Test
include Liquid
def test_variable
assert_equal(
["test"],
visit(%({{ test }}))
)
end
def test_varible_with_filter
assert_equal(
["test", "infilter"],
visit(%({{ test | split: infilter }}))
)
end
def test_dynamic_variable
assert_equal(
["test", "inlookup"],
visit(%({{ test[inlookup] }}))
)
end
def test_if_condition
assert_equal(
["test"],
visit(%({% if test %}{% endif %}))
)
end
def test_complex_if_condition
assert_equal(
["test"],
visit(%({% if 1 == 1 and 2 == test %}{% endif %}))
)
end
def test_if_body
assert_equal(
["test"],
visit(%({% if 1 == 1 %}{{ test }}{% endif %}))
)
end
def test_unless_condition
assert_equal(
["test"],
visit(%({% unless test %}{% endunless %}))
)
end
def test_complex_unless_condition
assert_equal(
["test"],
visit(%({% unless 1 == 1 and 2 == test %}{% endunless %}))
)
end
def test_unless_body
assert_equal(
["test"],
visit(%({% unless 1 == 1 %}{{ test }}{% endunless %}))
)
end
def test_elsif_condition
assert_equal(
["test"],
visit(%({% if 1 == 1 %}{% elsif test %}{% endif %}))
)
end
def test_complex_elsif_condition
assert_equal(
["test"],
visit(%({% if 1 == 1 %}{% elsif 1 == 1 and 2 == test %}{% endif %}))
)
end
def test_elsif_body
assert_equal(
["test"],
visit(%({% if 1 == 1 %}{% elsif 2 == 2 %}{{ test }}{% endif %}))
)
end
def test_else_body
assert_equal(
["test"],
visit(%({% if 1 == 1 %}{% else %}{{ test }}{% endif %}))
)
end
def test_case_left
assert_equal(
["test"],
visit(%({% case test %}{% endcase %}))
)
end
def test_case_condition
assert_equal(
["test"],
visit(%({% case 1 %}{% when test %}{% endcase %}))
)
end
def test_case_when_body
assert_equal(
["test"],
visit(%({% case 1 %}{% when 2 %}{{ test }}{% endcase %}))
)
end
def test_case_else_body
assert_equal(
["test"],
visit(%({% case 1 %}{% else %}{{ test }}{% endcase %}))
)
end
def test_for_in
assert_equal(
["test"],
visit(%({% for x in test %}{% endfor %}))
)
end
def test_for_limit
assert_equal(
["test"],
visit(%({% for x in (1..5) limit: test %}{% endfor %}))
)
end
def test_for_offset
assert_equal(
["test"],
visit(%({% for x in (1..5) offset: test %}{% endfor %}))
)
end
def test_for_body
assert_equal(
["test"],
visit(%({% for x in (1..5) %}{{ test }}{% endfor %}))
)
end
def test_tablerow_in
assert_equal(
["test"],
visit(%({% tablerow x in test %}{% endtablerow %}))
)
end
def test_tablerow_limit
assert_equal(
["test"],
visit(%({% tablerow x in (1..5) limit: test %}{% endtablerow %}))
)
end
def test_tablerow_offset
assert_equal(
["test"],
visit(%({% tablerow x in (1..5) offset: test %}{% endtablerow %}))
)
end
def test_tablerow_body
assert_equal(
["test"],
visit(%({% tablerow x in (1..5) %}{{ test }}{% endtablerow %}))
)
end
def test_cycle
assert_equal(
["test"],
visit(%({% cycle test %}))
)
end
def test_assign
assert_equal(
["test"],
visit(%({% assign x = test %}))
)
end
def test_capture
assert_equal(
["test"],
visit(%({% capture x %}{{ test }}{% endcapture %}))
)
end
def test_include
assert_equal(
["test"],
visit(%({% include test %}))
)
end
def test_include_with
assert_equal(
["test"],
visit(%({% include "hai" with test %}))
)
end
def test_include_for
assert_equal(
["test"],
visit(%({% include "hai" for test %}))
)
end
def test_preserve_tree_structure
assert_equal(
[[nil, [
[nil, [[nil, [["other", []]]]]],
["test", []],
["xs", []]
]]],
traversal(%({% for x in xs offset: test %}{{ other }}{% endfor %})).visit
)
end
private
def traversal(template)
ParseTreeVisitor
.for(Template.parse(template).root)
.add_callback_for(VariableLookup, &:name)
end
def visit(template)
traversal(template).visit.flatten.compact
end
end

View File

@@ -115,8 +115,4 @@ class ParsingQuirksTest < Minitest::Test
assert_template_result('12345', "{% for i in (1...5) %}{{ i }}{% endfor %}")
end
end
def test_contains_in_id
assert_template_result(' YES ', '{% if containsallshipments == true %} YES {% endif %}', 'containsallshipments' => true)
end
end # ParsingQuirksTest

View File

@@ -9,10 +9,6 @@ end
class SecurityTest < Minitest::Test
include Liquid
def setup
@assigns = {}
end
def test_no_instance_eval
text = %( {{ '1+1' | instance_eval }} )
expected = %( 1+1 )
@@ -63,18 +59,4 @@ class SecurityTest < Minitest::Test
assert_equal [], (Symbol.all_symbols - current_symbols)
end
def test_max_depth_nested_blocks_does_not_raise_exception
depth = Liquid::Block::MAX_DEPTH
code = "{% if true %}" * depth + "rendered" + "{% endif %}" * depth
assert_equal "rendered", Template.parse(code).render!
end
def test_more_than_max_depth_nested_blocks_raises_exception
depth = Liquid::Block::MAX_DEPTH + 1
code = "{% if true %}" * depth + "rendered" + "{% endif %}" * depth
assert_raises(Liquid::StackLevelError) do
Template.parse(code).render!
end
end
end # SecurityTest

View File

@@ -41,16 +41,6 @@ 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
@@ -115,29 +105,21 @@ 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
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 nil, @filters.escape(nil)
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
def test_escape_once
@@ -146,18 +128,14 @@ class StandardFiltersTest < Minitest::Test
def test_url_encode
assert_equal 'foo%2B1%40example.com', @filters.url_encode('foo+1@example.com')
assert_equal '1', @filters.url_encode(1)
assert_equal '2001-02-03', @filters.url_encode(Date.new(2001, 2, 3))
assert_nil @filters.url_encode(nil)
assert_equal 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)
assert_equal nil, @filters.url_decode(nil)
end
def test_truncatewords
@@ -166,7 +144,6 @@ 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
@@ -182,7 +159,6 @@ class StandardFiltersTest < Minitest::Test
def test_join
assert_equal '1 2 3 4', @filters.join([1, 2, 3, 4])
assert_equal '1 - 2 - 3 - 4', @filters.join([1, 2, 3, 4], ' - ')
assert_equal '1121314', @filters.join([1, 2, 3, 4], 1)
end
def test_sort
@@ -190,80 +166,6 @@ 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_with_nils
assert_equal [1, 2, 3, 4, nil], @filters.sort([nil, 4, 3, 2, 1])
assert_equal [{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }, {}], @filters.sort([{ "a" => 4 }, { "a" => 3 }, {}, { "a" => 1 }, { "a" => 2 }], "a")
end
def test_sort_when_property_is_sometimes_missing_puts_nils_last
input = [
{ "price" => 4, "handle" => "alpha" },
{ "handle" => "beta" },
{ "price" => 1, "handle" => "gamma" },
{ "handle" => "delta" },
{ "price" => 2, "handle" => "epsilon" }
]
expectation = [
{ "price" => 1, "handle" => "gamma" },
{ "price" => 2, "handle" => "epsilon" },
{ "price" => 4, "handle" => "alpha" },
{ "handle" => "delta" },
{ "handle" => "beta" }
]
assert_equal expectation, @filters.sort(input, "price")
end
def test_sort_natural
assert_equal ["a", "B", "c", "D"], @filters.sort_natural(["c", "D", "a", "B"])
assert_equal [{ "a" => "a" }, { "a" => "B" }, { "a" => "c" }, { "a" => "D" }], @filters.sort_natural([{ "a" => "D" }, { "a" => "c" }, { "a" => "a" }, { "a" => "B" }], "a")
end
def test_sort_natural_with_nils
assert_equal ["a", "B", "c", "D", nil], @filters.sort_natural([nil, "c", "D", "a", "B"])
assert_equal [{ "a" => "a" }, { "a" => "B" }, { "a" => "c" }, { "a" => "D" }, {}], @filters.sort_natural([{ "a" => "D" }, { "a" => "c" }, {}, { "a" => "a" }, { "a" => "B" }], "a")
end
def test_sort_natural_when_property_is_sometimes_missing_puts_nils_last
input = [
{ "price" => "4", "handle" => "alpha" },
{ "handle" => "beta" },
{ "price" => "1", "handle" => "gamma" },
{ "handle" => "delta" },
{ "price" => 2, "handle" => "epsilon" }
]
expectation = [
{ "price" => "1", "handle" => "gamma" },
{ "price" => 2, "handle" => "epsilon" },
{ "price" => "4", "handle" => "alpha" },
{ "handle" => "delta" },
{ "handle" => "beta" }
]
assert_equal expectation, @filters.sort_natural(input, "price")
end
def test_sort_natural_case_check
input = [
{ "key" => "X" },
{ "key" => "Y" },
{ "key" => "Z" },
{ "fake" => "t" },
{ "key" => "a" },
{ "key" => "b" },
{ "key" => "c" }
]
expectation = [
{ "key" => "a" },
{ "key" => "b" },
{ "key" => "c" },
{ "key" => "X" },
{ "key" => "Y" },
{ "key" => "Z" },
{ "fake" => "t" }
]
assert_equal expectation, @filters.sort_natural(input, "key")
assert_equal ["a", "b", "c", "X", "Y", "Z"], @filters.sort_natural(["X", "Y", "Z", "a", "b", "c"])
end
def test_sort_empty_array
assert_equal [], @filters.sort([], "a")
end
@@ -398,7 +300,7 @@ class StandardFiltersTest < Minitest::Test
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")
@@ -411,8 +313,8 @@ class StandardFiltersTest < Minitest::Test
def test_first_last
assert_equal 1, @filters.first([1, 2, 3])
assert_equal 3, @filters.last([1, 2, 3])
assert_nil @filters.first([])
assert_nil @filters.last([])
assert_equal nil, @filters.first([])
assert_equal nil, @filters.last([])
end
def test_replace
@@ -462,38 +364,20 @@ 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
@@ -507,8 +391,6 @@ 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
@@ -516,8 +398,6 @@ 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
@@ -527,9 +407,6 @@ 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
@@ -538,8 +415,6 @@ 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
@@ -548,30 +423,6 @@ 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_at_most
assert_template_result "4", "{{ 5 | at_most:4 }}"
assert_template_result "5", "{{ 5 | at_most:5 }}"
assert_template_result "5", "{{ 5 | at_most:6 }}"
assert_template_result "4.5", "{{ 4.5 | at_most:5 }}"
assert_template_result "5", "{{ width | at_most:5 }}", 'width' => NumberLikeThing.new(6)
assert_template_result "4", "{{ width | at_most:5 }}", 'width' => NumberLikeThing.new(4)
assert_template_result "4", "{{ 5 | at_most: width }}", 'width' => NumberLikeThing.new(4)
end
def test_at_least
assert_template_result "5", "{{ 5 | at_least:4 }}"
assert_template_result "5", "{{ 5 | at_least:5 }}"
assert_template_result "6", "{{ 5 | at_least:6 }}"
assert_template_result "5", "{{ 4.5 | at_least:5 }}"
assert_template_result "6", "{{ width | at_least:5 }}", 'width' => NumberLikeThing.new(6)
assert_template_result "5", "{{ width | at_least:5 }}", 'width' => NumberLikeThing.new(4)
assert_template_result "6", "{{ 5 | at_least: width }}", 'width' => NumberLikeThing.new(6)
end
def test_append
@@ -585,7 +436,8 @@ 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(Liquid::ArgumentError, "concat filter requires an array argument") do
assert_raises(TypeError) do
# no implicit conversion of Fixnum into Array
@filters.concat([1, 2], 10)
end
end
@@ -614,78 +466,6 @@ class StandardFiltersTest < Minitest::Test
assert_template_result('abc', "{{ 'abc' | date: '%D' }}")
end
def test_where
input = [
{ "handle" => "alpha", "ok" => true },
{ "handle" => "beta", "ok" => false },
{ "handle" => "gamma", "ok" => false },
{ "handle" => "delta", "ok" => true }
]
expectation = [
{ "handle" => "alpha", "ok" => true },
{ "handle" => "delta", "ok" => true }
]
assert_equal expectation, @filters.where(input, "ok", true)
assert_equal expectation, @filters.where(input, "ok")
end
def test_where_no_key_set
input = [
{ "handle" => "alpha", "ok" => true },
{ "handle" => "beta" },
{ "handle" => "gamma" },
{ "handle" => "delta", "ok" => true }
]
expectation = [
{ "handle" => "alpha", "ok" => true },
{ "handle" => "delta", "ok" => true }
]
assert_equal expectation, @filters.where(input, "ok", true)
assert_equal expectation, @filters.where(input, "ok")
end
def test_where_non_array_map_input
assert_equal [{ "a" => "ok" }], @filters.where({ "a" => "ok" }, "a", "ok")
assert_equal [], @filters.where({ "a" => "not ok" }, "a", "ok")
end
def test_where_indexable_but_non_map_value
assert_raises(Liquid::ArgumentError) { @filters.where(1, "ok", true) }
assert_raises(Liquid::ArgumentError) { @filters.where(1, "ok") }
end
def test_where_non_boolean_value
input = [
{ "message" => "Bonjour!", "language" => "French" },
{ "message" => "Hello!", "language" => "English" },
{ "message" => "Hallo!", "language" => "German" }
]
assert_equal [{ "message" => "Bonjour!", "language" => "French" }], @filters.where(input, "language", "French")
assert_equal [{ "message" => "Hallo!", "language" => "German" }], @filters.where(input, "language", "German")
assert_equal [{ "message" => "Hello!", "language" => "English" }], @filters.where(input, "language", "English")
end
def test_where_array_of_only_unindexable_values
assert_nil @filters.where([nil], "ok", true)
assert_nil @filters.where([nil], "ok")
end
def test_where_no_target_value
input = [
{ "foo" => false },
{ "foo" => true },
{ "foo" => "for sure" },
{ "bar" => true }
]
assert_equal [{ "foo" => true }, { "foo" => "for sure" }], @filters.where(input, "foo")
end
private
def with_timezone(tz)

View File

@@ -159,7 +159,7 @@ HERE
assert_template_result(expected, markup, assigns)
end
def test_pause_resume_big_limit
def test_pause_resume_BIG_limit
assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } }
markup = <<-MKUP
{%for i in array.items limit:3 %}{{i}}{%endfor%}
@@ -178,7 +178,7 @@ HERE
assert_template_result(expected, markup, assigns)
end
def test_pause_resume_big_offset
def test_pause_resume_BIG_offset
assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } }
markup = '{%for i in array.items limit:3 %}{{i}}{%endfor%}
next

View File

@@ -137,7 +137,7 @@ class IncludeTagTest < Minitest::Test
Liquid::Template.file_system = infinite_file_system.new
assert_raises(Liquid::StackLevelError) do
assert_raises(Liquid::StackLevelError, SystemStackError) do
Template.parse("{% include 'loop' %}").render!
end
end
@@ -217,17 +217,6 @@ 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 %}"
@@ -235,11 +224,4 @@ class IncludeTagTest < Minitest::Test
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

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,12 +27,6 @@ class ErroneousDrop < Liquid::Drop
end
end
class DropWithUndefinedMethod < Liquid::Drop
def foo
'foo'
end
end
class TemplateTest < Minitest::Test
include Liquid
@@ -139,17 +133,6 @@ 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
@@ -215,20 +198,16 @@ class TemplateTest < Minitest::Test
assert_equal 'ruby error in drop', e.message
end
def test_exception_renderer_that_returns_string
def test_exception_handler_doesnt_reraise_if_it_returns_false
exception = nil
handler = ->(e) { exception = e; '<!-- error -->' }
output = Template.parse("{{ 1 | divided_by: 0 }}").render({}, exception_renderer: handler)
Template.parse("{{ 1 | divided_by: 0 }}").render({}, exception_handler: ->(e) { exception = e; false })
assert exception.is_a?(Liquid::ZeroDivisionError)
assert_equal '<!-- error -->', output
end
def test_exception_renderer_that_raises
def test_exception_handler_does_reraise_if_it_returns_true
exception = nil
assert_raises(Liquid::ZeroDivisionError) do
Template.parse("{{ 1 | divided_by: 0 }}").render({}, exception_renderer: ->(e) { exception = e; raise })
Template.parse("{{ 1 | divided_by: 0 }}").render({}, exception_handler: ->(e) { exception = e; true })
end
assert exception.is_a?(Liquid::ZeroDivisionError)
end
@@ -246,87 +225,4 @@ 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_nil_value_does_not_raise
Liquid::Template.error_mode = :strict
t = Template.parse("some{{x}}thing")
result = t.render!({ 'x' => nil }, strict_variables: true)
assert_equal 0, t.errors.count
assert_equal 'something', result
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

@@ -1,529 +0,0 @@
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_right_trim_followed_by_tag
assert_template_result('ab c', '{{ "a" -}}{{ "b" }} c')
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

@@ -89,8 +89,4 @@ class VariableTest < Minitest::Test
def test_multiline_variable
assert_equal 'worked', Template.parse("{{\ntest\n}}").render!('test' => 'worked')
end
def test_render_symbol
assert_template_result 'bar', '{{ foo }}', 'foo' => :bar
end
end

View File

@@ -2,6 +2,7 @@
ENV["MT_NO_EXPECTATIONS"] = "1"
require 'minitest/autorun'
require 'spy/integration'
$LOAD_PATH.unshift(File.join(File.expand_path(__dir__), '..', 'lib'))
require 'liquid.rb'

View File

@@ -46,8 +46,6 @@ 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,54 +3,50 @@ 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_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
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
# negative numbers
assert_evaluates_true 1, '>', -1
assert_evaluates_true (-1), '<', 1
assert_evaluates_true 1.0, '>', -1.0
assert_evaluates_true (-1.0), '<', 1.0
assert_evalutes_true 1, '>', -1
assert_evalutes_true -1, '<', 1
assert_evalutes_true 1.0, '>', -1.0
assert_evalutes_true -1.0, '<', 1.0
end
def test_default_operators_evalute_false
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
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
end
def test_contains_works_on_strings
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_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_false 'bob', 'contains', 'bob2'
assert_evaluates_false 'bob', 'contains', 'a'
assert_evaluates_false 'bob', 'contains', '---'
assert_evalutes_false 'bob', 'contains', 'bob2'
assert_evalutes_false 'bob', 'contains', 'a'
assert_evalutes_false 'bob', 'contains', '---'
end
def test_invalid_comparation_operator
@@ -64,42 +60,34 @@ class ConditionUnitTest < Minitest::Test
assert_evaluates_argument_error '1', '<=', 0
end
def test_hash_compare_backwards_compatibility
assert_nil Condition.new({}, '>', 2).evaluate
assert_nil Condition.new(2, '>', {}).evaluate
assert_equal false, Condition.new({}, '==', 2).evaluate
assert_equal true, Condition.new({ 'a' => 1 }, '==', { 'a' => 1 }).evaluate
assert_equal true, Condition.new({ 'a' => 2 }, 'contains', 'a').evaluate
end
def test_contains_works_on_arrays
@context = Liquid::Context.new
@context['array'] = [1, 2, 3, 4, 5]
array_expr = VariableLookup.new("array")
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"
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"
end
def test_contains_returns_false_for_nil_operands
@context = Liquid::Context.new
assert_evaluates_false VariableLookup.new('not_assigned'), 'contains', '0'
assert_evaluates_false 0, 'contains', VariableLookup.new('not_assigned')
assert_evalutes_false VariableLookup.new('not_assigned'), 'contains', '0'
assert_evalutes_false 0, 'contains', VariableLookup.new('not_assigned')
end
def test_contains_return_false_on_wrong_data_type
assert_evaluates_false 1, 'contains', 0
assert_evalutes_false 1, 'contains', 0
end
def test_contains_with_string_left_operand_coerces_right_operand_to_string
assert_evaluates_true ' 1 ', 'contains', 1
assert_evaluates_false ' 1 ', 'contains', 2
assert_evalutes_true ' 1 ', 'contains', 1
assert_evalutes_false ' 1 ', 'contains', 2
end
def test_or_condition
@@ -133,8 +121,8 @@ class ConditionUnitTest < Minitest::Test
def test_should_allow_custom_proc_operator
Condition.operators['starts_with'] = proc { |cond, left, right| left =~ %r{^#{right}} }
assert_evaluates_true 'bob', 'starts_with', 'b'
assert_evaluates_false 'bob', 'starts_with', 'o'
assert_evalutes_true 'bob', 'starts_with', 'b'
assert_evalutes_false 'bob', 'starts_with', 'o'
ensure
Condition.operators.delete 'starts_with'
end
@@ -143,24 +131,24 @@ class ConditionUnitTest < Minitest::Test
@context = Liquid::Context.new
@context['one'] = @context['another'] = "gnomeslab-and-or-liquid"
assert_evaluates_true VariableLookup.new("one"), '==', VariableLookup.new("another")
assert_evalutes_true VariableLookup.new("one"), '==', VariableLookup.new("another")
end
private
def assert_evaluates_true(left, op, right)
assert Condition.new(left, op, right).evaluate(@context),
def assert_evalutes_true(left, op, right)
assert Condition.new(left, op, right).evaluate(@context || Liquid::Context.new),
"Evaluated false: #{left} #{op} #{right}"
end
def assert_evaluates_false(left, op, right)
assert !Condition.new(left, op, right).evaluate(@context),
def assert_evalutes_false(left, op, right)
assert !Condition.new(left, op, right).evaluate(@context || Liquid::Context.new),
"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)
Condition.new(left, op, right).evaluate(@context || Liquid::Context.new)
end
end
end # ConditionTest

View File

@@ -70,6 +70,10 @@ class ContextUnitTest < Minitest::Test
@context = Liquid::Context.new
end
def teardown
Spy.teardown
end
def test_variables
@context['string'] = 'string'
assert_equal 'string', @context['string']
@@ -94,12 +98,12 @@ class ContextUnitTest < Minitest::Test
assert_equal false, @context['bool']
@context['nil'] = nil
assert_nil @context['nil']
assert_nil @context['nil']
assert_equal nil, @context['nil']
assert_equal nil, @context['nil']
end
def test_variables_not_existing
assert_nil @context['does_not_exist']
assert_equal nil, @context['does_not_exist']
end
def test_scoping
@@ -181,7 +185,7 @@ class ContextUnitTest < Minitest::Test
@context['test'] = 'test'
assert_equal 'test', @context['test']
@context.pop
assert_nil @context['test']
assert_equal nil, @context['test']
end
def test_hierachical_data
@@ -296,7 +300,7 @@ class ContextUnitTest < Minitest::Test
@context['hash'] = { 'first' => 'Hello' }
assert_equal 1, @context['array.first']
assert_nil @context['array["first"]']
assert_equal nil, @context['array["first"]']
assert_equal 'Hello', @context['hash["first"]']
end
@@ -446,10 +450,14 @@ class ContextUnitTest < Minitest::Test
assert_equal @context, @context['category'].context
end
def test_interrupt_avoids_object_allocations
assert_no_object_allocations do
@context.interrupt?
end
def test_use_empty_instead_of_any_in_interrupt_handling_to_avoid_lots_of_unnecessary_object_allocations
mock_any = Spy.on_instance_method(Array, :any?)
mock_empty = Spy.on_instance_method(Array, :empty?)
@context.interrupt?
refute mock_any.has_been_called?
assert mock_empty.has_been_called?
end
def test_context_initialization_with_a_proc_in_environment
@@ -472,18 +480,4 @@ class ContextUnitTest < Minitest::Test
context = Context.new
assert_equal 'hi', context.apply_global_filter('hi')
end
private
def assert_no_object_allocations
unless RUBY_ENGINE == 'ruby'
skip "stackprof needed to count object allocations"
end
require 'stackprof'
profile = StackProf.run(mode: :object) do
yield
end
assert_equal 0, profile[:samples]
end
end # ContextTest

View File

@@ -19,7 +19,7 @@ class LexerUnitTest < Minitest::Test
end
def test_comparison
tokens = Lexer.new('== <> contains ').tokenize
tokens = Lexer.new('== <> contains').tokenize
assert_equal [[:comparison, '=='], [:comparison, '<>'], [:comparison, 'contains'], [:end_of_string]], tokens
end

View File

@@ -77,88 +77,4 @@ 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
def test_add_filter_does_not_include_already_included_module
mod = Module.new do
class << self
attr_accessor :include_count
def included(mod)
self.include_count += 1
end
end
self.include_count = 0
end
strainer = Context.new.strainer
strainer.class.add_filter(mod)
strainer.class.add_filter(mod)
assert_equal 1, mod.include_count
end
end # StrainerTest

View File

@@ -67,12 +67,4 @@ 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