Compare commits

..
93 changed files with 874 additions and 2721 deletions
+17 -14
View File
@@ -1,36 +1,39 @@
name: Liquid name: Liquid
on: [push, pull_request] on: [push, pull_request]
env:
BUNDLE_JOBS: 4
BUNDLE_RETRY: 3
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
entry: entry:
- { ruby: 3.0, allowed-failure: false } # minimum supported - { ruby: 2.7, allowed-failure: false } # minimum supported
- { ruby: 3.2, allowed-failure: false } - { ruby: 3.1, allowed-failure: false } # latest
- { ruby: 3.3, allowed-failure: false } # latest - { ruby: ruby-head, allowed-failure: true }
- { ruby: ruby-head, allowed-failure: false } name: test (${{ matrix.entry.ruby }})
name: Test Ruby ${{ matrix.entry.ruby }}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: ruby/setup-ruby@v1 - uses: ruby/setup-ruby@v1
with: with:
ruby-version: ${{ matrix.entry.ruby }} ruby-version: ${{ matrix.entry.ruby }}
bundler-cache: true - uses: actions/cache@v1
bundler: latest with:
path: vendor/bundle
key: ${{ runner.os }}-gems-${{ hashFiles('Gemfile') }}
restore-keys: ${{ runner.os }}-gems-
- run: bundle install --jobs=3 --retry=3 --path=vendor/bundle
- run: bundle exec rake - run: bundle exec rake
continue-on-error: ${{ matrix.entry.allowed-failure }} continue-on-error: ${{ matrix.entry.allowed-failure }}
memory_profile: memory_profile:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: ruby/setup-ruby@v1 - uses: ruby/setup-ruby@v1
with: with:
bundler-cache: true ruby-version: 2.7
- uses: actions/cache@v1
with:
path: vendor/bundle
key: ${{ runner.os }}-gems-${{ hashFiles('Gemfile') }}
restore-keys: ${{ runner.os }}-gems-
- run: bundle install --jobs=3 --retry=3 --path=vendor/bundle
- run: bundle exec rake memory_profile:run - run: bundle exec rake memory_profile:run
+2 -1
View File
@@ -4,6 +4,7 @@
pkg pkg
*.rbc *.rbc
.rvmrc .rvmrc
.ruby-version
Gemfile.lock
.bundle .bundle
.byebug_history .byebug_history
Gemfile.lock
+4 -8
View File
@@ -10,8 +10,8 @@ Performance:
Enabled: true Enabled: true
AllCops: AllCops:
TargetRubyVersion: 2.7
NewCops: disable NewCops: disable
SuggestExtensions: false
Exclude: Exclude:
- 'vendor/bundle/**/*' - 'vendor/bundle/**/*'
@@ -19,10 +19,6 @@ Naming/MethodName:
Exclude: Exclude:
- 'example/server/liquid_servlet.rb' - 'example/server/liquid_servlet.rb'
Style/ClassMethodsDefinitions: # Backport https://github.com/Shopify/ruby-style-guide/pull/258
Enabled: false Layout/BeginEndAlignment:
Enabled: true
# liquid filter calls were being mistaken to be calls on arrays
Style/ConcatArrayLiterals:
Exclude:
- 'test/integration/standard_filter_test.rb'
-1
View File
@@ -1 +0,0 @@
3.3.4
+2 -5
View File
@@ -7,8 +7,6 @@ end
gemspec gemspec
gem "base64"
group :benchmark, :test do group :benchmark, :test do
gem 'benchmark-ips' gem 'benchmark-ips'
gem 'memory_profiler' gem 'memory_profiler'
@@ -20,11 +18,10 @@ group :benchmark, :test do
end end
group :test do group :test do
gem 'rubocop', '~> 1.61.0' gem 'rubocop-shopify', '~> 2.7.0', require: false
gem 'rubocop-shopify', '~> 2.12.0', require: false
gem 'rubocop-performance', require: false gem 'rubocop-performance', require: false
platform :mri, :truffleruby do platform :mri, :truffleruby do
gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'main' gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'master'
end end
end end
-4
View File
@@ -1,9 +1,5 @@
# Liquid Change Log # Liquid Change Log
## 5.5.0 2024-03-21
Please reference the GitHub release for more information.
## 5.4.0 2022-07-29 ## 5.4.0 2022-07-29
### Breaking Changes ### Breaking Changes
+6 -48
View File
@@ -1,11 +1,11 @@
[![Build status](https://github.com/Shopify/liquid/actions/workflows/liquid.yml/badge.svg)](https://github.com/Shopify/liquid/actions/workflows/liquid.yml) [![Build Status](https://api.travis-ci.org/Shopify/liquid.svg?branch=master)](http://travis-ci.org/Shopify/liquid)
[![Inline docs](http://inch-ci.org/github/Shopify/liquid.svg?branch=master)](http://inch-ci.org/github/Shopify/liquid) [![Inline docs](http://inch-ci.org/github/Shopify/liquid.svg?branch=master)](http://inch-ci.org/github/Shopify/liquid)
# Liquid template engine # Liquid template engine
* [Contributing guidelines](CONTRIBUTING.md) * [Contributing guidelines](CONTRIBUTING.md)
* [Version history](History.md) * [Version history](History.md)
* [Liquid documentation from Shopify](https://shopify.dev/docs/api/liquid) * [Liquid documentation from Shopify](https://shopify.dev/api/liquid)
* [Liquid Wiki at GitHub](https://github.com/Shopify/liquid/wiki) * [Liquid Wiki at GitHub](https://github.com/Shopify/liquid/wiki)
* [Website](http://liquidmarkup.org/) * [Website](http://liquidmarkup.org/)
@@ -52,47 +52,6 @@ For standard use you can just pass it the content of a file and call render with
@template.render('name' => 'tobi') # => "hi tobi" @template.render('name' => 'tobi') # => "hi tobi"
``` ```
### Concept of Environments
In Liquid, a "Environment" is a scoped environment that encapsulates custom tags, filters, and other configurations. This allows you to define and isolate different sets of functionality for different contexts, avoiding global overrides that can lead to conflicts and unexpected behavior.
By using environments, you can:
1. **Encapsulate Logic**: Keep the logic for different parts of your application separate.
2. **Avoid Conflicts**: Prevent custom tags and filters from clashing with each other.
3. **Improve Maintainability**: Make it easier to manage and understand the scope of customizations.
4. **Enhance Security**: Limit the availability of certain tags and filters to specific contexts.
We encourage the use of Environments over globally overriding things because it promotes better software design principles such as modularity, encapsulation, and separation of concerns.
Here's an example of how you can define and use Environments in Liquid:
```ruby
user_environment = Liquid::Environment.build do |environment|
environment.register_tag("renderobj", RenderObjTag)
end
Liquid::Template.parse(<<~LIQUID, environment: user_environment)
{% renderobj src: "path/to/model.obj" %}
LIQUID
```
In this example, `RenderObjTag` is a custom tag that is only available within the `user_environment`.
Similarly, you can define another environment for a different context, such as email templates:
```ruby
email_environment = Liquid::Environment.build do |environment|
environment.register_tag("unsubscribe_footer", UnsubscribeFooter)
end
Liquid::Template.parse(<<~LIQUID, environment: email_environment)
{% unsubscribe_footer %}
LIQUID
```
By using Environments, you ensure that custom tags and filters are only available in the contexts where they are needed, making your Liquid templates more robust and easier to manage.
### Error Modes ### Error Modes
Setting the error mode of Liquid lets you specify how strictly you want your templates to be interpreted. Setting the error mode of Liquid lets you specify how strictly you want your templates to be interpreted.
@@ -103,10 +62,9 @@ Liquid also comes with a stricter parser that can be used when editing templates
when templates are invalid. You can enable this new parser like this: when templates are invalid. You can enable this new parser like this:
```ruby ```ruby
Liquid::Environment.default.error_mode = :strict Liquid::Template.error_mode = :strict # Raises a SyntaxError when invalid syntax is used
Liquid::Environment.default.error_mode = :strict # Raises a SyntaxError when invalid syntax is used Liquid::Template.error_mode = :warn # Adds strict errors to template.errors but continues as normal
Liquid::Environment.default.error_mode = :warn # Adds strict errors to template.errors but continues as normal Liquid::Template.error_mode = :lax # The default mode, accepts almost anything.
Liquid::Environment.default.error_mode = :lax # The default mode, accepts almost anything.
``` ```
If you want to set the error mode only on specific templates you can pass `:error_mode` as an option to `parse`: If you want to set the error mode only on specific templates you can pass `:error_mode` as an option to `parse`:
@@ -153,4 +111,4 @@ template.render!({ 'x' => 1}, { strict_variables: true })
To help track usages of a feature or code path in production, we have released opt-in usage tracking. To enable this, we provide an empty `Liquid:: Usage.increment` method which you can customize to your needs. The feature is well suited to https://github.com/Shopify/statsd-instrument. However, the choice of implementation is up to you. To help track usages of a feature or code path in production, we have released opt-in usage tracking. To enable this, we provide an empty `Liquid:: Usage.increment` method which you can customize to your needs. The feature is well suited to https://github.com/Shopify/statsd-instrument. However, the choice of implementation is up to you.
Once you have enabled usage tracking, we recommend reporting any events through Github Issues that your system may be logging. It is highly likely this event has been added to consider deprecating or improving code specific to this event, so please raise any concerns. Once you have enabled usage tracking, we recommend reporting any events through Github Issues that your system may be logging. It is highly likely this event has been added to consider deprecating or improving code specific to this event, so please raise any concerns.
-8
View File
@@ -81,14 +81,6 @@ namespace :benchmark do
task :strict do task :strict do
ruby "./performance/benchmark.rb strict" ruby "./performance/benchmark.rb strict"
end end
desc "Run unit benchmarks"
task :unit do
Dir["./performance/unit/*_benchmark.rb"].each do |file|
puts "🧪 Running #{file}"
ruby file
end
end
end end
namespace :profile do namespace :profile do
+5 -9
View File
@@ -30,18 +30,14 @@ class Servlet < LiquidServlet
private private
def products_list def products_list
[ [{ 'name' => 'Arbor Draft', 'price' => 39900, 'description' => 'the *arbor draft* is a excellent product' },
{ 'name' => 'Arbor Draft', 'price' => 39900, 'description' => 'the *arbor draft* is a excellent product' }, { 'name' => 'Arbor Element', 'price' => 40000, 'description' => 'the *arbor element* rocks for freestyling' },
{ 'name' => 'Arbor Element', 'price' => 40000, 'description' => 'the *arbor element* rocks for freestyling' }, { 'name' => 'Arbor Diamond', 'price' => 59900, 'description' => 'the *arbor diamond* is a made up product because im obsessed with arbor and have no creativity' }]
{ 'name' => 'Arbor Diamond', 'price' => 59900, 'description' => 'the *arbor diamond* is a made up product because im obsessed with arbor and have no creativity' }
]
end end
def more_products_list def more_products_list
[ [{ 'name' => 'Arbor Catalyst', 'price' => 39900, 'description' => 'the *arbor catalyst* is an advanced drop-through for freestyle and flatground performance and versatility' },
{ 'name' => 'Arbor Catalyst', 'price' => 39900, 'description' => 'the *arbor catalyst* is an advanced drop-through for freestyle and flatground performance and versatility' }, { 'name' => 'Arbor Fish', 'price' => 40000, 'description' => 'the *arbor fish* is a compact pin that features an extended wheelbase and time-honored teardrop shape' }]
{ 'name' => 'Arbor Fish', 'price' => 40000, 'description' => 'the *arbor fish* is a compact pin that features an extended wheelbase and time-honored teardrop shape' }
]
end end
def description def description
+11 -12
View File
@@ -41,7 +41,7 @@ module Liquid
AnyStartingTag = /#{TagStart}|#{VariableStart}/o AnyStartingTag = /#{TagStart}|#{VariableStart}/o
PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om
VariableParser = /\[(?>[^\[\]]+|\g<0>)*\]|#{VariableSegment}+\??/o VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o
RAISE_EXCEPTION_LAMBDA = ->(_e) { raise } RAISE_EXCEPTION_LAMBDA = ->(_e) { raise }
@@ -50,18 +50,7 @@ module Liquid
end end
require "liquid/version" require "liquid/version"
require "liquid/deprecations"
require "liquid/const"
require "liquid/template/tag_registry"
require 'liquid/standardfilters'
require 'liquid/file_system'
require 'liquid/parser_switching'
require 'liquid/tag'
require 'liquid/block'
require 'liquid/parse_tree_visitor' require 'liquid/parse_tree_visitor'
require 'liquid/interrupts'
require 'liquid/tags'
require "liquid/environment"
require 'liquid/lexer' require 'liquid/lexer'
require 'liquid/parser' require 'liquid/parser'
require 'liquid/i18n' require 'liquid/i18n'
@@ -75,14 +64,20 @@ require 'liquid/strainer_template'
require 'liquid/strainer_factory' require 'liquid/strainer_factory'
require 'liquid/expression' require 'liquid/expression'
require 'liquid/context' require 'liquid/context'
require 'liquid/parser_switching'
require 'liquid/tag' require 'liquid/tag'
require 'liquid/tag/disabler'
require 'liquid/tag/disableable'
require 'liquid/block'
require 'liquid/block_body' require 'liquid/block_body'
require 'liquid/document' require 'liquid/document'
require 'liquid/variable' require 'liquid/variable'
require 'liquid/variable_lookup' require 'liquid/variable_lookup'
require 'liquid/range_lookup' require 'liquid/range_lookup'
require 'liquid/file_system'
require 'liquid/resource_limits' require 'liquid/resource_limits'
require 'liquid/template' require 'liquid/template'
require 'liquid/standardfilters'
require 'liquid/condition' require 'liquid/condition'
require 'liquid/utils' require 'liquid/utils'
require 'liquid/tokenizer' require 'liquid/tokenizer'
@@ -91,3 +86,7 @@ require 'liquid/partial_cache'
require 'liquid/usage' require 'liquid/usage'
require 'liquid/registers' require 'liquid/registers'
require 'liquid/template_factory' require 'liquid/template_factory'
# Load all the tags of the standard library
#
Dir["#{__dir__}/liquid/tags/*.rb"].each { |f| require f }
+4 -8
View File
@@ -36,17 +36,13 @@ module Liquid
# @api private # @api private
def self.raise_unknown_tag(tag, block_name, block_delimiter, parse_context) def self.raise_unknown_tag(tag, block_name, block_delimiter, parse_context)
if tag == 'else' if tag == 'else'
raise SyntaxError, parse_context.locale.t( raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_else",
"errors.syntax.unexpected_else", block_name: block_name)
block_name: block_name,
)
elsif tag.start_with?('end') elsif tag.start_with?('end')
raise SyntaxError, parse_context.locale.t( raise SyntaxError, parse_context.locale.t("errors.syntax.invalid_delimiter",
"errors.syntax.invalid_delimiter",
tag: tag, tag: tag,
block_name: block_name, block_name: block_name,
block_delimiter: block_delimiter, block_delimiter: block_delimiter)
)
else else
raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag", tag: tag) raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag", tag: tag)
end end
+10 -28
View File
@@ -6,7 +6,6 @@ module Liquid
class BlockBody class BlockBody
LiquidTagToken = /\A\s*(#{TagName})\s*(.*?)\z/o LiquidTagToken = /\A\s*(#{TagName})\s*(.*?)\z/o
FullToken = /\A#{TagStart}#{WhitespaceControl}?(\s*)(#{TagName})(\s*)(.*?)#{WhitespaceControl}?#{TagEnd}\z/om FullToken = /\A#{TagStart}#{WhitespaceControl}?(\s*)(#{TagName})(\s*)(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
FullTokenPossiblyInvalid = /\A(.*)#{TagStart}#{WhitespaceControl}?\s*(\w+)\s*(.*)?#{WhitespaceControl}?#{TagEnd}\z/om
ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
WhitespaceOrNothing = /\A\s*\z/ WhitespaceOrNothing = /\A\s*\z/
TAGSTART = "{%" TAGSTART = "{%"
@@ -46,13 +45,7 @@ module Liquid
end end
tag_name = Regexp.last_match(1) tag_name = Regexp.last_match(1)
markup = Regexp.last_match(2) markup = Regexp.last_match(2)
unless (tag = registered_tags[tag_name])
if tag_name == 'liquid'
parse_context.line_number -= 1
next parse_liquid_tag(markup, parse_context)
end
unless (tag = parse_context.environment.tag_for_name(tag_name))
# end parsing if we reach an unknown tag and let the caller decide # end parsing if we reach an unknown tag and let the caller decide
# determine how to proceed # determine how to proceed
return yield tag_name, markup return yield tag_name, markup
@@ -116,22 +109,14 @@ module Liquid
end end
end end
private def handle_invalid_tag_token(token, parse_context) private def parse_for_document(tokenizer, parse_context)
if token.end_with?('%}')
yield token, token
else
BlockBody.raise_missing_tag_terminator(token, parse_context)
end
end
private def parse_for_document(tokenizer, parse_context, &block)
while (token = tokenizer.shift) while (token = tokenizer.shift)
next if token.empty? next if token.empty?
case case
when token.start_with?(TAGSTART) when token.start_with?(TAGSTART)
whitespace_handler(token, parse_context) whitespace_handler(token, parse_context)
unless token =~ FullToken unless token =~ FullToken
return handle_invalid_tag_token(token, parse_context, &block) BlockBody.raise_missing_tag_terminator(token, parse_context)
end end
tag_name = Regexp.last_match(2) tag_name = Regexp.last_match(2)
markup = Regexp.last_match(4) markup = Regexp.last_match(4)
@@ -147,7 +132,7 @@ module Liquid
next next
end end
unless (tag = parse_context.environment.tag_for_name(tag_name)) unless (tag = registered_tags[tag_name])
# end parsing if we reach an unknown tag and let the caller decide # end parsing if we reach an unknown tag and let the caller decide
# determine how to proceed # determine how to proceed
return yield tag_name, markup return yield tag_name, markup
@@ -246,17 +231,10 @@ module Liquid
end end
def create_variable(token, parse_context) def create_variable(token, parse_context)
if token.end_with?("}}") if token =~ ContentOfVariable
i = 2 markup = Regexp.last_match(1)
i = 3 if token[i] == "-"
parse_end = token.length - 3
parse_end -= 1 if token[parse_end] == "-"
markup_end = parse_end - i + 1
markup = markup_end <= 0 ? "" : token.slice(i, markup_end)
return Variable.new(markup, parse_context) return Variable.new(markup, parse_context)
end end
BlockBody.raise_missing_variable_terminator(token, parse_context) BlockBody.raise_missing_variable_terminator(token, parse_context)
end end
@@ -269,5 +247,9 @@ module Liquid
def raise_missing_variable_terminator(token, parse_context) def raise_missing_variable_terminator(token, parse_context)
BlockBody.raise_missing_variable_terminator(token, parse_context) BlockBody.raise_missing_variable_terminator(token, parse_context)
end end
def registered_tags
Template.tags
end
end end
end end
+4 -9
View File
@@ -24,9 +24,6 @@ module Liquid
else else
false false
end end
rescue Encoding::CompatibilityError
# "✅".b.include?("✅") raises Encoding::CompatibilityError despite being materially equal
left.b.include?(right.b)
end, end,
} }
@@ -72,9 +69,9 @@ module Liquid
case condition.child_relation case condition.child_relation
when :or when :or
break if Liquid::Utils.to_liquid_value(result) break if result
when :and when :and
break unless Liquid::Utils.to_liquid_value(result) break unless result
else else
break break
end end
@@ -162,10 +159,8 @@ module Liquid
class ParseTreeVisitor < Liquid::ParseTreeVisitor class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children def children
[ [
@node.left, @node.left, @node.right,
@node.right, @node.child_condition, @node.attachment
@node.child_condition,
@node.attachment
].compact ].compact
end end
end end
-8
View File
@@ -1,8 +0,0 @@
# frozen_string_literal: true
module Liquid
module Const
EMPTY_HASH = {}.freeze
EMPTY_ARRAY = [].freeze
end
end
+11 -16
View File
@@ -15,25 +15,24 @@ module Liquid
# context['bob'] #=> nil class Context # context['bob'] #=> nil class Context
class Context class Context
attr_reader :scopes, :errors, :registers, :environments, :resource_limits, :static_registers, :static_environments attr_reader :scopes, :errors, :registers, :environments, :resource_limits, :static_registers, :static_environments
attr_accessor :exception_renderer, :template_name, :partial, :global_filter, :strict_variables, :strict_filters, :environment attr_accessor :exception_renderer, :template_name, :partial, :global_filter, :strict_variables, :strict_filters
# rubocop:disable Metrics/ParameterLists # rubocop:disable Metrics/ParameterLists
def self.build(environment: Environment.default, environments: {}, outer_scope: {}, registers: {}, rethrow_errors: false, resource_limits: nil, static_environments: {}, &block) def self.build(environments: {}, outer_scope: {}, registers: {}, rethrow_errors: false, resource_limits: nil, static_environments: {}, &block)
new(environments, outer_scope, registers, rethrow_errors, resource_limits, static_environments, &block) new(environments, outer_scope, registers, rethrow_errors, resource_limits, static_environments, &block)
end end
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil, static_environments = {}, environment = Environment.default) def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil, static_environments = {})
@environment = environment
@environments = [environments] @environments = [environments]
@environments.flatten! @environments.flatten!
@static_environments = [static_environments].flatten(1).freeze @static_environments = [static_environments].flatten(1).freeze
@scopes = [outer_scope || {}] @scopes = [(outer_scope || {})]
@registers = registers.is_a?(Registers) ? registers : Registers.new(registers) @registers = registers.is_a?(Registers) ? registers : Registers.new(registers)
@errors = [] @errors = []
@partial = false @partial = false
@strict_variables = false @strict_variables = false
@resource_limits = resource_limits || ResourceLimits.new(environment.default_resource_limits) @resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
@base_scope_depth = 0 @base_scope_depth = 0
@interrupts = [] @interrupts = []
@filters = [] @filters = []
@@ -41,10 +40,10 @@ module Liquid
@disabled_tags = {} @disabled_tags = {}
@registers.static[:cached_partials] ||= {} @registers.static[:cached_partials] ||= {}
@registers.static[:file_system] ||= environment.file_system @registers.static[:file_system] ||= Liquid::Template.file_system
@registers.static[:template_factory] ||= Liquid::TemplateFactory.new @registers.static[:template_factory] ||= Liquid::TemplateFactory.new
self.exception_renderer = environment.exception_renderer self.exception_renderer = Template.default_exception_renderer
if rethrow_errors if rethrow_errors
self.exception_renderer = Liquid::RAISE_EXCEPTION_LAMBDA self.exception_renderer = Liquid::RAISE_EXCEPTION_LAMBDA
end end
@@ -61,7 +60,7 @@ module Liquid
end end
def strainer def strainer
@strainer ||= @environment.create_strainer(self, @filters) @strainer ||= StrainerFactory.create(self, @filters)
end end
# Adds filters to this context. # Adds filters to this context.
@@ -145,7 +144,7 @@ module Liquid
self.class.build( self.class.build(
resource_limits: resource_limits, resource_limits: resource_limits,
static_environments: static_environments, static_environments: static_environments,
registers: Registers.new(registers), registers: Registers.new(registers)
).tap do |subcontext| ).tap do |subcontext|
subcontext.base_scope_depth = base_scope_depth + 1 subcontext.base_scope_depth = base_scope_depth + 1
subcontext.exception_renderer = exception_renderer subcontext.exception_renderer = exception_renderer
@@ -198,14 +197,10 @@ module Liquid
try_variable_find_in_environments(key, raise_on_not_found: raise_on_not_found) try_variable_find_in_environments(key, raise_on_not_found: raise_on_not_found)
end end
# update variable's context before invoking #to_liquid variable = variable.to_liquid
variable.context = self if variable.respond_to?(:context=) variable.context = self if variable.respond_to?(:context=)
liquid_variable = variable.to_liquid variable
liquid_variable.context = self if variable != liquid_variable && liquid_variable.respond_to?(:context=)
liquid_variable
end end
def lookup_and_evaluate(obj, key, raise_on_not_found: true) def lookup_and_evaluate(obj, key, raise_on_not_found: true)
-22
View File
@@ -1,22 +0,0 @@
# frozen_string_literal: true
require "set"
module Liquid
class Deprecations
class << self
attr_accessor :warned
Deprecations.warned = Set.new
def warn(name, alternative)
return if warned.include?(name)
warned << name
caller_location = caller_locations(2, 1).first
Warning.warn("[DEPRECATION] #{name} is deprecated. Use #{alternative} instead. Called from #{caller_location}\n")
end
end
end
end
-4
View File
@@ -25,10 +25,6 @@ module Liquid
class Drop class Drop
attr_writer :context attr_writer :context
def initialize
@context = nil
end
# Catch all for the method # Catch all for the method
def liquid_method_missing(method) def liquid_method_missing(method)
return nil unless @context&.strict_variables return nil unless @context&.strict_variables
-159
View File
@@ -1,159 +0,0 @@
# frozen_string_literal: true
module Liquid
# The Environment is the container for all configuration options of Liquid, such as
# the registered tags, filters, and the default error mode.
class Environment
# The default error mode for all templates. This can be overridden on a
# per-template basis.
attr_accessor :error_mode
# The tags that are available to use in the template.
attr_accessor :tags
# The strainer template which is used to store filters that are available to
# use in templates.
attr_accessor :strainer_template
# The exception renderer that is used to render exceptions that are raised
# when rendering a template
attr_accessor :exception_renderer
# The default file system that is used to load templates from.
attr_accessor :file_system
# The default resource limits that are used to limit the resources that a
# template can consume.
attr_accessor :default_resource_limits
class << self
# Creates a new environment instance.
#
# @param tags [Hash] The tags that are available to use in
# the template.
# @param file_system The default file system that is used
# to load templates from.
# @param error_mode [Symbol] The default error mode for all templates
# (either :strict, :warn, or :lax).
# @param exception_renderer [Proc] The exception renderer that is used to
# render exceptions.
# @yieldparam environment [Environment] The environment instance that is being built.
# @return [Environment] The new environment instance.
def build(tags: nil, file_system: nil, error_mode: nil, exception_renderer: nil)
ret = new
ret.tags = Template::TagRegistry.new(tags) if tags
ret.file_system = file_system if file_system
ret.error_mode = error_mode if error_mode
ret.exception_renderer = exception_renderer if exception_renderer
yield ret if block_given?
ret.freeze
end
# Returns the default environment instance.
#
# @return [Environment] The default environment instance.
def default
@default ||= new
end
# Sets the default environment instance for the duration of the block
#
# @param environment [Environment] The environment instance to use as the default for the
# duration of the block.
# @yield
# @return [Object] The return value of the block.
def dangerously_override(environment)
original_default = @default
@default = environment
yield
ensure
@default = original_default
end
end
# Initializes a new environment instance.
# @api private
def initialize
@tags = Template::TagRegistry.new(Tags::STANDARD_TAGS)
@error_mode = :lax
@strainer_template = Class.new(StrainerTemplate).tap do |klass|
klass.add_filter(StandardFilters)
end
@exception_renderer = ->(exception) { exception }
@file_system = BlankFileSystem.new
@default_resource_limits = Const::EMPTY_HASH
@strainer_template_class_cache = {}
end
# Registers a new tag with the environment.
#
# @param name [String] The name of the tag.
# @param klass [Liquid::Tag] The class that implements the tag.
# @return [void]
def register_tag(name, klass)
@tags[name] = klass
end
# Registers a new filter with the environment.
#
# @param filter [Module] The module that contains the filter methods.
# @return [void]
def register_filter(filter)
@strainer_template_class_cache.clear
@strainer_template.add_filter(filter)
end
# Registers multiple filters with this environment.
#
# @param filters [Array<Module>] The modules that contain the filter methods.
# @return [self]
def register_filters(filters)
@strainer_template_class_cache.clear
filters.each { |f| @strainer_template.add_filter(f) }
self
end
# Creates a new strainer instance with the given filters, caching the result
# for faster lookup.
#
# @param context [Liquid::Context] The context that the strainer will be
# used in.
# @param filters [Array<Module>] The filters that the strainer will have
# access to.
# @return [Liquid::Strainer] The new strainer instance.
def create_strainer(context, filters = Const::EMPTY_ARRAY)
return @strainer_template.new(context) if filters.empty?
strainer_template = @strainer_template_class_cache[filters] ||= begin
klass = Class.new(@strainer_template)
filters.each { |f| klass.add_filter(f) }
klass
end
strainer_template.new(context)
end
# Returns the names of all the filter methods that are available to use in
# the strainer template.
#
# @return [Array<String>] The names of all the filter methods.
def filter_method_names
@strainer_template.filter_method_names
end
# Returns the tag class for the given tag name.
#
# @param name [String] The name of the tag.
# @return [Liquid::Tag] The tag class.
def tag_for_name(name)
@tags[name]
end
def freeze
@tags.freeze
# TODO: freeze the tags, currently this is not possible because of liquid-c
# @strainer_template.freeze
super
end
end
end
+15 -16
View File
@@ -40,20 +40,19 @@ module Liquid
end end
end end
ArgumentError = Class.new(Error) ArgumentError = Class.new(Error)
ContextError = Class.new(Error) ContextError = Class.new(Error)
FileSystemError = Class.new(Error) FileSystemError = Class.new(Error)
StandardError = Class.new(Error) StandardError = Class.new(Error)
SyntaxError = Class.new(Error) SyntaxError = Class.new(Error)
StackLevelError = Class.new(Error) StackLevelError = Class.new(Error)
MemoryError = Class.new(Error) MemoryError = Class.new(Error)
ZeroDivisionError = Class.new(Error) ZeroDivisionError = Class.new(Error)
FloatDomainError = Class.new(Error) FloatDomainError = Class.new(Error)
UndefinedVariable = Class.new(Error) UndefinedVariable = Class.new(Error)
UndefinedDropMethod = Class.new(Error) UndefinedDropMethod = Class.new(Error)
UndefinedFilter = Class.new(Error) UndefinedFilter = Class.new(Error)
MethodOverrideError = Class.new(Error) MethodOverrideError = Class.new(Error)
DisabledError = Class.new(Error) DisabledError = Class.new(Error)
InternalError = Class.new(Error) InternalError = Class.new(Error)
TemplateEncodingError = Class.new(Error)
end end
+1 -4
View File
@@ -3,10 +3,7 @@
module Liquid module Liquid
class Expression class Expression
LITERALS = { LITERALS = {
nil => nil, nil => nil, 'nil' => nil, 'null' => nil, '' => nil,
'nil' => nil,
'null' => nil,
'' => nil,
'true' => true, 'true' => true,
'false' => false, 'false' => false,
'blank' => '', 'blank' => '',
+5 -2
View File
@@ -5,7 +5,7 @@ module Liquid
# @liquid_type object # @liquid_type object
# @liquid_name forloop # @liquid_name forloop
# @liquid_summary # @liquid_summary
# Information about a parent [`for` loop](/docs/api/liquid/tags/for). # Information about a parent [`for` loop](/api/liquid/tags#for).
class ForloopDrop < Drop class ForloopDrop < Drop
def initialize(name, length, parentloop) def initialize(name, length, parentloop)
@name = name @name = name
@@ -30,7 +30,10 @@ module Liquid
# @liquid_return [forloop] # @liquid_return [forloop]
attr_reader :parentloop attr_reader :parentloop
attr_reader :name def name
Usage.increment('forloop_drop_name')
@name
end
# @liquid_public_docs # @liquid_public_docs
# @liquid_summary # @liquid_summary
+1 -155
View File
@@ -1,9 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
require "strscan" require "strscan"
module Liquid module Liquid
class Lexer1 class Lexer
SPECIALS = { SPECIALS = {
'|' => :pipe, '|' => :pipe,
'.' => :dot, '.' => :dot,
@@ -59,157 +58,4 @@ module Liquid
@output << [:end_of_string] @output << [:end_of_string]
end end
end end
class Lexer2
CLOSE_ROUND = [:close_round, ")"].freeze
CLOSE_SQUARE = [:close_square, "]"].freeze
COLON = [:colon, ":"].freeze
COMMA = [:comma, ","].freeze
COMPARISION_NOT_EQUAL = [:comparison, "!="].freeze
COMPARISON_CONTAINS = [:comparison, "contains"].freeze
COMPARISON_EQUAL = [:comparison, "=="].freeze
COMPARISON_GREATER_THAN = [:comparison, ">"].freeze
COMPARISON_GREATER_THAN_OR_EQUAL = [:comparison, ">="].freeze
COMPARISON_LESS_THAN = [:comparison, "<"].freeze
COMPARISON_LESS_THAN_OR_EQUAL = [:comparison, "<="].freeze
COMPARISON_NOT_EQUAL_ALT = [:comparison, "<>"].freeze
CONTAINS = /contains(?=\s)/
DASH = [:dash, "-"].freeze
DOT = [:dot, "."].freeze
DOTDOT = [:dotdot, ".."].freeze
DOT_ORD = ".".ord
DOUBLE_STRING_LITERAL = /"[^\"]*"/
EOS = [:end_of_string].freeze
IDENTIFIER = /[a-zA-Z_][\w-]*\??/
NUMBER_LITERAL = /-?\d+(\.\d+)?/
OPEN_ROUND = [:open_round, "("].freeze
OPEN_SQUARE = [:open_square, "["].freeze
PIPE = [:pipe, "|"].freeze
QUESTION = [:question, "?"].freeze
RUBY_WHITESPACE = [" ", "\t", "\r", "\n", "\f"].freeze
SINGLE_STRING_LITERAL = /'[^\']*'/
WHITESPACE_OR_NOTHING = /\s*/
COMPARISON_JUMP_TABLE = [].tap do |table|
table["=".ord] = [].tap do |sub_table|
sub_table["=".ord] = COMPARISON_EQUAL
sub_table.freeze
end
table["!".ord] = [].tap do |sub_table|
sub_table["=".ord] = COMPARISION_NOT_EQUAL
sub_table.freeze
end
table["<".ord] = [].tap do |sub_table|
sub_table["=".ord] = COMPARISON_LESS_THAN_OR_EQUAL
sub_table[">".ord] = COMPARISON_NOT_EQUAL_ALT
RUBY_WHITESPACE.each { |c| sub_table[c.ord] = COMPARISON_LESS_THAN }
sub_table.freeze
end
table[">".ord] = [].tap do |sub_table|
sub_table["=".ord] = COMPARISON_GREATER_THAN_OR_EQUAL
RUBY_WHITESPACE.each { |c| sub_table[c.ord] = COMPARISON_GREATER_THAN }
sub_table.freeze
end
table.freeze
end
NEXT_MATCHER_JUMP_TABLE = [].tap do |table|
"a".upto("z") do |c|
table[c.ord] = [:id, IDENTIFIER].freeze
table[c.upcase.ord] = [:id, IDENTIFIER].freeze
end
table["_".ord] = [:id, IDENTIFIER].freeze
"0".upto("9") do |c|
table[c.ord] = [:number, NUMBER_LITERAL].freeze
end
table["-".ord] = [:number, NUMBER_LITERAL].freeze
table["'".ord] = [:string, SINGLE_STRING_LITERAL].freeze
table["\"".ord] = [:string, DOUBLE_STRING_LITERAL].freeze
table.freeze
end
SPECIAL_TABLE = [].tap do |table|
table["|".ord] = PIPE
table[".".ord] = DOT
table[":".ord] = COLON
table[",".ord] = COMMA
table["[".ord] = OPEN_SQUARE
table["]".ord] = CLOSE_SQUARE
table["(".ord] = OPEN_ROUND
table[")".ord] = CLOSE_ROUND
table["?".ord] = QUESTION
table["-".ord] = DASH
end
NUMBER_TABLE = [].tap do |table|
"0".upto("9") do |c|
table[c.ord] = true
end
table.freeze
end
def initialize(input)
@ss = StringScanner.new(input)
end
# rubocop:disable Metrics/BlockNesting
def tokenize
@output = []
until @ss.eos?
@ss.skip(WHITESPACE_OR_NOTHING)
break if @ss.eos?
peeked = @ss.peek_byte
if (special = SPECIAL_TABLE[peeked])
@ss.scan_byte
# Special case for ".."
if special == DOT && @ss.peek_byte == DOT_ORD
@ss.scan_byte
@output << DOTDOT
elsif special == DASH
# Special case for negative numbers
if NUMBER_TABLE[@ss.peek_byte]
@ss.pos -= 1
@output << [:number, @ss.scan(NUMBER_LITERAL)]
else
@output << special
end
else
@output << special
end
elsif (sub_table = COMPARISON_JUMP_TABLE[peeked])
@ss.scan_byte
if (found = sub_table[@ss.peek_byte])
@output << found
@ss.scan_byte
else
raise SyntaxError, "Unexpected character #{peeked.chr}"
end
else
type, pattern = NEXT_MATCHER_JUMP_TABLE[peeked]
if type && (t = @ss.scan(pattern))
# Special case for "contains"
@output << if type == :id && t == "contains"
COMPARISON_CONTAINS
else
[type, t]
end
else
raise SyntaxError, "Unexpected character #{peeked.chr}"
end
end
end
# rubocop:enable Metrics/BlockNesting
@output << EOS
end
end
Lexer = StringScanner.instance_methods.include?(:scan_byte) ? Lexer2 : Lexer1
end end
-1
View File
@@ -15,7 +15,6 @@
include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]" include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]"
inline_comment_invalid: "Syntax error in tag '#' - Each line of comments must be prefixed by the '#' character" inline_comment_invalid: "Syntax error in tag '#' - Each line of comments must be prefixed by the '#' character"
invalid_delimiter: "'%{tag}' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}" invalid_delimiter: "'%{tag}' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}"
invalid_template_encoding: "Invalid template encoding"
render: "Syntax error in tag 'render' - Template name must be a quoted string" render: "Syntax error in tag 'render' - Template name must be a quoted string"
table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3" table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
tag_never_closed: "'%{block_name}' tag was never closed" tag_never_closed: "'%{block_name}' tag was never closed"
+3 -4
View File
@@ -3,10 +3,9 @@
module Liquid module Liquid
class ParseContext class ParseContext
attr_accessor :locale, :line_number, :trim_whitespace, :depth attr_accessor :locale, :line_number, :trim_whitespace, :depth
attr_reader :partial, :warnings, :error_mode, :environment attr_reader :partial, :warnings, :error_mode
def initialize(options = Const::EMPTY_HASH) def initialize(options = {})
@environment = options.fetch(:environment, Environment.default)
@template_options = options ? options.dup : {} @template_options = options ? options.dup : {}
@locale = @template_options[:locale] ||= I18n.new @locale = @template_options[:locale] ||= I18n.new
@@ -36,7 +35,7 @@ module Liquid
@partial = value @partial = value
@options = value ? partial_options : @template_options @options = value ? partial_options : @template_options
@error_mode = @options[:error_mode] || @environment.error_mode @error_mode = @options[:error_mode] || Template.error_mode
end end
def partial_options def partial_options
+1 -1
View File
@@ -53,7 +53,7 @@ module Liquid
str = consume str = consume
str << variable_lookups str << variable_lookups
when :open_square when :open_square
str = consume.dup str = consume
str << expression str << expression
str << consume(:close_square) str << consume(:close_square)
str << variable_lookups str << variable_lookups
+3 -12
View File
@@ -4,8 +4,7 @@ module Liquid
class PartialCache class PartialCache
def self.load(template_name, context:, parse_context:) def self.load(template_name, context:, parse_context:)
cached_partials = context.registers[:cached_partials] cached_partials = context.registers[:cached_partials]
cache_key = "#{template_name}:#{parse_context.error_mode}" cached = cached_partials[template_name]
cached = cached_partials[cache_key]
return cached if cached return cached if cached
file_system = context.registers[:file_system] file_system = context.registers[:file_system]
@@ -16,16 +15,8 @@ module Liquid
template_factory = context.registers[:template_factory] template_factory = context.registers[:template_factory]
template = template_factory.for(template_name) template = template_factory.for(template_name)
begin partial = template.parse(source, parse_context)
partial = template.parse(source, parse_context) cached_partials[template_name] = partial
rescue Liquid::Error => e
e.template_name = template&.name || template_name
raise e
end
partial.name ||= template_name
cached_partials[cache_key] = partial
ensure ensure
parse_context.partial = false parse_context.partial = false
end end
+21 -82
View File
@@ -6,14 +6,7 @@ require 'bigdecimal'
module Liquid module Liquid
module StandardFilters module StandardFilters
MAX_I32 = (1 << 31) - 1 MAX_INT = (1 << 31) - 1
private_constant :MAX_I32
MIN_I64 = -(1 << 63)
MAX_I64 = (1 << 63) - 1
I64_RANGE = MIN_I64..MAX_I64
private_constant :MIN_I64, :MAX_I64, :I64_RANGE
HTML_ESCAPE = { HTML_ESCAPE = {
'&' => '&amp;', '&' => '&amp;',
'>' => '&gt;', '>' => '&gt;',
@@ -25,23 +18,10 @@ module Liquid
STRIP_HTML_BLOCKS = Regexp.union( STRIP_HTML_BLOCKS = Regexp.union(
%r{<script.*?</script>}m, %r{<script.*?</script>}m,
/<!--.*?-->/m, /<!--.*?-->/m,
%r{<style.*?</style>}m, %r{<style.*?</style>}m
) )
STRIP_HTML_TAGS = /<.*?>/m STRIP_HTML_TAGS = /<.*?>/m
class << self
def try_coerce_encoding(input, encoding:)
original_encoding = input.encoding
if input.encoding != encoding
input.force_encoding(encoding)
unless input.valid_encoding?
input.force_encoding(original_encoding)
end
end
input
end
end
# @liquid_public_docs # @liquid_public_docs
# @liquid_type filter # @liquid_type filter
# @liquid_category array # @liquid_category array
@@ -82,7 +62,7 @@ module Liquid
# @liquid_type filter # @liquid_type filter
# @liquid_category string # @liquid_category string
# @liquid_summary # @liquid_summary
# Capitalizes the first word in a string and downcases the remaining characters. # Capitalizes the first word in a string.
# @liquid_syntax string | capitalize # @liquid_syntax string | capitalize
# @liquid_return [string] # @liquid_return [string]
def capitalize(input) def capitalize(input)
@@ -93,7 +73,7 @@ module Liquid
# @liquid_type filter # @liquid_type filter
# @liquid_category string # @liquid_category string
# @liquid_summary # @liquid_summary
# Escapes special characters in HTML, such as `<>`, `'`, and `&`, and converts characters into escape sequences. The filter doesn't effect characters within the string that don’t have a corresponding escape sequence.". # Escapes a string.
# @liquid_syntax string | escape # @liquid_syntax string | escape
# @liquid_return [string] # @liquid_return [string]
def escape(input) def escape(input)
@@ -163,8 +143,7 @@ module Liquid
# @liquid_syntax string | base64_decode # @liquid_syntax string | base64_decode
# @liquid_return [string] # @liquid_return [string]
def base64_decode(input) def base64_decode(input)
input = input.to_s Base64.strict_decode64(input.to_s)
StandardFilters.try_coerce_encoding(Base64.strict_decode64(input), encoding: input.encoding)
rescue ::ArgumentError rescue ::ArgumentError
raise Liquid::ArgumentError, "invalid base64 provided to base64_decode" raise Liquid::ArgumentError, "invalid base64 provided to base64_decode"
end end
@@ -188,8 +167,7 @@ module Liquid
# @liquid_syntax string | base64_url_safe_decode # @liquid_syntax string | base64_url_safe_decode
# @liquid_return [string] # @liquid_return [string]
def base64_url_safe_decode(input) def base64_url_safe_decode(input)
input = input.to_s Base64.urlsafe_decode64(input.to_s)
StandardFilters.try_coerce_encoding(Base64.urlsafe_decode64(input), encoding: input.encoding)
rescue ::ArgumentError rescue ::ArgumentError
raise Liquid::ArgumentError, "invalid base64 provided to base64_url_safe_decode" raise Liquid::ArgumentError, "invalid base64 provided to base64_url_safe_decode"
end end
@@ -208,19 +186,10 @@ module Liquid
offset = Utils.to_integer(offset) offset = Utils.to_integer(offset)
length = length ? Utils.to_integer(length) : 1 length = length ? Utils.to_integer(length) : 1
begin if input.is_a?(Array)
if input.is_a?(Array) input.slice(offset, length) || []
input.slice(offset, length) || [] else
else input.to_s.slice(offset, length) || ''
input.to_s.slice(offset, length) || ''
end
rescue RangeError
if I64_RANGE.cover?(length) && I64_RANGE.cover?(offset)
raise # unexpected error
end
offset = offset.clamp(I64_RANGE)
length = length.clamp(I64_RANGE)
retry
end end
end end
@@ -270,9 +239,9 @@ module Liquid
wordlist = begin wordlist = begin
input.split(" ", words + 1) input.split(" ", words + 1)
rescue RangeError rescue RangeError
# integer too big for String#split, but we can semantically assume no truncation is needed raise if words + 1 < MAX_INT
return input if words + 1 > MAX_I32 # e.g. integer #{words} too big to convert to `int'
raise # unexpected error raise Liquid::ArgumentError, "integer #{words} too big for truncatewords"
end end
return input if wordlist.length <= words return input if wordlist.length <= words
@@ -630,7 +599,7 @@ module Liquid
# @liquid_description # @liquid_description
# > Note: # > Note:
# > The `concat` filter won't filter out duplicates. If you want to remove duplicates, then you need to use the # > The `concat` filter won't filter out duplicates. If you want to remove duplicates, then you need to use the
# > [`uniq` filter](/docs/api/liquid/filters/uniq). # > [`uniq` filter](/api/liquid/filters#uniq).
# @liquid_syntax array | concat: array # @liquid_syntax array | concat: array
# @liquid_return [array[untyped]] # @liquid_return [array[untyped]]
def concat(input, array) def concat(input, array)
@@ -772,7 +741,7 @@ module Liquid
# @liquid_type filter # @liquid_type filter
# @liquid_category math # @liquid_category math
# @liquid_summary # @liquid_summary
# Divides a number by a given number. The `divided_by` filter produces a result of the same type as the divisor. This means if you divide by an integer, the result will be an integer, and if you divide by a float, the result will be a float. # Divides a number by a given number.
# @liquid_syntax number | divided_by: number # @liquid_syntax number | divided_by: number
# @liquid_return [number] # @liquid_return [number]
def divided_by(input, operand) def divided_by(input, operand)
@@ -872,48 +841,18 @@ module Liquid
# @liquid_summary # @liquid_summary
# Sets a default value for any variable whose value is one of the following: # Sets a default value for any variable whose value is one of the following:
# #
# - [`empty`](/docs/api/liquid/basics#empty) # - [`empty`](/api/liquid/basics#empty)
# - [`false`](/docs/api/liquid/basics#truthy-and-falsy) # - [`false`](/api/liquid/basics#truthy-and-falsy)
# - [`nil`](/docs/api/liquid/basics#nil) # - [`nil`](/api/liquid/basics#nil)
# @liquid_syntax variable | default: variable # @liquid_syntax variable | default: variable
# @liquid_return [untyped] # @liquid_return [untyped]
# @liquid_optional_param allow_false: [boolean] Whether to use false values instead of the default. # @liquid_optional_param allow_false [boolean] Whether to use false values instead of the default.
def default(input, default_value = '', options = {}) def default(input, default_value = '', options = {})
options = {} unless options.is_a?(Hash) options = {} unless options.is_a?(Hash)
false_check = options['allow_false'] ? input.nil? : !Liquid::Utils.to_liquid_value(input) false_check = options['allow_false'] ? input.nil? : !Liquid::Utils.to_liquid_value(input)
false_check || (input.respond_to?(:empty?) && input.empty?) ? default_value : input false_check || (input.respond_to?(:empty?) && input.empty?) ? default_value : input
end end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Returns the sum of all elements in an array.
# @liquid_syntax array | sum
# @liquid_return [number]
def sum(input, property = nil)
ary = InputIterator.new(input, context)
return 0 if ary.empty?
values_for_sum = ary.map do |item|
if property.nil?
item
elsif item.respond_to?(:[])
item[property]
else
0
end
rescue TypeError
raise_property_error(property)
end
result = InputIterator.new(values_for_sum, context).sum do |item|
Utils.to_number(item)
end
result.is_a?(BigDecimal) ? result.to_f : result
end
private private
attr_reader :context attr_reader :context
@@ -944,8 +883,6 @@ module Liquid
def nil_safe_casecmp(a, b) def nil_safe_casecmp(a, b)
if !a.nil? && !b.nil? if !a.nil? && !b.nil?
a.to_s.casecmp(b.to_s) a.to_s.casecmp(b.to_s)
elsif a.nil? && b.nil?
0
else else
a.nil? ? 1 : -1 a.nil? ? 1 : -1
end end
@@ -1001,4 +938,6 @@ module Liquid
end end
end end
end end
Template.register_filter(StandardFilters)
end end
+27 -9
View File
@@ -5,19 +5,37 @@ module Liquid
module StrainerFactory module StrainerFactory
extend self extend self
def add_global_filter(filter, environment = Environment.default) def add_global_filter(filter)
Deprecations.warn("StrainerFactory.add_global_filter", "Environment#register_filter") strainer_class_cache.clear
environment.register_filter(filter) GlobalCache.add_filter(filter)
end end
def create(context, filters = Const::EMPTY_ARRAY, environment = Environment.default) def create(context, filters = [])
Deprecations.warn("StrainerFactory.create", "StrainerFactory.create_strainer") strainer_from_cache(filters).new(context)
environment.create_strainer(context, filters)
end end
def global_filter_names(environment = Environment.default) def global_filter_names
Deprecations.warn("StrainerFactory.global_filter_names", "Environment#filter_method_names") GlobalCache.filter_method_names
Environment.strainer_template.filter_method_names end
GlobalCache = Class.new(StrainerTemplate)
private
def strainer_from_cache(filters)
if filters.empty?
GlobalCache
else
strainer_class_cache[filters] ||= begin
klass = Class.new(GlobalCache)
filters.each { |f| klass.add_filter(f) }
klass
end
end
end
def strainer_class_cache
@strainer_class_cache ||= {}
end end
end end
end end
+1 -1
View File
@@ -5,7 +5,7 @@ module Liquid
# @liquid_type object # @liquid_type object
# @liquid_name tablerowloop # @liquid_name tablerowloop
# @liquid_summary # @liquid_summary
# Information about a parent [`tablerow` loop](/docs/api/liquid/tags/tablerow). # Information about a parent [`tablerow` loop](/api/liquid/tags#tablerow).
class TablerowloopDrop < Drop class TablerowloopDrop < Drop
def initialize(length, cols) def initialize(length, cols)
@length = length @length = length
+3 -13
View File
@@ -1,8 +1,5 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'liquid/tag/disabler'
require 'liquid/tag/disableable'
module Liquid module Liquid
class Tag class Tag
attr_reader :nodelist, :tag_name, :line_number, :parse_context attr_reader :nodelist, :tag_name, :line_number, :parse_context
@@ -17,18 +14,12 @@ module Liquid
end end
def disable_tags(*tag_names) def disable_tags(*tag_names)
tag_names += disabled_tags @disabled_tags ||= []
define_singleton_method(:disabled_tags) { tag_names } @disabled_tags.concat(tag_names)
prepend(Disabler) prepend(Disabler)
end end
private :new private :new
protected
def disabled_tags
[]
end
end end
def initialize(tag_name, markup, parse_context) def initialize(tag_name, markup, parse_context)
@@ -57,8 +48,7 @@ module Liquid
# of the `render_to_output_buffer` method will become the default and the `render` # of the `render_to_output_buffer` method will become the default and the `render`
# method will be removed. # method will be removed.
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
render_result = render(context) output << render(context)
output << render_result if render_result
output output
end end
+8
View File
@@ -3,6 +3,14 @@
module Liquid module Liquid
class Tag class Tag
module Disabler module Disabler
module ClassMethods
attr_reader :disabled_tags
end
def self.prepended(base)
base.extend(ClassMethods)
end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
context.with_disabled_tags(self.class.disabled_tags) do context.with_disabled_tags(self.class.disabled_tags) do
super super
-47
View File
@@ -1,47 +0,0 @@
# frozen_string_literal: true
require_relative "tags/table_row"
require_relative "tags/echo"
require_relative "tags/if"
require_relative "tags/break"
require_relative "tags/inline_comment"
require_relative "tags/for"
require_relative "tags/assign"
require_relative "tags/ifchanged"
require_relative "tags/case"
require_relative "tags/include"
require_relative "tags/continue"
require_relative "tags/capture"
require_relative "tags/decrement"
require_relative "tags/unless"
require_relative "tags/increment"
require_relative "tags/comment"
require_relative "tags/raw"
require_relative "tags/render"
require_relative "tags/cycle"
module Liquid
module Tags
STANDARD_TAGS = {
'cycle' => Cycle,
'render' => Render,
'raw' => Raw,
'comment' => Comment,
'increment' => Increment,
'unless' => Unless,
'decrement' => Decrement,
'capture' => Capture,
'continue' => Continue,
'include' => Include,
'case' => Case,
'ifchanged' => Ifchanged,
'assign' => Assign,
'for' => For,
'#' => InlineComment,
'break' => Break,
'if' => If,
'echo' => Echo,
'tablerow' => TableRow,
}.freeze
end
end
+3 -1
View File
@@ -8,7 +8,7 @@ module Liquid
# @liquid_summary # @liquid_summary
# Creates a new variable. # Creates a new variable.
# @liquid_description # @liquid_description
# You can create variables of any [basic type](/docs/api/liquid/basics#types), [object](/docs/api/liquid/objects), or object property. # You can create variables of any [basic type](/api/liquid/basics#types), [object](/api/liquid/objects), or object property.
# @liquid_syntax # @liquid_syntax
# {% assign variable_name = value %} # {% assign variable_name = value %}
# @liquid_syntax_keyword variable_name The name of the variable being created. # @liquid_syntax_keyword variable_name The name of the variable being created.
@@ -72,4 +72,6 @@ module Liquid
end end
end end
end end
Template.register_tag('assign', Assign)
end end
+3 -1
View File
@@ -15,7 +15,7 @@ module Liquid
# @liquid_category iteration # @liquid_category iteration
# @liquid_name break # @liquid_name break
# @liquid_summary # @liquid_summary
# Stops a [`for` loop](/docs/api/liquid/tags/for) from iterating. # Stops a [`for` loop](/api/liquid/tags#for) from iterating.
# @liquid_syntax # @liquid_syntax
# {% break %} # {% break %}
class Break < Tag class Break < Tag
@@ -26,4 +26,6 @@ module Liquid
output output
end end
end end
Template.register_tag('break', Break)
end end
+2
View File
@@ -39,4 +39,6 @@ module Liquid
true true
end end
end end
Template.register_tag('capture', Capture)
end end
+3 -1
View File
@@ -77,7 +77,7 @@ module Liquid
end end
result = Liquid::Utils.to_liquid_value( result = Liquid::Utils.to_liquid_value(
block.evaluate(context), block.evaluate(context)
) )
if result if result
@@ -123,4 +123,6 @@ module Liquid
end end
end end
end end
Template.register_tag('case', Case)
end end
+3 -60
View File
@@ -8,7 +8,7 @@ module Liquid
# @liquid_summary # @liquid_summary
# Prevents an expression from being rendered or output. # Prevents an expression from being rendered or output.
# @liquid_description # @liquid_description
# Any text inside `comment` tags won't be output, and any Liquid code will be parsed, but not executed. # Any text inside `comment` tags won't be output, and any Liquid code won't be rendered.
# @liquid_syntax # @liquid_syntax
# {% comment %} # {% comment %}
# content # content
@@ -25,64 +25,7 @@ module Liquid
def blank? def blank?
true true
end end
private
def parse_body(body, tokenizer)
if parse_context.depth >= MAX_DEPTH
raise StackLevelError, "Nesting too deep"
end
parse_context.depth += 1
comment_tag_depth = 1
begin
# Consume tokens without creating child nodes.
# The children tag doesn't require to be a valid Liquid except the comment and raw tag.
# The child comment and raw tag must be closed.
while (token = tokenizer.send(:shift))
tag_name = if tokenizer.for_liquid_tag
next if token.empty? || token.match?(BlockBody::WhitespaceOrNothing)
tag_name_match = BlockBody::LiquidTagToken.match(token)
next if tag_name_match.nil?
tag_name_match[1]
else
token =~ BlockBody::FullToken
Regexp.last_match(2)
end
case tag_name
when "raw"
parse_raw_tag_body(tokenizer)
when "comment"
comment_tag_depth += 1
when "endcomment"
comment_tag_depth -= 1
end
if comment_tag_depth.zero?
parse_context.trim_whitespace = (token[-3] == WhitespaceControl) unless tokenizer.for_liquid_tag
return false
end
end
raise_tag_never_closed(block_name)
ensure
parse_context.depth -= 1
end
false
end
def parse_raw_tag_body(tokenizer)
while (token = tokenizer.send(:shift))
return if token =~ BlockBody::FullTokenPossiblyInvalid && "endraw" == Regexp.last_match(2)
end
raise_tag_never_closed("raw")
end
end end
Template.register_tag('comment', Comment)
end end
+3 -1
View File
@@ -6,7 +6,7 @@ module Liquid
# @liquid_category iteration # @liquid_category iteration
# @liquid_name continue # @liquid_name continue
# @liquid_summary # @liquid_summary
# Causes a [`for` loop](/docs/api/liquid/tags/for) to skip to the next iteration. # Causes a [`for` loop](/api/liquid/tags#for) to skip to the next iteration.
# @liquid_syntax # @liquid_syntax
# {% continue %} # {% continue %}
class Continue < Tag class Continue < Tag
@@ -17,4 +17,6 @@ module Liquid
output output
end end
end end
Template.register_tag('continue', Continue)
end end
+3 -7
View File
@@ -6,7 +6,7 @@ module Liquid
# @liquid_category iteration # @liquid_category iteration
# @liquid_name cycle # @liquid_name cycle
# @liquid_summary # @liquid_summary
# Loops through a group of strings and outputs them one at a time for each iteration of a [`for` loop](/docs/api/liquid/tags/for). # Loops through a group of strings and outputs them one at a time for each iteration of a [`for` loop](/api/liquid/tags#for).
# @liquid_description # @liquid_description
# The `cycle` tag must be used inside a `for` loop. # The `cycle` tag must be used inside a `for` loop.
# #
@@ -26,20 +26,14 @@ module Liquid
when NamedSyntax when NamedSyntax
@variables = variables_from_string(Regexp.last_match(2)) @variables = variables_from_string(Regexp.last_match(2))
@name = parse_expression(Regexp.last_match(1)) @name = parse_expression(Regexp.last_match(1))
@is_named = true
when SimpleSyntax when SimpleSyntax
@variables = variables_from_string(markup) @variables = variables_from_string(markup)
@name = @variables.to_s @name = @variables.to_s
@is_named = !@name.match?(/\w+:0x\h{8}/)
else else
raise SyntaxError, options[:locale].t("errors.syntax.cycle") raise SyntaxError, options[:locale].t("errors.syntax.cycle")
end end
end end
def named?
@is_named
end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
context.registers[:cycle] ||= {} context.registers[:cycle] ||= {}
@@ -78,4 +72,6 @@ module Liquid
end end
end end
end end
Template.register_tag('cycle', Cycle)
end end
+4 -2
View File
@@ -12,8 +12,8 @@ module Liquid
# or [section](/themes/architecture/sections) file that they're created in. However, the variable is shared across # or [section](/themes/architecture/sections) file that they're created in. However, the variable is shared across
# [snippets](/themes/architecture#snippets) included in the file. # [snippets](/themes/architecture#snippets) included in the file.
# #
# Similarly, variables that are created with `decrement` are independent from those created with [`assign`](/docs/api/liquid/tags/assign) # Similarly, variables that are created with `decrement` are independent from those created with [`assign`](/api/liquid/tags#assign)
# and [`capture`](/docs/api/liquid/tags/capture). However, `decrement` and [`increment`](/docs/api/liquid/tags/increment) share # and [`capture`](/api/liquid/tags#capture). However, `decrement` and [`increment`](/api/liquid/tags#increment) share
# variables. # variables.
# @liquid_syntax # @liquid_syntax
# {% decrement variable_name %} # {% decrement variable_name %}
@@ -35,4 +35,6 @@ module Liquid
output output
end end
end end
Template.register_tag('decrement', Decrement)
end end
+4 -2
View File
@@ -9,10 +9,10 @@ module Liquid
# Outputs an expression. # Outputs an expression.
# @liquid_description # @liquid_description
# Using the `echo` tag is the same as wrapping an expression in curly brackets (`{{` and `}}`). However, unlike the curly # Using the `echo` tag is the same as wrapping an expression in curly brackets (`{{` and `}}`). However, unlike the curly
# bracket method, you can use the `echo` tag inside [`liquid` tags](/docs/api/liquid/tags/liquid). # bracket method, you can use the `echo` tag inside [`liquid` tags](/api/liquid/tags#liquid).
# #
# > Tip: # > Tip:
# > You can use [filters](/docs/api/liquid/filters) on expressions inside `echo` tags. # > You can use [filters](/api/liquid/filters) on expressions inside `echo` tags.
# @liquid_syntax # @liquid_syntax
# {% liquid # {% liquid
# echo expression # echo expression
@@ -36,4 +36,6 @@ module Liquid
end end
end end
end end
Template.register_tag('echo', Echo)
end end
+7 -5
View File
@@ -9,10 +9,10 @@ module Liquid
# Renders an expression for every item in an array. # Renders an expression for every item in an array.
# @liquid_description # @liquid_description
# You can do a maximum of 50 iterations with a `for` loop. If you need to iterate over more than 50 items, then use the # You can do a maximum of 50 iterations with a `for` loop. If you need to iterate over more than 50 items, then use the
# [`paginate` tag](/docs/api/liquid/tags/paginate) to split the items over multiple pages. # [`paginate` tag](/api/liquid/tags#paginate) to split the items over multiple pages.
# #
# > Tip: # > Tip:
# > Every `for` loop has an associated [`forloop` object](/docs/api/liquid/objects/forloop) with information about the loop. # > Every `for` loop has an associated [`forloop` object](/api/liquid/objects#forloop) with information about the loop.
# @liquid_syntax # @liquid_syntax
# {% for variable in array %} # {% for variable in array %}
# expression # expression
@@ -98,12 +98,11 @@ module Liquid
@name = "#{@variable_name}-#{collection_name}" @name = "#{@variable_name}-#{collection_name}"
@reversed = p.id?('reversed') @reversed = p.id?('reversed')
while p.look(:comma) || p.look(:id) while p.look(:id) && p.look(:colon, 1)
p.consume?(:comma)
unless (attribute = p.id?('limit') || p.id?('offset')) unless (attribute = p.id?('limit') || p.id?('offset'))
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_attribute") raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_attribute")
end end
p.consume(:colon) p.consume
set_attribute(attribute, p.expression) set_attribute(attribute, p.expression)
end end
p.consume(:end_of_string) p.consume(:end_of_string)
@@ -178,6 +177,7 @@ module Liquid
case key case key
when 'offset' when 'offset'
@from = if expr == 'continue' @from = if expr == 'continue'
Usage.increment('for_offset_continue')
:continue :continue
else else
parse_expression(expr) parse_expression(expr)
@@ -201,4 +201,6 @@ module Liquid
end end
end end
end end
Template.register_tag('for', For)
end end
+4 -2
View File
@@ -53,7 +53,7 @@ module Liquid
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
@blocks.each do |block| @blocks.each do |block|
result = Liquid::Utils.to_liquid_value( result = Liquid::Utils.to_liquid_value(
block.evaluate(context), block.evaluate(context)
) )
if result if result
@@ -111,7 +111,7 @@ module Liquid
def parse_binary_comparisons(p) def parse_binary_comparisons(p)
condition = parse_comparison(p) condition = parse_comparison(p)
first_condition = condition first_condition = condition
while (op = p.id?('and') || p.id?('or')) while (op = (p.id?('and') || p.id?('or')))
child_condition = parse_comparison(p) child_condition = parse_comparison(p)
condition.send(op, child_condition) condition.send(op, child_condition)
condition = child_condition condition = child_condition
@@ -135,4 +135,6 @@ module Liquid
end end
end end
end end
Template.register_tag('if', If)
end end
+2
View File
@@ -14,4 +14,6 @@ module Liquid
output output
end end
end end
Template.register_tag('ifchanged', Ifchanged)
end end
+8 -8
View File
@@ -8,7 +8,7 @@ module Liquid
# @liquid_summary # @liquid_summary
# Renders a [snippet](/themes/architecture#snippets). # Renders a [snippet](/themes/architecture#snippets).
# @liquid_description # @liquid_description
# Inside the snippet, you can access and alter variables that are [created](/docs/api/liquid/tags/variable-tags) outside of the # Inside the snippet, you can access and alter variables that are [created](/api/liquid/tags#variable-tags) outside of the
# snippet. # snippet.
# @liquid_syntax # @liquid_syntax
# {% include 'filename' %} # {% include 'filename' %}
@@ -16,7 +16,7 @@ module Liquid
# @liquid_deprecated # @liquid_deprecated
# Deprecated because the way that variables are handled reduces performance and makes code harder to both read and maintain. # Deprecated because the way that variables are handled reduces performance and makes code harder to both read and maintain.
# #
# The `include` tag has been replaced by [`render`](/docs/api/liquid/tags/render). # The `include` tag has been replaced by [`render`](/api/liquid/tags#render).
class Include < Tag class Include < Tag
prepend Tag::Disableable prepend Tag::Disableable
@@ -52,12 +52,12 @@ module Liquid
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
template_name = context.evaluate(@template_name_expr) template_name = context.evaluate(@template_name_expr)
raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name.is_a?(String) raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name
partial = PartialCache.load( partial = PartialCache.load(
template_name, template_name,
context: context, context: context,
parse_context: parse_context, parse_context: parse_context
) )
context_variable_name = @alias_name || template_name.split('/').last context_variable_name = @alias_name || template_name.split('/').last
@@ -70,11 +70,9 @@ module Liquid
old_template_name = context.template_name old_template_name = context.template_name
old_partial = context.partial old_partial = context.partial
begin begin
context.template_name = partial.name context.template_name = template_name
context.partial = true context.partial = true
context.stack do context.stack do
@attributes.each do |key, value| @attributes.each do |key, value|
context[key] = context.evaluate(value) context[key] = context.evaluate(value)
@@ -110,4 +108,6 @@ module Liquid
end end
end end
end end
Template.register_tag('include', Include)
end end
+4 -2
View File
@@ -12,8 +12,8 @@ module Liquid
# or [section](/themes/architecture/sections) file that they're created in. However, the variable is shared across # or [section](/themes/architecture/sections) file that they're created in. However, the variable is shared across
# [snippets](/themes/architecture#snippets) included in the file. # [snippets](/themes/architecture#snippets) included in the file.
# #
# Similarly, variables that are created with `increment` are independent from those created with [`assign`](/docs/api/liquid/tags/assign) # Similarly, variables that are created with `increment` are independent from those created with [`assign`](/api/liquid/tags#assign)
# and [`capture`](/docs/api/liquid/tags/capture). However, `increment` and [`decrement`](/docs/api/liquid/tags/decrement) share # and [`capture`](/api/liquid/tags#capture). However, `increment` and [`decrement`](/api/liquid/tags#decrement) share
# variables. # variables.
# @liquid_syntax # @liquid_syntax
# {% increment variable_name %} # {% increment variable_name %}
@@ -35,4 +35,6 @@ module Liquid
output output
end end
end end
Template.register_tag('increment', Increment)
end end
+15
View File
@@ -1,6 +1,19 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category syntax
# @liquid_name inline_comment
# @liquid_summary
# Prevents an expression from being rendered or output.
# @liquid_description
# Any text inside an `inline_comment` tag won't be rendered or output.
#
# You can create multi-line inline comments. However, each line must begin with a `#`.
# @liquid_syntax
# {% # content %}
# @liquid_syntax_keyword content The content of the comment.
class InlineComment < Tag class InlineComment < Tag
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
@@ -25,4 +38,6 @@ module Liquid
true true
end end
end end
Template.register_tag('#', InlineComment)
end end
+4 -2
View File
@@ -14,6 +14,7 @@ module Liquid
# @liquid_syntax_keyword expression The expression to be output without being rendered. # @liquid_syntax_keyword expression The expression to be output without being rendered.
class Raw < Block class Raw < Block
Syntax = /\A\s*\z/ Syntax = /\A\s*\z/
FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
def initialize(tag_name, markup, parse_context) def initialize(tag_name, markup, parse_context)
super super
@@ -24,8 +25,7 @@ module Liquid
def parse(tokens) def parse(tokens)
@body = +'' @body = +''
while (token = tokens.shift) while (token = tokens.shift)
if token =~ BlockBody::FullTokenPossiblyInvalid && block_delimiter == Regexp.last_match(2) if token =~ FullTokenPossiblyInvalid && block_delimiter == Regexp.last_match(2)
parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
@body << Regexp.last_match(1) if Regexp.last_match(1) != "" @body << Regexp.last_match(1) if Regexp.last_match(1) != ""
return return
end end
@@ -56,4 +56,6 @@ module Liquid
end end
end end
end end
Template.register_tag('raw', Raw)
end end
+9 -7
View File
@@ -8,19 +8,19 @@ module Liquid
# @liquid_summary # @liquid_summary
# Renders a [snippet](/themes/architecture#snippets) or [app block](/themes/architecture/sections/section-schema#render-app-blocks). # Renders a [snippet](/themes/architecture#snippets) or [app block](/themes/architecture/sections/section-schema#render-app-blocks).
# @liquid_description # @liquid_description
# Inside snippets and app blocks, you can't directly access variables that are [created](/docs/api/liquid/tags/variable-tags) outside # Inside snippets and app blocks, you can't directly access variables that are [created](/api/liquid/tags#variable-tags) outside
# of the snippet or app block. However, you can [specify variables as parameters](/docs/api/liquid/tags/render#render-passing-variables-to-a-snippet) # of the snippet or app block. However, you can [specify variables as parameters](/api/liquid/tags#render-passing-variables-to-snippets)
# to pass outside variables to snippets. # to pass outside variables to snippets.
# #
# While you can't directly access created variables, you can access global objects, as well as any objects that are # While you can't directly access created variables, you can access global objects, as well as any objects that are
# directly accessible outside the snippet or app block. For example, a snippet or app block inside the [product template](/themes/architecture/templates/product) # directly accessible outside the snippet or app block. For example, a snippet or app block inside the [product template](/themes/architecture/templates/product)
# can access the [`product` object](/docs/api/liquid/objects/product), and a snippet or app block inside a [section](/themes/architecture/sections) # can access the [`product` object](/api/liquid/objects#product), and a snippet or app block inside a [section](/themes/architecture/sections)
# can access the [`section` object](/docs/api/liquid/objects/section). # can access the [`section` object](/api/liquid/objects#section).
# #
# Outside a snippet or app block, you can't access variables created inside the snippet or app block. # Outside a snippet or app block, you can't access variables created inside the snippet or app block.
# #
# > Note: # > Note:
# > When you render a snippet using the `render` tag, you can't use the [`include` tag](/docs/api/liquid/tags/include) # > When you render a snippet using the `render` tag, you can't use the [`include` tag](/api/liquid/tags#include)
# > inside the snippet. # > inside the snippet.
# @liquid_syntax # @liquid_syntax
# {% render 'filename' %} # {% render 'filename' %}
@@ -69,14 +69,14 @@ module Liquid
partial = PartialCache.load( partial = PartialCache.load(
template_name, template_name,
context: context, context: context,
parse_context: parse_context, parse_context: parse_context
) )
context_variable_name = @alias_name || template_name.split('/').last context_variable_name = @alias_name || template_name.split('/').last
render_partial_func = ->(var, forloop) { render_partial_func = ->(var, forloop) {
inner_context = context.new_isolated_subcontext inner_context = context.new_isolated_subcontext
inner_context.template_name = partial.name inner_context.template_name = template_name
inner_context.partial = true inner_context.partial = true
inner_context['forloop'] = forloop if forloop inner_context['forloop'] = forloop if forloop
@@ -108,4 +108,6 @@ module Liquid
end end
end end
end end
Template.register_tag('render', Render)
end end
+6 -18
View File
@@ -11,7 +11,7 @@ module Liquid
# The `tablerow` tag must be wrapped in HTML `<table>` and `</table>` tags. # The `tablerow` tag must be wrapped in HTML `<table>` and `</table>` tags.
# #
# > Tip: # > Tip:
# > Every `tablerow` loop has an associated [`tablerowloop` object](/docs/api/liquid/objects/tablerowloop) with information about the loop. # > Every `tablerow` loop has an associated [`tablerowloop` object](/api/liquid/objects#tablerowloop) with information about the loop.
# @liquid_syntax # @liquid_syntax
# {% tablerow variable in array %} # {% tablerow variable in array %}
# expression # expression
@@ -45,13 +45,13 @@ module Liquid
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
(collection = context.evaluate(@collection_name)) || (return '') (collection = context.evaluate(@collection_name)) || (return '')
from = @attributes.key?('offset') ? to_integer(context.evaluate(@attributes['offset'])) : 0 from = @attributes.key?('offset') ? context.evaluate(@attributes['offset']).to_i : 0
to = @attributes.key?('limit') ? from + to_integer(context.evaluate(@attributes['limit'])) : nil to = @attributes.key?('limit') ? from + context.evaluate(@attributes['limit']).to_i : nil
collection = Utils.slice_collection(collection, from, to) collection = Utils.slice_collection(collection, from, to)
length = collection.length length = collection.length
cols = @attributes.key?('cols') ? to_integer(context.evaluate(@attributes['cols'])) : length cols = context.evaluate(@attributes['cols']).to_i
output << "<tr class=\"row1\">\n" output << "<tr class=\"row1\">\n"
context.stack do context.stack do
@@ -65,12 +65,6 @@ module Liquid
super super
output << '</td>' output << '</td>'
# Handle any interrupts if they exist.
if context.interrupt?
interrupt = context.pop_interrupt
break if interrupt.is_a?(BreakInterrupt)
end
if tablerowloop.col_last && !tablerowloop.last if tablerowloop.col_last && !tablerowloop.last
output << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">" output << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">"
end end
@@ -88,13 +82,7 @@ module Liquid
super + @node.attributes.values + [@node.collection_name] super + @node.attributes.values + [@node.collection_name]
end end
end end
private
def to_integer(value)
value.to_i
rescue NoMethodError
raise Liquid::ArgumentError, "invalid integer"
end
end end
Template.register_tag('tablerow', TableRow)
end end
+5 -3
View File
@@ -11,7 +11,7 @@ module Liquid
# Renders an expression unless a specific condition is `true`. # Renders an expression unless a specific condition is `true`.
# @liquid_description # @liquid_description
# > Tip: # > Tip:
# > Similar to the [`if` tag](/docs/api/liquid/tags/if), you can use `elsif` to add more conditions to an `unless` tag. # > Similar to the [`if` tag](/api/liquid/tags#if), you can use `elsif` to add more conditions to an `unless` tag.
# @liquid_syntax # @liquid_syntax
# {% unless condition %} # {% unless condition %}
# expression # expression
@@ -23,7 +23,7 @@ module Liquid
# First condition is interpreted backwards ( if not ) # First condition is interpreted backwards ( if not )
first_block = @blocks.first first_block = @blocks.first
result = Liquid::Utils.to_liquid_value( result = Liquid::Utils.to_liquid_value(
first_block.evaluate(context), first_block.evaluate(context)
) )
unless result unless result
@@ -33,7 +33,7 @@ module Liquid
# After the first condition unless works just like if # After the first condition unless works just like if
@blocks[1..-1].each do |block| @blocks[1..-1].each do |block|
result = Liquid::Utils.to_liquid_value( result = Liquid::Utils.to_liquid_value(
block.evaluate(context), block.evaluate(context)
) )
if result if result
@@ -44,4 +44,6 @@ module Liquid
output output
end end
end end
Template.register_tag('unless', Unless)
end end
+61 -64
View File
@@ -15,9 +15,45 @@ module Liquid
# template.render('user_name' => 'bob') # template.render('user_name' => 'bob')
# #
class Template class Template
attr_accessor :root, :name attr_accessor :root
attr_reader :resource_limits, :warnings attr_reader :resource_limits, :warnings
class TagRegistry
include Enumerable
def initialize
@tags = {}
@cache = {}
end
def [](tag_name)
return nil unless @tags.key?(tag_name)
return @cache[tag_name] if Liquid.cache_classes
lookup_class(@tags[tag_name]).tap { |o| @cache[tag_name] = o }
end
def []=(tag_name, klass)
@tags[tag_name] = klass.name
@cache[tag_name] = klass
end
def delete(tag_name)
@tags.delete(tag_name)
@cache.delete(tag_name)
end
def each(&block)
@tags.each(&block)
end
private
def lookup_class(name)
Object.const_get(name)
end
end
attr_reader :profiler attr_reader :profiler
class << self class << self
@@ -25,83 +61,52 @@ module Liquid
# :lax acts like liquid 2.5 and silently ignores malformed tags in most cases. # :lax acts like liquid 2.5 and silently ignores malformed tags in most cases.
# :warn is the default and will give deprecation warnings when invalid syntax is used. # :warn is the default and will give deprecation warnings when invalid syntax is used.
# :strict will enforce correct syntax. # :strict will enforce correct syntax.
def error_mode=(mode) attr_accessor :error_mode
Deprecations.warn("Template.error_mode=", "Environment#error_mode=") Template.error_mode = :lax
Environment.default.error_mode = mode
attr_accessor :default_exception_renderer
Template.default_exception_renderer = lambda do |exception|
exception
end end
def error_mode attr_accessor :file_system
Environment.default.error_mode Template.file_system = BlankFileSystem.new
end
def default_exception_renderer=(renderer) attr_accessor :tags
Deprecations.warn("Template.default_exception_renderer=", "Environment#exception_renderer=") Template.tags = TagRegistry.new
Environment.default.exception_renderer = renderer private :tags=
end
def default_exception_renderer
Environment.default.exception_renderer
end
def file_system=(file_system)
Deprecations.warn("Template.file_system=", "Environment#file_system=")
Environment.default.file_system = file_system
end
def file_system
Environment.default.file_system
end
def tags
Environment.default.tags
end
def register_tag(name, klass) def register_tag(name, klass)
Deprecations.warn("Template.register_tag", "Environment#register_tag") tags[name.to_s] = klass
Environment.default.register_tag(name, klass)
end end
# Pass a module with filter methods which should be available # Pass a module with filter methods which should be available
# to all liquid views. Good for registering the standard library # to all liquid views. Good for registering the standard library
def register_filter(mod) def register_filter(mod)
Deprecations.warn("Template.register_filter", "Environment#register_filter") StrainerFactory.add_global_filter(mod)
Environment.default.register_filter(mod)
end end
private def default_resource_limits=(limits) attr_accessor :default_resource_limits
Deprecations.warn("Template.default_resource_limits=", "Environment#default_resource_limits=") Template.default_resource_limits = {}
Environment.default.default_resource_limits = limits private :default_resource_limits=
end
def default_resource_limits
Environment.default.default_resource_limits
end
# creates a new <tt>Template</tt> object from liquid source code # creates a new <tt>Template</tt> object from liquid source code
# To enable profiling, pass in <tt>profile: true</tt> as an option. # To enable profiling, pass in <tt>profile: true</tt> as an option.
# See Liquid::Profiler for more information # See Liquid::Profiler for more information
def parse(source, options = {}) def parse(source, options = {})
environment = options[:environment] || Environment.default new.parse(source, options)
new(environment: environment).parse(source, options)
end end
end end
def initialize(environment: Environment.default) def initialize
@environment = environment
@rethrow_errors = false @rethrow_errors = false
@resource_limits = ResourceLimits.new(environment.default_resource_limits) @resource_limits = ResourceLimits.new(Template.default_resource_limits)
end end
# Parse source code. # Parse source code.
# Returns self for easy chaining # Returns self for easy chaining
def parse(source, options = {}) def parse(source, options = {})
parse_context = configure_options(options) parse_context = configure_options(options)
source = source.to_s.to_str
unless source.valid_encoding?
raise TemplateEncodingError, parse_context.locale.t("errors.syntax.invalid_template_encoding")
end
tokenizer = parse_context.new_tokenizer(source, start_line_number: @line_numbers && 1) tokenizer = parse_context.new_tokenizer(source, start_line_number: @line_numbers && 1)
@root = Document.parse(tokenizer, parse_context) @root = Document.parse(tokenizer, parse_context)
self self
@@ -151,11 +156,11 @@ module Liquid
c c
when Liquid::Drop when Liquid::Drop
drop = args.shift drop = args.shift
drop.context = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits, {}, @environment) drop.context = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits)
when Hash when Hash
Context.new([args.shift, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits, {}, @environment) Context.new([args.shift, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits)
when nil when nil
Context.new(assigns, instance_assigns, registers, @rethrow_errors, @resource_limits, {}, @environment) Context.new(assigns, instance_assigns, registers, @rethrow_errors, @resource_limits)
else else
raise ArgumentError, "Expected Hash or Liquid::Context as parameter" raise ArgumentError, "Expected Hash or Liquid::Context as parameter"
end end
@@ -184,8 +189,6 @@ module Liquid
@profiler = context.profiler = Liquid::Profiler.new @profiler = context.profiler = Liquid::Profiler.new
end end
context.template_name ||= name
begin begin
# render the nodelist. # render the nodelist.
@root.render_to_output_buffer(context, output || +'') @root.render_to_output_buffer(context, output || +'')
@@ -215,14 +218,8 @@ module Liquid
@options = options @options = options
@profiling = profiling @profiling = profiling
@line_numbers = options[:line_numbers] || @profiling @line_numbers = options[:line_numbers] || @profiling
parse_context = if options.is_a?(ParseContext) parse_context = options.is_a?(ParseContext) ? options : ParseContext.new(options)
options @warnings = parse_context.warnings
else
opts = options.key?(:environment) ? options : options.merge(environment: @environment)
ParseContext.new(opts)
end
@warnings = parse_context.warnings
parse_context parse_context
end end
-44
View File
@@ -1,44 +0,0 @@
# frozen_string_literal: true
module Liquid
class Template
class TagRegistry
include Enumerable
def initialize(tags = nil)
@tags = {}
@cache = {}
tags.each { |tag_name, klass| self[tag_name] = klass }
Deprecations.warn("Template::TagRegistry", "Use a Environment instance with zeitwerk")
end
def [](tag_name)
return nil unless @tags.key?(tag_name)
return @cache[tag_name] if Liquid.cache_classes
lookup_class(@tags[tag_name]).tap { |o| @cache[tag_name] = o }
end
def delete(tag_name)
Deprecations.warn("Template::TagRegistry#delete", "Use a Environment instance with immutable tags")
@tags.delete(tag_name)
@cache.delete(tag_name)
end
def []=(tag_name, klass)
@tags[tag_name] = klass.name
@cache[tag_name] = klass
end
def each(&block)
@tags.each(&block)
end
private
def lookup_class(name)
Object.const_get(name)
end
end
end
end
+3 -9
View File
@@ -5,18 +5,14 @@ module Liquid
attr_reader :line_number, :for_liquid_tag attr_reader :line_number, :for_liquid_tag
def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false) def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false)
@source = source @source = source.to_s.to_str
@line_number = line_number || (line_numbers ? 1 : nil) @line_number = line_number || (line_numbers ? 1 : nil)
@for_liquid_tag = for_liquid_tag @for_liquid_tag = for_liquid_tag
@offset = 0
@tokens = tokenize @tokens = tokenize
end end
def shift def shift
token = @tokens[@offset] (token = @tokens.shift) || return
return nil unless token
@offset += 1
if @line_number if @line_number
@line_number += @for_liquid_tag ? 1 : token.count("\n") @line_number += @for_liquid_tag ? 1 : token.count("\n")
@@ -35,9 +31,7 @@ module Liquid
tokens = @source.split(TemplateParser) tokens = @source.split(TemplateParser)
# removes the rogue empty element at the beginning of the array # removes the rogue empty element at the beginning of the array
if tokens[0]&.empty? tokens.shift if tokens[0]&.empty?
@offset += 1
end
tokens tokens
end end
+1 -1
View File
@@ -2,5 +2,5 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
VERSION = "5.6.0.alpha" VERSION = "5.4.0"
end end
+1 -4
View File
@@ -17,7 +17,7 @@ Gem::Specification.new do |s|
s.license = "MIT" s.license = "MIT"
# s.description = "A secure, non-evaling end user template engine with aesthetic markup." # s.description = "A secure, non-evaling end user template engine with aesthetic markup."
s.required_ruby_version = ">= 3.0.0" s.required_ruby_version = ">= 2.7.0"
s.required_rubygems_version = ">= 1.3.7" s.required_rubygems_version = ">= 1.3.7"
s.metadata['allowed_push_host'] = 'https://rubygems.org' s.metadata['allowed_push_host'] = 'https://rubygems.org'
@@ -28,9 +28,6 @@ Gem::Specification.new do |s|
s.require_path = "lib" s.require_path = "lib"
s.add_dependency("strscan")
s.add_dependency("bigdecimal")
s.add_development_dependency('rake', '~> 13.0') s.add_development_dependency('rake', '~> 13.0')
s.add_development_dependency('minitest') s.add_development_dependency('minitest')
end end
-1
View File
@@ -3,7 +3,6 @@
require 'benchmark/ips' require 'benchmark/ips'
require_relative 'theme_runner' require_relative 'theme_runner'
RubyVM::YJIT.enable if defined?(RubyVM::YJIT)
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new profiler = ThemeRunner.new
+5
View File
@@ -49,3 +49,8 @@ module Database
end end
end end
end end
if __FILE__ == $PROGRAM_NAME
p(Database.tables['collections']['frontpage'].keys)
# p Database.tables['blog']['articles']
end
+2 -2
View File
@@ -45,8 +45,8 @@ class Paginate < Liquid::Block
pagination['items'] = collection_size pagination['items'] = collection_size
pagination['pages'] = page_count - 1 pagination['pages'] = page_count - 1
pagination['previous'] = link('&laquo; Previous', current_page - 1) if 1 < current_page pagination['previous'] = link('&laquo; Previous', current_page - 1) unless 1 >= current_page
pagination['next'] = link('Next &raquo;', current_page + 1) if page_count > current_page + 1 pagination['next'] = link('Next &raquo;', current_page + 1) unless page_count <= current_page + 1
pagination['parts'] = [] pagination['parts'] = []
hellip_break = false hellip_break = false
-62
View File
@@ -1,62 +0,0 @@
# frozen_string_literal: true
require "benchmark/ips"
# benchmark liquid lexing
require 'liquid'
RubyVM::YJIT.enable
EXPRESSIONS = [
"foo[1..2].baz",
"12.0",
"foo.bar.based",
"21 - 62",
"foo.bar.baz",
"foo > 12",
"foo < 12",
"foo <= 12",
"foo >= 12",
"foo <> 12",
"foo == 12",
"foo != 12",
"foo contains 12",
"foo contains 'bar'",
"foo != 'bar'",
"'foo' contains 'bar'",
'234089',
"foo | default: -1",
]
EXPRESSIONS.each do |expr|
lexer_1_result = Liquid::Lexer1.new(expr).tokenize
lexer_2_result = Liquid::Lexer2.new(expr).tokenize
next if lexer_1_result == lexer_2_result
warn "Lexer1 and Lexer2 results are different for expression: #{expr}"
warn "expected: #{lexer_1_result}"
warn "got: #{lexer_2_result}"
abort
end
Benchmark.ips do |x|
x.config(time: 10, warmup: 5)
x.report("Liquid::Lexer1#tokenize") do
EXPRESSIONS.each do |expr|
l = Liquid::Lexer1.new(expr)
l.tokenize
end
end
x.report("Liquid::Lexer2#tokenize") do
EXPRESSIONS.each do |expr|
l = Liquid::Lexer2.new(expr)
l.tokenize
end
end
x.compare!
end
+8 -17
View File
@@ -14,25 +14,19 @@ class AssignTest < Minitest::Test
end end
def test_assigned_variable def test_assigned_variable
assert_template_result( assert_template_result('.foo.',
'.foo.',
'{% assign foo = values %}.{{ foo[0] }}.', '{% assign foo = values %}.{{ foo[0] }}.',
{ 'values' => %w(foo bar baz) }, { 'values' => %w(foo bar baz) })
)
assert_template_result( assert_template_result('.bar.',
'.bar.',
'{% assign foo = values %}.{{ foo[1] }}.', '{% assign foo = values %}.{{ foo[1] }}.',
{ 'values' => %w(foo bar baz) }, { 'values' => %w(foo bar baz) })
)
end end
def test_assign_with_filter def test_assign_with_filter
assert_template_result( assert_template_result('.bar.',
'.bar.',
'{% assign foo = values | split: "," %}.{{ foo[1] }}.', '{% assign foo = values | split: "," %}.{{ foo[1] }}.',
{ 'values' => "foo,bar,baz" }, { 'values' => "foo,bar,baz" })
)
end end
def test_assign_syntax_error def test_assign_syntax_error
@@ -40,11 +34,8 @@ class AssignTest < Minitest::Test
end end
def test_assign_uses_error_mode def test_assign_uses_error_mode
assert_match_syntax_error( assert_match_syntax_error("Expected dotdot but found pipe in ",
"Expected dotdot but found pipe in ", "{% assign foo = ('X' | downcase) %}", error_mode: :strict)
"{% assign foo = ('X' | downcase) %}",
error_mode: :strict,
)
assert_template_result("", "{% assign foo = ('X' | downcase) %}", error_mode: :lax) assert_template_result("", "{% assign foo = ('X' | downcase) %}", error_mode: :lax)
end end
+9 -20
View File
@@ -27,7 +27,7 @@ class BlankTest < Minitest::Test
def test_new_tags_are_not_blank_by_default def test_new_tags_are_not_blank_by_default
with_custom_tag('foobar', FoobarTag) do with_custom_tag('foobar', FoobarTag) do
assert_equal(" " * N, Liquid::Template.parse(wrap_in_for("{% foobar %}")).render!) assert_template_result(" " * N, wrap_in_for("{% foobar %}"))
end end
end end
@@ -57,11 +57,9 @@ class BlankTest < Minitest::Test
def test_nested_blocks_are_blank_but_only_if_all_children_are def test_nested_blocks_are_blank_but_only_if_all_children_are
assert_template_result("", wrap(wrap(" "))) assert_template_result("", wrap(wrap(" ")))
assert_template_result( assert_template_result("\n but this is not " * (N + 1),
"\n but this is not " * (N + 1),
wrap('{% if true %} {% comment %} this is blank {% endcomment %} {% endif %} wrap('{% if true %} {% comment %} this is blank {% endcomment %} {% endif %}
{% if true %} but this is not {% endif %}'), {% if true %} but this is not {% endif %}'))
)
end end
def test_assigns_are_blank def test_assigns_are_blank
@@ -91,21 +89,12 @@ class BlankTest < Minitest::Test
end end
def test_include_is_blank def test_include_is_blank
assert_template_result( assert_template_result("foobar" * (N + 1), wrap("{% include 'foobar' %}"),
"foobar" * (N + 1), partials: { 'foobar' => 'foobar' })
wrap("{% include 'foobar' %}"), assert_template_result(" foobar " * (N + 1), wrap("{% include ' foobar ' %}"),
partials: { 'foobar' => 'foobar' }, partials: { ' foobar ' => ' foobar ' })
) assert_template_result(" " * (N + 1), wrap(" {% include ' ' %} "),
assert_template_result( partials: { ' ' => ' ' })
" foobar " * (N + 1),
wrap("{% include ' foobar ' %}"),
partials: { ' foobar ' => ' foobar ' },
)
assert_template_result(
" " * (N + 1),
wrap(" {% include ' ' %} "),
partials: { ' ' => ' ' },
)
end end
def test_case_is_blank def test_case_is_blank
+22 -73
View File
@@ -36,24 +36,6 @@ class Category
end end
end end
class ProductsDrop < Liquid::Drop
def initialize(products)
@products = products
end
def size
@products.size
end
def to_liquid
if @context["forloop"]
@products.first(@context["forloop"].length)
else
@products
end
end
end
class CategoryDrop < Liquid::Drop class CategoryDrop < Liquid::Drop
attr_accessor :category, :context attr_accessor :category, :context
@@ -139,23 +121,14 @@ class ContextTest < Minitest::Test
end end
def test_length_query def test_length_query
assert_template_result( assert_template_result("true", "{% if numbers.size == 4 %}true{% endif %}",
"true", { "numbers" => [1, 2, 3, 4] })
"{% if numbers.size == 4 %}true{% endif %}",
{ "numbers" => [1, 2, 3, 4] },
)
assert_template_result( assert_template_result("true", "{% if numbers.size == 4 %}true{% endif %}",
"true", { "numbers" => { 1 => 1, 2 => 2, 3 => 3, 4 => 4 } })
"{% if numbers.size == 4 %}true{% endif %}",
{ "numbers" => { 1 => 1, 2 => 2, 3 => 3, 4 => 4 } },
)
assert_template_result( assert_template_result("true", "{% if numbers.size == 1000 %}true{% endif %}",
"true", { "numbers" => { 1 => 1, 2 => 2, 3 => 3, 4 => 4, 'size' => 1000 } })
"{% if numbers.size == 1000 %}true{% endif %}",
{ "numbers" => { 1 => 1, 2 => 2, 3 => 3, 4 => 4, 'size' => 1000 } },
)
end end
def test_hyphenated_variable def test_hyphenated_variable
@@ -255,14 +228,12 @@ class ContextTest < Minitest::Test
end end
def test_hash_to_array_transition def test_hash_to_array_transition
assigns = { assigns = { 'colors' => {
'colors' => { 'Blue' => ['003366', '336699', '6699CC', '99CCFF'],
'Blue' => ['003366', '336699', '6699CC', '99CCFF'], 'Green' => ['003300', '336633', '669966', '99CC99'],
'Green' => ['003300', '336633', '669966', '99CC99'], 'Yellow' => ['CC9900', 'FFCC00', 'FFFF99', 'FFFFCC'],
'Yellow' => ['CC9900', 'FFCC00', 'FFFF99', 'FFFFCC'], 'Red' => ['660000', '993333', 'CC6666', 'FF9999'],
'Red' => ['660000', '993333', 'CC6666', 'FF9999'], } }
},
}
assert_template_result("003366", "{{ colors.Blue[0] }}", assigns) assert_template_result("003366", "{{ colors.Blue[0] }}", assigns)
assert_template_result("FF9999", "{{ colors.Red[3] }}", assigns) assert_template_result("FF9999", "{{ colors.Red[3] }}", assigns)
@@ -291,7 +262,7 @@ class ContextTest < Minitest::Test
assigns = { 'product' => { 'variants' => [{ 'title' => 'draft151cm' }, { 'title' => 'element151cm' }] } } assigns = { 'product' => { 'variants' => [{ 'title' => 'draft151cm' }, { 'title' => 'element151cm' }] } }
assert_template_result("draft151cm", '{{ product["variants"][0]["title"] }}', assigns) assert_template_result("draft151cm", '{{ product["variants"][0]["title"] }}', assigns)
assert_template_result("element151cm", '{{ product["variants"][1]["title"] }}', assigns) assert_template_result("element151cm", '{{ product["variants"][1]["title"] }}', assigns)
assert_template_result("draft151cm", '{{ product["variants"].first["title"] }}', assigns) assert_template_result("draft151cm", '{{ product["variants"][0]["title"] }}', assigns)
assert_template_result("element151cm", '{{ product["variants"].last["title"] }}', assigns) assert_template_result("element151cm", '{{ product["variants"].last["title"] }}', assigns)
end end
@@ -439,12 +410,10 @@ class ContextTest < Minitest::Test
def test_nested_lambda_is_called_once def test_nested_lambda_is_called_once
@global = 0 @global = 0
@context['callcount'] = { @context['callcount'] = { "lambda" => proc {
"lambda" => proc { @global += 1
@global += 1 @global.to_s
@global.to_s } }
},
}
assert_equal('1', @context['callcount.lambda']) assert_equal('1', @context['callcount.lambda'])
assert_equal('1', @context['callcount.lambda']) assert_equal('1', @context['callcount.lambda'])
@@ -454,11 +423,10 @@ class ContextTest < Minitest::Test
def test_lambda_in_array_is_called_once def test_lambda_in_array_is_called_once
@global = 0 @global = 0
p = proc { @context['callcount'] = [1, 2, proc {
@global += 1 @global += 1
@global.to_s @global.to_s
} }, 4, 5]
@context['callcount'] = [1, 2, p, 4, 5]
assert_equal('1', @context['callcount[2]']) assert_equal('1', @context['callcount[2]'])
assert_equal('1', @context['callcount[2]']) assert_equal('1', @context['callcount[2]'])
@@ -505,7 +473,7 @@ class ContextTest < Minitest::Test
def test_static_environments_are_read_with_lower_priority_than_environments def test_static_environments_are_read_with_lower_priority_than_environments
context = Context.build( context = Context.build(
static_environments: { 'shadowed' => 'static', 'unshadowed' => 'static' }, static_environments: { 'shadowed' => 'static', 'unshadowed' => 'static' },
environments: { 'shadowed' => 'dynamic' }, environments: { 'shadowed' => 'dynamic' }
) )
assert_equal('dynamic', context['shadowed']) assert_equal('dynamic', context['shadowed'])
@@ -653,25 +621,6 @@ class ContextTest < Minitest::Test
assert_equal(:my_value, c.registers[:my_register]) assert_equal(:my_value, c.registers[:my_register])
end end
def test_variable_to_liquid_returns_contextual_drop
context = {
"products" => ProductsDrop.new(["A", "B", "C", "D", "E"]),
}
template = Liquid::Template.parse(<<~LIQUID)
{%- for i in (1..3) -%}
for_loop_products_count: {{ products | size }}
{% endfor %}
unscoped_products_count: {{ products | size }}
LIQUID
result = template.render(context)
assert_includes(result, "for_loop_products_count: 3")
assert_includes(result, "unscoped_products_count: 5")
end
private private
def assert_no_object_allocations def assert_no_object_allocations
+15 -107
View File
@@ -105,8 +105,7 @@ class ErrorHandlingTest < Minitest::Test
end end
def test_parsing_warn_with_line_numbers_adds_numbers_to_lexer_errors def test_parsing_warn_with_line_numbers_adds_numbers_to_lexer_errors
template = Liquid::Template.parse( template = Liquid::Template.parse('
'
foobar foobar
{% if 1 =! 2 %}ok{% endif %} {% if 1 =! 2 %}ok{% endif %}
@@ -114,19 +113,15 @@ class ErrorHandlingTest < Minitest::Test
bla bla
', ',
error_mode: :warn, error_mode: :warn,
line_numbers: true, line_numbers: true)
)
assert_equal( assert_equal(['Liquid syntax error (line 4): Unexpected character = in "1 =! 2"'],
['Liquid syntax error (line 4): Unexpected character = in "1 =! 2"'], template.warnings.map(&:message))
template.warnings.map(&:message),
)
end end
def test_parsing_strict_with_line_numbers_adds_numbers_to_lexer_errors def test_parsing_strict_with_line_numbers_adds_numbers_to_lexer_errors
err = assert_raises(SyntaxError) do err = assert_raises(SyntaxError) do
Liquid::Template.parse( Liquid::Template.parse('
'
foobar foobar
{% if 1 =! 2 %}ok{% endif %} {% if 1 =! 2 %}ok{% endif %}
@@ -134,8 +129,7 @@ class ErrorHandlingTest < Minitest::Test
bla bla
', ',
error_mode: :strict, error_mode: :strict,
line_numbers: true, line_numbers: true)
)
end end
assert_equal('Liquid syntax error (line 4): Unexpected character = in "1 =! 2"', err.message) assert_equal('Liquid syntax error (line 4): Unexpected character = in "1 =! 2"', err.message)
@@ -219,21 +213,6 @@ class ErrorHandlingTest < Minitest::Test
Liquid::Template.default_exception_renderer = old_exception_renderer if old_exception_renderer Liquid::Template.default_exception_renderer = old_exception_renderer if old_exception_renderer
end end
def test_setting_exception_renderer_on_environment
exceptions = []
exception_renderer = ->(e) do
exceptions << e
''
end
environment = Liquid::Environment.build(exception_renderer: exception_renderer)
template = Liquid::Template.parse('This is a runtime error: {{ errors.argument_error }}', environment: environment)
output = template.render('errors' => ErrorDrop.new)
assert_equal('This is a runtime error: ', output)
assert_equal([Liquid::ArgumentError], template.errors.map(&:class))
end
def test_exception_renderer_exposing_non_liquid_error def test_exception_renderer_exposing_non_liquid_error
template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true) template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true)
exceptions = [] exceptions = []
@@ -257,10 +236,16 @@ class ErrorHandlingTest < Minitest::Test
end end
def test_included_template_name_with_line_numbers def test_included_template_name_with_line_numbers
environment = Liquid::Environment.build(file_system: TestFileSystem.new) old_file_system = Liquid::Template.file_system
template = Liquid::Template.parse("Argument error:\n{% include 'product' %}", line_numbers: true, environment: environment)
page = template.render('errors' => ErrorDrop.new)
begin
Liquid::Template.file_system = TestFileSystem.new
template = Liquid::Template.parse("Argument error:\n{% include 'product' %}", line_numbers: true)
page = template.render('errors' => ErrorDrop.new)
ensure
Liquid::Template.file_system = old_file_system
end
assert_equal("Argument error:\nLiquid error (product line 1): argument error", page) assert_equal("Argument error:\nLiquid error (product line 1): argument error", page)
assert_equal("product", template.errors.first.template_name) assert_equal("product", template.errors.first.template_name)
end end
@@ -272,81 +257,4 @@ class ErrorHandlingTest < Minitest::Test
output = Liquid::Template.parse("{% assign x = 0 %}{% if 1 < '2' %}{% assign x = 3 %}{% endif %}{{ x }}").render output = Liquid::Template.parse("{% assign x = 0 %}{% if 1 < '2' %}{% assign x = 3 %}{% endif %}{{ x }}").render
assert_equal("0", output) assert_equal("0", output)
end end
def test_syntax_error_is_raised_with_template_name
file_system = StubFileSystem.new("snippet" => "1\n2\n{{ 1")
context = Liquid::Context.build(
registers: { file_system: file_system },
)
template = Template.parse(
'{% render "snippet" %}',
line_numbers: true,
)
template.name = "template/index"
assert_equal(
"Liquid syntax error (snippet line 3): Variable '{{' was not properly terminated with regexp: /\\}\\}/",
template.render(context),
)
end
def test_syntax_error_is_raised_with_template_name_from_template_factory
file_system = StubFileSystem.new("snippet" => "1\n2\n{{ 1")
context = Liquid::Context.build(
registers: {
file_system: file_system,
template_factory: StubTemplateFactory.new,
},
)
template = Template.parse(
'{% render "snippet" %}',
line_numbers: true,
)
template.name = "template/index"
assert_equal(
"Liquid syntax error (some/path/snippet line 3): Variable '{{' was not properly terminated with regexp: /\\}\\}/",
template.render(context),
)
end
def test_error_is_raised_during_parse_with_template_name
depth = Liquid::Block::MAX_DEPTH + 1
code = "{% if true %}" * depth + "rendered" + "{% endif %}" * depth
template = Template.parse("{% render 'snippet' %}", line_numbers: true)
context = Liquid::Context.build(
registers: {
file_system: StubFileSystem.new("snippet" => code),
template_factory: StubTemplateFactory.new,
},
)
assert_equal("Liquid error (some/path/snippet line 1): Nesting too deep", template.render(context))
end
def test_internal_error_is_raised_with_template_name
template = Template.new
template.parse(
"{% render 'snippet' %}",
line_numbers: true,
)
template.name = "template/index"
context = Liquid::Context.build(
registers: {
file_system: StubFileSystem.new({}),
},
)
assert_equal(
"Liquid error (template/index line 1): internal",
template.render(context),
)
end
end end
+2 -2
View File
@@ -32,11 +32,11 @@ class ExpressionTest < Minitest::Test
assert_match_syntax_error( assert_match_syntax_error(
"Liquid syntax error (line 1): Invalid expression type 'false' in range expression", "Liquid syntax error (line 1): Invalid expression type 'false' in range expression",
"{{ (false..true) }}", "{{ (false..true) }}"
) )
assert_match_syntax_error( assert_match_syntax_error(
"Liquid syntax error (line 1): Invalid expression type '(1..2)' in range expression", "Liquid syntax error (line 1): Invalid expression type '(1..2)' in range expression",
"{{ ((1..2)..3) }}", "{{ ((1..2)..3) }}"
) )
end end
+1 -1
View File
@@ -17,7 +17,7 @@ class FilterKwargTest < Minitest::Test
with_global_filter(KwargFilter) do with_global_filter(KwargFilter) do
assert_equal( assert_equal(
"data-src='src' data-widths='100, 200'", "data-src='src' data-widths='100, 200'",
Template.parse("{{ 'img' | html_tag: data-src: 'src', data-widths: '100, 200' }}").render(nil, nil), Template.parse("{{ 'img' | html_tag: data-src: 'src', data-widths: '100, 200' }}").render(nil, nil)
) )
end end
end end
+14 -35
View File
@@ -68,34 +68,22 @@ class FiltersTest < Minitest::Test
def test_sort def test_sort
assert_template_result("1 2 3 4", "{{numbers | sort | join}}", { "numbers" => [2, 1, 4, 3] }) assert_template_result("1 2 3 4", "{{numbers | sort | join}}", { "numbers" => [2, 1, 4, 3] })
assert_template_result( assert_template_result("alphabetic as expected", "{{words | sort | join}}",
"alphabetic as expected", { "words" => ['expected', 'as', 'alphabetic'] })
"{{words | sort | join}}",
{ "words" => ['expected', 'as', 'alphabetic'] },
)
assert_template_result("3", "{{value | sort}}", { "value" => 3 }) assert_template_result("3", "{{value | sort}}", { "value" => 3 })
assert_template_result('are flower', "{{arrays | sort | join}}", { 'arrays' => ['flower', 'are'] }) assert_template_result('are flower', "{{arrays | sort | join}}", { 'arrays' => ['flower', 'are'] })
assert_template_result( assert_template_result("Expected case sensitive", "{{case_sensitive | sort | join}}",
"Expected case sensitive", { "case_sensitive" => ["sensitive", "Expected", "case"] })
"{{case_sensitive | sort | join}}",
{ "case_sensitive" => ["sensitive", "Expected", "case"] },
)
end end
def test_sort_natural def test_sort_natural
# Test strings # Test strings
assert_template_result( assert_template_result("Assert case Insensitive", "{{words | sort_natural | join}}",
"Assert case Insensitive", { "words" => ["case", "Assert", "Insensitive"] })
"{{words | sort_natural | join}}",
{ "words" => ["case", "Assert", "Insensitive"] },
)
# Test hashes # Test hashes
assert_template_result( assert_template_result("A b C", "{{hashes | sort_natural: 'a' | map: 'a' | join}}",
"A b C", { "hashes" => [{ "a" => "A" }, { "a" => "b" }, { "a" => "C" }] })
"{{hashes | sort_natural: 'a' | map: 'a' | join}}",
{ "hashes" => [{ "a" => "A" }, { "a" => "b" }, { "a" => "C" }] },
)
# Test objects # Test objects
@context['objects'] = [TestObject.new('A'), TestObject.new('b'), TestObject.new('C')] @context['objects'] = [TestObject.new('A'), TestObject.new('b'), TestObject.new('C')]
@@ -104,18 +92,12 @@ class FiltersTest < Minitest::Test
def test_compact def test_compact
# Test strings # Test strings
assert_template_result( assert_template_result("a b c", "{{words | compact | join}}",
"a b c", { "words" => ['a', nil, 'b', nil, 'c'] })
"{{words | compact | join}}",
{ "words" => ['a', nil, 'b', nil, 'c'] },
)
# Test hashes # Test hashes
assert_template_result( assert_template_result("A C", "{{hashes | compact: 'a' | map: 'a' | join}}",
"A C", { "hashes" => [{ "a" => "A" }, { "a" => nil }, { "a" => "C" }] })
"{{hashes | compact: 'a' | map: 'a' | join}}",
{ "hashes" => [{ "a" => "A" }, { "a" => nil }, { "a" => "C" }] },
)
# Test objects # Test objects
@context['objects'] = [TestObject.new('A'), TestObject.new(nil), TestObject.new('C')] @context['objects'] = [TestObject.new('A'), TestObject.new(nil), TestObject.new('C')]
@@ -127,11 +109,8 @@ class FiltersTest < Minitest::Test
end end
def test_strip_html_ignore_comments_with_html def test_strip_html_ignore_comments_with_html
assert_template_result( assert_template_result("bla blub", "{{ var | strip_html }}",
"bla blub", { "var" => "<!-- split and some <ul> tag --><b>bla blub</a>" })
"{{ var | strip_html }}",
{ "var" => "<!-- split and some <ul> tag --><b>bla blub</a>" },
)
end end
def test_capitalize def test_capitalize
+19 -157
View File
@@ -32,7 +32,7 @@ class TestDrop < Liquid::Drop
attr_reader :value attr_reader :value
def registers def registers
"{#{@value.inspect}=>#{@context.registers[@value].inspect}}" { @value => @context.registers[@value] }
end end
end end
@@ -109,10 +109,6 @@ class StandardFiltersTest < Minitest::Test
assert_raises(Liquid::ArgumentError) do assert_raises(Liquid::ArgumentError) do
@filters.slice('foobar', 0, "") @filters.slice('foobar', 0, "")
end end
assert_equal("", @filters.slice("foobar", 0, -(1 << 64)))
assert_equal("foobar", @filters.slice("foobar", 0, 1 << 63))
assert_equal("", @filters.slice("foobar", 1 << 63, 6))
assert_equal("", @filters.slice("foobar", -(1 << 63), 6))
end end
def test_slice_on_arrays def test_slice_on_arrays
@@ -127,10 +123,6 @@ class StandardFiltersTest < Minitest::Test
assert_equal(%w(r), @filters.slice(input, -1)) assert_equal(%w(r), @filters.slice(input, -1))
assert_equal(%w(), @filters.slice(input, 100, 10)) assert_equal(%w(), @filters.slice(input, 100, 10))
assert_equal(%w(), @filters.slice(input, -100, 10)) assert_equal(%w(), @filters.slice(input, -100, 10))
assert_equal([], @filters.slice(input, 0, -(1 << 64)))
assert_equal(input, @filters.slice(input, 0, 1 << 63))
assert_equal([], @filters.slice(input, 1 << 63, 6))
assert_equal([], @filters.slice(input, -(1 << 63), 6))
end end
def test_truncate def test_truncate
@@ -140,8 +132,6 @@ class StandardFiltersTest < Minitest::Test
assert_equal('1234567890', @filters.truncate('1234567890')) assert_equal('1234567890', @filters.truncate('1234567890'))
assert_equal("测试...", @filters.truncate("测试测试测试测试", 5)) assert_equal("测试...", @filters.truncate("测试测试测试测试", 5))
assert_equal('12341', @filters.truncate("1234567890", 5, 1)) assert_equal('12341', @filters.truncate("1234567890", 5, 1))
assert_equal("foobar", @filters.truncate("foobar", 1 << 63))
assert_equal("...", @filters.truncate("foobar", -(1 << 63)))
end end
def test_split def test_split
@@ -176,17 +166,7 @@ class StandardFiltersTest < Minitest::Test
end end
def test_base64_decode def test_base64_decode
decoded = @filters.base64_decode('b25lIHR3byB0aHJlZQ==') assert_equal('one two three', @filters.base64_decode('b25lIHR3byB0aHJlZQ=='))
assert_equal('one two three', decoded)
assert_equal(Encoding::UTF_8, decoded.encoding)
decoded = @filters.base64_decode('4pyF')
assert_equal('✅', decoded)
assert_equal(Encoding::UTF_8, decoded.encoding)
decoded = @filters.base64_decode("/w==")
assert_equal(Encoding::ASCII_8BIT, decoded.encoding)
assert_equal((+"\xFF").force_encoding(Encoding::ASCII_8BIT), decoded)
exception = assert_raises(Liquid::ArgumentError) do exception = assert_raises(Liquid::ArgumentError) do
@filters.base64_decode("invalidbase64") @filters.base64_decode("invalidbase64")
@@ -198,27 +178,16 @@ class StandardFiltersTest < Minitest::Test
def test_base64_url_safe_encode def test_base64_url_safe_encode
assert_equal( assert_equal(
'YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXogQUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVogMTIzNDU2Nzg5MCAhQCMkJV4mKigpLT1fKy8_Ljo7W117fVx8', 'YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXogQUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVogMTIzNDU2Nzg5MCAhQCMkJV4mKigpLT1fKy8_Ljo7W117fVx8',
@filters.base64_url_safe_encode('abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890 !@#$%^&*()-=_+/?.:;[]{}\|'), @filters.base64_url_safe_encode('abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890 !@#$%^&*()-=_+/?.:;[]{}\|')
) )
assert_equal('', @filters.base64_url_safe_encode(nil)) assert_equal('', @filters.base64_url_safe_encode(nil))
end end
def test_base64_url_safe_decode def test_base64_url_safe_decode
decoded = @filters.base64_url_safe_decode('YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXogQUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVogMTIzNDU2Nzg5MCAhQCMkJV4mKigpLT1fKy8_Ljo7W117fVx8')
assert_equal( assert_equal(
'abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890 !@#$%^&*()-=_+/?.:;[]{}\|', 'abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890 !@#$%^&*()-=_+/?.:;[]{}\|',
decoded, @filters.base64_url_safe_decode('YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXogQUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVogMTIzNDU2Nzg5MCAhQCMkJV4mKigpLT1fKy8_Ljo7W117fVx8')
) )
assert_equal(Encoding::UTF_8, decoded.encoding)
decoded = @filters.base64_url_safe_decode('4pyF')
assert_equal('✅', decoded)
assert_equal(Encoding::UTF_8, decoded.encoding)
decoded = @filters.base64_url_safe_decode("_w==")
assert_equal(Encoding::ASCII_8BIT, decoded.encoding)
assert_equal((+"\xFF").force_encoding(Encoding::ASCII_8BIT), decoded)
exception = assert_raises(Liquid::ArgumentError) do exception = assert_raises(Liquid::ArgumentError) do
@filters.base64_url_safe_decode("invalidbase64") @filters.base64_url_safe_decode("invalidbase64")
end end
@@ -251,15 +220,17 @@ class StandardFiltersTest < Minitest::Test
assert_equal('one two three', @filters.truncatewords('one two three')) assert_equal('one two three', @filters.truncatewords('one two three'))
assert_equal( assert_equal(
'Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221;...', '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), @filters.truncatewords('Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221; x 16&#8221; x 10.5&#8221; high) with cover.', 15)
) )
assert_equal("测试测试测试测试", @filters.truncatewords('测试测试测试测试', 5)) assert_equal("测试测试测试测试", @filters.truncatewords('测试测试测试测试', 5))
assert_equal('one two1', @filters.truncatewords("one two three", 2, 1)) assert_equal('one two1', @filters.truncatewords("one two three", 2, 1))
assert_equal('one two three...', @filters.truncatewords("one two\tthree\nfour", 3)) assert_equal('one two three...', @filters.truncatewords("one two\tthree\nfour", 3))
assert_equal('one two...', @filters.truncatewords("one two three four", 2)) assert_equal('one two...', @filters.truncatewords("one two three four", 2))
assert_equal('one...', @filters.truncatewords("one two three four", 0)) assert_equal('one...', @filters.truncatewords("one two three four", 0))
assert_equal('one two three four', @filters.truncatewords("one two three four", 1 << 31)) exception = assert_raises(Liquid::ArgumentError) do
assert_equal('one...', @filters.truncatewords("one two three four", -(1 << 32))) @filters.truncatewords("one two three four", 1 << 31)
end
assert_equal("Liquid error: integer #{1 << 31} too big for truncatewords", exception.message)
end end
def test_strip_html def test_strip_html
@@ -331,8 +302,8 @@ class StandardFiltersTest < Minitest::Test
{ "price" => "1", "handle" => "gamma" }, { "price" => "1", "handle" => "gamma" },
{ "price" => 2, "handle" => "epsilon" }, { "price" => 2, "handle" => "epsilon" },
{ "price" => "4", "handle" => "alpha" }, { "price" => "4", "handle" => "alpha" },
{ "handle" => "beta" },
{ "handle" => "delta" }, { "handle" => "delta" },
{ "handle" => "beta" },
] ]
assert_equal(expectation, @filters.sort_natural(input, "price")) assert_equal(expectation, @filters.sort_natural(input, "price"))
end end
@@ -454,11 +425,8 @@ class StandardFiltersTest < Minitest::Test
def test_map def test_map
assert_equal([1, 2, 3, 4], @filters.map([{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }], 'a')) assert_equal([1, 2, 3, 4], @filters.map([{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }], 'a'))
assert_template_result( assert_template_result('abc', "{{ ary | map:'foo' | map:'bar' }}",
'abc', { 'ary' => [{ 'foo' => { 'bar' => 'a' } }, { 'foo' => { 'bar' => 'b' } }, { 'foo' => { 'bar' => 'c' } }] })
"{{ ary | map:'foo' | map:'bar' }}",
{ 'ary' => [{ 'foo' => { 'bar' => 'a' } }, { 'foo' => { 'bar' => 'b' } }, { 'foo' => { 'bar' => 'c' } }] },
)
end end
def test_map_doesnt_call_arbitrary_stuff def test_map_doesnt_call_arbitrary_stuff
@@ -482,11 +450,8 @@ class StandardFiltersTest < Minitest::Test
end end
def test_map_on_hashes def test_map_on_hashes
assert_template_result( assert_template_result("4217", '{{ thing | map: "foo" | map: "bar" }}',
"4217", { "thing" => { "foo" => [{ "bar" => 42 }, { "bar" => 17 }] } })
'{{ thing | map: "foo" | map: "bar" }}',
{ "thing" => { "foo" => [{ "bar" => 42 }, { "bar" => 17 }] } },
)
end end
def test_legacy_map_on_hashes_with_dynamic_key def test_legacy_map_on_hashes_with_dynamic_key
@@ -504,8 +469,8 @@ class StandardFiltersTest < Minitest::Test
def test_map_over_proc def test_map_over_proc
drop = TestDrop.new(value: "testfoo") drop = TestDrop.new(value: "testfoo")
p = proc { drop } p = proc { drop }
output = Liquid::Template.parse('{{ procs | map: "value" }}').render!({ "procs" => [p] }) templ = '{{ procs | map: "value" }}'
assert_equal("testfoo", output) assert_template_result("testfoo", templ, { "procs" => [p] })
end end
def test_map_over_drops_returning_procs def test_map_over_drops_returning_procs
@@ -517,13 +482,12 @@ class StandardFiltersTest < Minitest::Test
"proc" => -> { "bar" }, "proc" => -> { "bar" },
}, },
] ]
output = Liquid::Template.parse('{{ drops | map: "proc" }}').render!({ "drops" => drops }) templ = '{{ drops | map: "proc" }}'
assert_equal("foobar", output) assert_template_result("foobar", templ, { "drops" => drops })
end end
def test_map_works_on_enumerables def test_map_works_on_enumerables
output = Liquid::Template.parse('{{ foo | map: "foo" }}').render!({ "foo" => TestEnumerable.new }) assert_template_result("123", '{{ foo | map: "foo" }}', { "foo" => TestEnumerable.new })
assert_equal("123", output)
end end
def test_map_returns_empty_on_2d_input_array def test_map_returns_empty_on_2d_input_array
@@ -949,108 +913,6 @@ class StandardFiltersTest < Minitest::Test
assert_equal([{ "foo" => true }, { "foo" => "for sure" }], @filters.where(input, "foo")) assert_equal([{ "foo" => true }, { "foo" => "for sure" }], @filters.where(input, "foo"))
end end
def test_sum_with_all_numbers
input = [1, 2]
assert_equal(3, @filters.sum(input))
assert_raises(Liquid::ArgumentError, "cannot select the property 'quantity'") do
@filters.sum(input, "quantity")
end
end
def test_sum_with_numeric_strings
input = [1, 2, "3", "4"]
assert_equal(10, @filters.sum(input))
assert_raises(Liquid::ArgumentError, "cannot select the property 'quantity'") do
@filters.sum(input, "quantity")
end
end
def test_sum_with_nested_arrays
input = [1, [2, [3, 4]]]
assert_equal(10, @filters.sum(input))
assert_raises(Liquid::ArgumentError, "cannot select the property 'quantity'") do
@filters.sum(input, "quantity")
end
end
def test_sum_with_indexable_map_values
input = [{ "quantity" => 1 }, { "quantity" => 2, "weight" => 3 }, { "weight" => 4 }]
assert_equal(0, @filters.sum(input))
assert_equal(3, @filters.sum(input, "quantity"))
assert_equal(7, @filters.sum(input, "weight"))
assert_equal(0, @filters.sum(input, "subtotal"))
end
def test_sum_with_indexable_non_map_values
input = [1, [2], "foo", { "quantity" => 3 }]
assert_equal(3, @filters.sum(input))
assert_raises(Liquid::ArgumentError, "cannot select the property 'quantity'") do
@filters.sum(input, "quantity")
end
end
def test_sum_with_unindexable_values
input = [1, true, nil, { "quantity" => 2 }]
assert_equal(1, @filters.sum(input))
assert_raises(Liquid::ArgumentError, "cannot select the property 'quantity'") do
@filters.sum(input, "quantity")
end
end
def test_sum_without_property_calls_to_liquid
t = TestThing.new
Liquid::Template.parse('{{ foo | sum }}').render("foo" => [t])
assert(t.foo > 0)
end
def test_sum_with_property_calls_to_liquid_on_property_values
t = TestThing.new
Liquid::Template.parse('{{ foo | sum: "quantity" }}').render("foo" => [{ "quantity" => t }])
assert(t.foo > 0)
end
def test_sum_of_floats
input = [0.1, 0.2, 0.3]
assert_equal(0.6, @filters.sum(input))
assert_template_result("0.6", "{{ input | sum }}", { "input" => input })
end
def test_sum_of_negative_floats
input = [0.1, 0.2, -0.3]
assert_equal(0.0, @filters.sum(input))
assert_template_result("0.0", "{{ input | sum }}", { "input" => input })
end
def test_sum_with_float_strings
input = [0.1, "0.2", "0.3"]
assert_equal(0.6, @filters.sum(input))
assert_template_result("0.6", "{{ input | sum }}", { "input" => input })
end
def test_sum_resulting_in_negative_float
input = [0.1, -0.2, -0.3]
assert_equal(-0.4, @filters.sum(input))
assert_template_result("-0.4", "{{ input | sum }}", { "input" => input })
end
def test_sum_with_floats_and_indexable_map_values
input = [{ "quantity" => 1 }, { "quantity" => 0.2, "weight" => -0.3 }, { "weight" => 0.4 }]
assert_equal(0.0, @filters.sum(input))
assert_equal(1.2, @filters.sum(input, "quantity"))
assert_equal(0.1, @filters.sum(input, "weight"))
assert_equal(0.0, @filters.sum(input, "subtotal"))
assert_template_result("0", "{{ input | sum }}", { "input" => input })
assert_template_result("1.2", "{{ input | sum: 'quantity' }}", { "input" => input })
assert_template_result("0.1", "{{ input | sum: 'weight' }}", { "input" => input })
assert_template_result("0", "{{ input | sum: 'subtotal' }}", { "input" => input })
end
private private
def with_timezone(tz) def with_timezone(tz)
+2 -2
View File
@@ -9,8 +9,8 @@ class BreakTagTest < Minitest::Test
# block # block
def test_break_with_no_block def test_break_with_no_block
assigns = { 'i' => 1 } assigns = { 'i' => 1 }
markup = 'before{% break %}after' markup = '{% break %}'
expected = 'before' expected = ''
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
end end
+40 -42
View File
@@ -73,11 +73,9 @@ HERE
def test_for_helpers def test_for_helpers
assigns = { 'array' => [1, 2, 3] } assigns = { 'array' => [1, 2, 3] }
assert_template_result( assert_template_result(' 1/3 2/3 3/3 ',
' 1/3 2/3 3/3 ',
'{%for item in array%} {{forloop.index}}/{{forloop.length}} {%endfor%}', '{%for item in array%} {{forloop.index}}/{{forloop.length}} {%endfor%}',
assigns, assigns)
)
assert_template_result(' 1 2 3 ', '{%for item in array%} {{forloop.index}} {%endfor%}', assigns) assert_template_result(' 1 2 3 ', '{%for item in array%} {{forloop.index}} {%endfor%}', assigns)
assert_template_result(' 0 1 2 ', '{%for item in array%} {{forloop.index0}} {%endfor%}', assigns) assert_template_result(' 0 1 2 ', '{%for item in array%} {{forloop.index0}} {%endfor%}', assigns)
assert_template_result(' 2 1 0 ', '{%for item in array%} {{forloop.rindex0}} {%endfor%}', assigns) assert_template_result(' 2 1 0 ', '{%for item in array%} {{forloop.rindex0}} {%endfor%}', assigns)
@@ -88,11 +86,9 @@ HERE
def test_for_and_if def test_for_and_if
assigns = { 'array' => [1, 2, 3] } assigns = { 'array' => [1, 2, 3] }
assert_template_result( assert_template_result('+--',
'+--',
'{%for item in array%}{% if forloop.first %}+{% else %}-{% endif %}{%endfor%}', '{%for item in array%}{% if forloop.first %}+{% else %}-{% endif %}{%endfor%}',
assigns, assigns)
)
end end
def test_for_else def test_for_else
@@ -107,7 +103,6 @@ HERE
assert_template_result('1234', '{%for i in array limit:4 %}{{ i }}{%endfor%}', assigns) assert_template_result('1234', '{%for i in array limit:4 %}{{ i }}{%endfor%}', assigns)
assert_template_result('3456', '{%for i in array limit:4 offset:2 %}{{ i }}{%endfor%}', assigns) assert_template_result('3456', '{%for i in array limit:4 offset:2 %}{{ i }}{%endfor%}', assigns)
assert_template_result('3456', '{%for i in array limit: 4 offset: 2 %}{{ i }}{%endfor%}', assigns) assert_template_result('3456', '{%for i in array limit: 4 offset: 2 %}{{ i }}{%endfor%}', assigns)
assert_template_result('3456', '{%for i in array, limit: 4, offset: 2 %}{{ i }}{%endfor%}', assigns)
end end
def test_limiting_with_invalid_limit def test_limiting_with_invalid_limit
@@ -268,19 +263,6 @@ HERE
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
end end
def test_for_with_break_after_nested_loop
source = <<~LIQUID.chomp
{% for i in (1..2) -%}
{% for j in (1..2) -%}
{{ i }}-{{ j }},
{%- endfor -%}
{% break -%}
{% endfor -%}
after
LIQUID
assert_template_result("1-1,1-2,after", source)
end
def test_for_with_continue def test_for_with_continue
assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5] } } assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5] } }
@@ -329,20 +311,15 @@ HERE
# ruby 1.9.3 no longer supports .each on String though we mimic # ruby 1.9.3 no longer supports .each on String though we mimic
# the functionality for backwards compatibility # the functionality for backwards compatibility
assert_template_result( assert_template_result('test string',
'test string',
'{%for val in string%}{{val}}{%endfor%}', '{%for val in string%}{{val}}{%endfor%}',
{ 'string' => "test string" }, { 'string' => "test string" })
)
assert_template_result( assert_template_result('test string',
'test string',
'{%for val in string limit:1%}{{val}}{%endfor%}', '{%for val in string limit:1%}{{val}}{%endfor%}',
{ 'string' => "test string" }, { 'string' => "test string" })
)
assert_template_result( assert_template_result('val-string-1-1-0-1-0-true-true-test string',
'val-string-1-1-0-1-0-true-true-test string',
'{%for val in string%}' \ '{%for val in string%}' \
'{{forloop.name}}-' \ '{{forloop.name}}-' \
'{{forloop.index}}-' \ '{{forloop.index}}-' \
@@ -353,28 +330,23 @@ HERE
'{{forloop.first}}-' \ '{{forloop.first}}-' \
'{{forloop.last}}-' \ '{{forloop.last}}-' \
'{{val}}{%endfor%}', '{{val}}{%endfor%}',
{ 'string' => "test string" }, { 'string' => "test string" })
)
end end
def test_for_parentloop_references_parent_loop def test_for_parentloop_references_parent_loop
assert_template_result( assert_template_result('1.1 1.2 1.3 2.1 2.2 2.3 ',
'1.1 1.2 1.3 2.1 2.2 2.3 ',
'{% for inner in outer %}{% for k in inner %}' \ '{% for inner in outer %}{% for k in inner %}' \
'{{ forloop.parentloop.index }}.{{ forloop.index }} ' \ '{{ forloop.parentloop.index }}.{{ forloop.index }} ' \
'{% endfor %}{% endfor %}', '{% endfor %}{% endfor %}',
{ 'outer' => [[1, 1, 1], [1, 1, 1]] }, { 'outer' => [[1, 1, 1], [1, 1, 1]] })
)
end end
def test_for_parentloop_nil_when_not_present def test_for_parentloop_nil_when_not_present
assert_template_result( assert_template_result('.1 .2 ',
'.1 .2 ',
'{% for inner in outer %}' \ '{% for inner in outer %}' \
'{{ forloop.parentloop.index }}.{{ forloop.index }} ' \ '{{ forloop.parentloop.index }}.{{ forloop.index }} ' \
'{% endfor %}', '{% endfor %}',
{ 'outer' => [[1, 1, 1], [1, 1, 1]] }, { 'outer' => [[1, 1, 1], [1, 1, 1]] })
)
end end
def test_inner_for_over_empty_input def test_inner_for_over_empty_input
@@ -465,4 +437,30 @@ HERE
assert(context.registers[:for_stack].empty?) assert(context.registers[:for_stack].empty?)
end end
def test_instrument_for_offset_continue
assert_usage_increment('for_offset_continue') do
Template.parse('{% for item in items offset:continue %}{{item}}{% endfor %}')
end
assert_usage_increment('for_offset_continue', times: 0) do
Template.parse('{% for item in items offset:2 %}{{item}}{% endfor %}')
end
end
def test_instrument_forloop_drop_name
assigns = { 'items' => [1, 2, 3, 4, 5] }
assert_usage_increment('forloop_drop_name', times: 5) do
Template.parse('{% for item in items %}{{forloop.name}}{% endfor %}').render!(assigns)
end
assert_usage_increment('forloop_drop_name', times: 0) do
Template.parse('{% for item in items %}{{forloop.index}}{% endfor %}').render!(assigns)
end
assert_usage_increment('forloop_drop_name', times: 0) do
Template.parse('{% for item in items %}{{item}}{% endfor %}').render!(assigns)
end
end
end end
+9 -17
View File
@@ -7,10 +7,8 @@ class IfElseTagTest < Minitest::Test
def test_if def test_if
assert_template_result(' ', ' {% if false %} this text should not go into the output {% endif %} ') assert_template_result(' ', ' {% if false %} this text should not go into the output {% endif %} ')
assert_template_result( assert_template_result(' this text should go into the output ',
' this text should go into the output ', ' {% if true %} this text should go into the output {% endif %} ')
' {% if true %} this text should go into the output {% endif %} ',
)
assert_template_result(' you rock ?', '{% if false %} you suck {% endif %} {% if true %} you rock {% endif %}?') assert_template_result(' you rock ?', '{% if false %} you suck {% endif %} {% if true %} you rock {% endif %}?')
end end
@@ -53,22 +51,18 @@ class IfElseTagTest < Minitest::Test
def test_comparison_of_expressions_starting_with_and_or_or def test_comparison_of_expressions_starting_with_and_or_or
assigns = { 'order' => { 'items_count' => 0 }, 'android' => { 'name' => 'Roy' } } assigns = { 'order' => { 'items_count' => 0 }, 'android' => { 'name' => 'Roy' } }
assert_template_result( assert_template_result("YES",
"YES",
"{% if android.name == 'Roy' %}YES{% endif %}", "{% if android.name == 'Roy' %}YES{% endif %}",
assigns, assigns)
) assert_template_result("YES",
assert_template_result(
"YES",
"{% if order.items_count == 0 %}YES{% endif %}", "{% if order.items_count == 0 %}YES{% endif %}",
assigns, assigns)
)
end end
def test_if_and def test_if_and
assert_template_result(' YES ', '{% if true and true %} YES {% endif %}') assert_template_result(' YES ', '{% if true and true %} YES {% endif %}')
assert_template_result('', '{% if false and true %} YES {% endif %}') assert_template_result('', '{% if false and true %} YES {% endif %}')
assert_template_result('', '{% if true and false %} YES {% endif %}') assert_template_result('', '{% if false and true %} YES {% endif %}')
end end
def test_hash_miss_generates_false def test_hash_miss_generates_false
@@ -161,10 +155,8 @@ class IfElseTagTest < Minitest::Test
original_op = Condition.operators['contains'] original_op = Condition.operators['contains']
Condition.operators['contains'] = :[] Condition.operators['contains'] = :[]
assert_template_result( assert_template_result('yes',
'yes', %({% if 'gnomeslab-and-or-liquid' contains 'gnomeslab-and-or-liquid' %}yes{% endif %}))
%({% if 'gnomeslab-and-or-liquid' contains 'gnomeslab-and-or-liquid' %}yes{% endif %}),
)
ensure ensure
Condition.operators['contains'] = original_op Condition.operators['contains'] = original_op
end end
+94 -192
View File
@@ -3,13 +3,44 @@
require 'test_helper' require 'test_helper'
class TestFileSystem class TestFileSystem
PARTIALS = {
"nested_template" => "{% include 'header' %} {% include 'body' %} {% include 'footer' %}",
"body" => "body {% include 'body_detail' %}",
}
def read_template_file(template_path) def read_template_file(template_path)
PARTIALS[template_path] || template_path case template_path
when "product"
"Product: {{ product.title }} "
when "product_alias"
"Product: {{ product.title }} "
when "locale_variables"
"Locale: {{echo1}} {{echo2}}"
when "variant"
"Variant: {{ variant.title }}"
when "nested_template"
"{% include 'header' %} {% include 'body' %} {% include 'footer' %}"
when "body"
"body {% include 'body_detail' %}"
when "nested_product_template"
"Product: {{ nested_product_template.title }} {%include 'details'%} "
when "recursively_nested_template"
"-{% include 'recursively_nested_template' %}"
when "pick_a_source"
"from TestFileSystem"
when 'assignments'
"{% assign foo = 'bar' %}"
when 'break'
"{% break %}"
else
template_path
end
end end
end end
@@ -49,122 +80,75 @@ end
class IncludeTagTest < Minitest::Test class IncludeTagTest < Minitest::Test
include Liquid include Liquid
def setup
Liquid::Template.file_system = TestFileSystem.new
end
def test_include_tag_looks_for_file_system_in_registers_first def test_include_tag_looks_for_file_system_in_registers_first
assert_equal( assert_equal('from OtherFileSystem',
'from OtherFileSystem', Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: OtherFileSystem.new }))
Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: OtherFileSystem.new }),
)
end end
def test_include_tag_with def test_include_tag_with
assert_template_result( assert_template_result("Product: Draft 151cm ",
"Product: Draft 151cm ",
"{% include 'product' with products[0] %}", "{% include 'product' with products[0] %}",
{ "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] }, { "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] })
partials: { "product" => "Product: {{ product.title }} " },
)
end end
def test_include_tag_with_alias def test_include_tag_with_alias
assert_template_result( assert_template_result("Product: Draft 151cm ",
"Product: Draft 151cm ",
"{% include 'product_alias' with products[0] as product %}", "{% include 'product_alias' with products[0] as product %}",
{ "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] }, { "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] })
partials: { "product_alias" => "Product: {{ product.title }} " },
)
end end
def test_include_tag_for_alias def test_include_tag_for_alias
assert_template_result( assert_template_result("Product: Draft 151cm Product: Element 155cm ",
"Product: Draft 151cm Product: Element 155cm ",
"{% include 'product_alias' for products as product %}", "{% include 'product_alias' for products as product %}",
{ "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] }, { "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] })
partials: { "product_alias" => "Product: {{ product.title }} " },
)
end end
def test_include_tag_with_default_name def test_include_tag_with_default_name
assert_template_result( assert_template_result("Product: Draft 151cm ",
"Product: Draft 151cm ", "{% include 'product' %}", { "product" => { 'title' => 'Draft 151cm' } })
"{% include 'product' %}",
{ "product" => { 'title' => 'Draft 151cm' } },
partials: { "product" => "Product: {{ product.title }} " },
)
end end
def test_include_tag_for def test_include_tag_for
assert_template_result( assert_template_result("Product: Draft 151cm Product: Element 155cm ",
"Product: Draft 151cm Product: Element 155cm ",
"{% include 'product' for products %}", "{% include 'product' for products %}",
{ "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] }, { "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] })
partials: { "product" => "Product: {{ product.title }} " },
)
end end
def test_include_tag_with_local_variables def test_include_tag_with_local_variables
assert_template_result( assert_template_result("Locale: test123 ", "{% include 'locale_variables' echo1: 'test123' %}")
"Locale: test123 ",
"{% include 'locale_variables' echo1: 'test123' %}",
partials: { "locale_variables" => "Locale: {{echo1}} {{echo2}}" },
)
end end
def test_include_tag_with_multiple_local_variables def test_include_tag_with_multiple_local_variables
assert_template_result( assert_template_result("Locale: test123 test321",
"Locale: test123 test321", "{% include 'locale_variables' echo1: 'test123', echo2: 'test321' %}")
"{% include 'locale_variables' echo1: 'test123', echo2: 'test321' %}",
partials: { "locale_variables" => "Locale: {{echo1}} {{echo2}}" },
)
end end
def test_include_tag_with_multiple_local_variables_from_context def test_include_tag_with_multiple_local_variables_from_context
assert_template_result( assert_template_result("Locale: test123 test321",
"Locale: test123 test321",
"{% include 'locale_variables' echo1: echo1, echo2: more_echos.echo2 %}", "{% include 'locale_variables' echo1: echo1, echo2: more_echos.echo2 %}",
{ 'echo1' => 'test123', 'more_echos' => { "echo2" => 'test321' } }, { 'echo1' => 'test123', 'more_echos' => { "echo2" => 'test321' } })
partials: { "locale_variables" => "Locale: {{echo1}} {{echo2}}" },
)
end end
def test_included_templates_assigns_variables def test_included_templates_assigns_variables
assert_template_result( assert_template_result("bar", "{% include 'assignments' %}{{ foo }}")
"bar",
"{% include 'assignments' %}{{ foo }}",
partials: { 'assignments' => "{% assign foo = 'bar' %}" },
)
end end
def test_nested_include_tag def test_nested_include_tag
partials = { "body" => "body {% include 'body_detail' %}", "body_detail" => "body_detail" } assert_template_result("body body_detail", "{% include 'body' %}")
assert_template_result("body body_detail", "{% include 'body' %}", partials: partials)
partials = partials.merge({ assert_template_result("header body body_detail footer", "{% include 'nested_template' %}")
"nested_template" => "{% include 'header' %} {% include 'body' %} {% include 'footer' %}",
"header" => "header",
"footer" => "footer",
})
assert_template_result("header body body_detail footer", "{% include 'nested_template' %}", partials: partials)
end end
def test_nested_include_with_variable def test_nested_include_with_variable
partials = { assert_template_result("Product: Draft 151cm details ",
"nested_product_template" => "Product: {{ nested_product_template.title }} {%include 'details'%} ", "{% include 'nested_product_template' with product %}", { "product" => { "title" => 'Draft 151cm' } })
"details" => "details",
}
assert_template_result( assert_template_result("Product: Draft 151cm details Product: Element 155cm details ",
"Product: Draft 151cm details ", "{% include 'nested_product_template' for products %}", { "products" => [{ "title" => 'Draft 151cm' }, { "title" => 'Element 155cm' }] })
"{% include 'nested_product_template' with product %}",
{ "product" => { "title" => 'Draft 151cm' } },
partials: partials,
)
assert_template_result(
"Product: Draft 151cm details Product: Element 155cm details ",
"{% include 'nested_product_template' for products %}",
{ "products" => [{ "title" => 'Draft 151cm' }, { "title" => 'Element 155cm' }] },
partials: partials,
)
end end
def test_recursively_included_template_does_not_produce_endless_loop def test_recursively_included_template_does_not_produce_endless_loop
@@ -182,69 +166,41 @@ class IncludeTagTest < Minitest::Test
end end
def test_dynamically_choosen_template def test_dynamically_choosen_template
assert_template_result( assert_template_result("Test123", "{% include template %}", { "template" => 'Test123' })
"Test123", assert_template_result("Test321", "{% include template %}", { "template" => 'Test321' })
"{% include template %}",
{ "template" => 'Test123' },
partials: { "Test123" => "Test123" },
)
assert_template_result( assert_template_result("Product: Draft 151cm ", "{% include template for product %}",
"Test321", { "template" => 'product', 'product' => { 'title' => 'Draft 151cm' } })
"{% include template %}",
{ "template" => 'Test321' },
partials: { "Test321" => "Test321" },
)
assert_template_result(
"Product: Draft 151cm ",
"{% include template for product %}",
{ "template" => 'product', 'product' => { 'title' => 'Draft 151cm' } },
partials: { "product" => "Product: {{ product.title }} " },
)
end end
def test_include_tag_caches_second_read_of_same_partial def test_include_tag_caches_second_read_of_same_partial
file_system = CountingFileSystem.new file_system = CountingFileSystem.new
environment = Liquid::Environment.build(file_system: file_system) assert_equal('from CountingFileSystemfrom CountingFileSystem',
assert_equal( Template.parse("{% include 'pick_a_source' %}{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system }))
'from CountingFileSystemfrom CountingFileSystem',
Template.parse("{% include 'pick_a_source' %}{% include 'pick_a_source' %}", environment: environment).render!({}, registers: { file_system: file_system }),
)
assert_equal(1, file_system.count) assert_equal(1, file_system.count)
end end
def test_include_tag_doesnt_cache_partials_across_renders def test_include_tag_doesnt_cache_partials_across_renders
file_system = CountingFileSystem.new file_system = CountingFileSystem.new
assert_equal( assert_equal('from CountingFileSystem',
'from CountingFileSystem', Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system }))
Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system }),
)
assert_equal(1, file_system.count) assert_equal(1, file_system.count)
assert_equal( assert_equal('from CountingFileSystem',
'from CountingFileSystem', Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system }))
Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system }),
)
assert_equal(2, file_system.count) assert_equal(2, file_system.count)
end end
def test_include_tag_within_if_statement def test_include_tag_within_if_statement
assert_template_result( assert_template_result("foo_if_true", "{% if true %}{% include 'foo_if_true' %}{% endif %}")
"foo_if_true",
"{% if true %}{% include 'foo_if_true' %}{% endif %}",
partials: { "foo_if_true" => "foo_if_true" },
)
end end
def test_custom_include_tag def test_custom_include_tag
original_tag = Liquid::Template.tags['include'] original_tag = Liquid::Template.tags['include']
Liquid::Template.tags['include'] = CustomInclude Liquid::Template.tags['include'] = CustomInclude
begin begin
assert_equal( assert_equal("custom_foo",
"custom_foo", Template.parse("{% include 'custom_foo' %}").render!)
Template.parse("{% include 'custom_foo' %}").render!,
)
ensure ensure
Liquid::Template.tags['include'] = original_tag Liquid::Template.tags['include'] = original_tag
end end
@@ -254,10 +210,8 @@ class IncludeTagTest < Minitest::Test
original_tag = Liquid::Template.tags['include'] original_tag = Liquid::Template.tags['include']
Liquid::Template.tags['include'] = CustomInclude Liquid::Template.tags['include'] = CustomInclude
begin begin
assert_equal( assert_equal("custom_foo_if_true",
"custom_foo_if_true", Template.parse("{% if true %}{% include 'custom_foo_if_true' %}{% endif %}").render!)
Template.parse("{% if true %}{% include 'custom_foo_if_true' %}{% endif %}").render!,
)
ensure ensure
Liquid::Template.tags['include'] = original_tag Liquid::Template.tags['include'] = original_tag
end end
@@ -272,7 +226,6 @@ class IncludeTagTest < Minitest::Test
end end
def test_passing_options_to_included_templates def test_passing_options_to_included_templates
Liquid::Template.file_system = TestFileSystem.new
assert_raises(Liquid::SyntaxError) do assert_raises(Liquid::SyntaxError) do
Template.parse("{% include template %}", error_mode: :strict).render!("template" => '{{ "X" || downcase }}') Template.parse("{% include template %}", error_mode: :strict).render!("template" => '{{ "X" || downcase }}')
end end
@@ -288,53 +241,27 @@ class IncludeTagTest < Minitest::Test
end end
def test_render_raise_argument_error_when_template_is_undefined def test_render_raise_argument_error_when_template_is_undefined
assert_template_result( assert_raises(Liquid::ArgumentError) do
"Liquid error (line 1): Argument error in tag 'include' - Illegal template name", template = Liquid::Template.parse('{% include undefined_variable %}')
"{% include undefined_variable %}", template.render!
render_errors: true, end
) assert_raises(Liquid::ArgumentError) do
template = Liquid::Template.parse('{% include nil %}')
assert_template_result( template.render!
"Liquid error (line 1): Argument error in tag 'include' - Illegal template name", end
"{% include nil %}",
render_errors: true,
)
end
def test_render_raise_argument_error_when_template_is_not_a_string
assert_template_result(
"Liquid error (line 1): Argument error in tag 'include' - Illegal template name",
"{% include 123 %}",
render_errors: true,
)
end end
def test_including_via_variable_value def test_including_via_variable_value
assert_template_result( assert_template_result("from TestFileSystem", "{% assign page = 'pick_a_source' %}{% include page %}")
"from TestFileSystem",
"{% assign page = 'pick_a_source' %}{% include page %}",
partials: { "pick_a_source" => "from TestFileSystem" },
)
partials = { "product" => "Product: {{ product.title }} " } assert_template_result("Product: Draft 151cm ", "{% assign page = 'product' %}{% include page %}",
{ "product" => { 'title' => 'Draft 151cm' } })
assert_template_result( assert_template_result("Product: Draft 151cm ", "{% assign page = 'product' %}{% include page for foo %}",
"Product: Draft 151cm ", { "foo" => { 'title' => 'Draft 151cm' } })
"{% assign page = 'product' %}{% include page %}",
{ "product" => { 'title' => 'Draft 151cm' } },
partials: partials,
)
assert_template_result(
"Product: Draft 151cm ",
"{% assign page = 'product' %}{% include page for foo %}",
{ "foo" => { 'title' => 'Draft 151cm' } },
partials: partials,
)
end end
def test_including_with_strict_variables def test_including_with_strict_variables
Liquid::Template.file_system = StubFileSystem.new({ "simple" => "simple" })
template = Liquid::Template.parse("{% include 'simple' %}", error_mode: :warn) template = Liquid::Template.parse("{% include 'simple' %}", error_mode: :warn)
template.render(nil, strict_variables: true) template.render(nil, strict_variables: true)
@@ -343,31 +270,6 @@ class IncludeTagTest < Minitest::Test
def test_break_through_include def test_break_through_include
assert_template_result("1", "{% for i in (1..3) %}{{ i }}{% break %}{{ i }}{% endfor %}") assert_template_result("1", "{% for i in (1..3) %}{{ i }}{% break %}{{ i }}{% endfor %}")
assert_template_result( assert_template_result("1", "{% for i in (1..3) %}{{ i }}{% include 'break' %}{{ i }}{% endfor %}")
"1",
"{% for i in (1..3) %}{{ i }}{% include 'break' %}{{ i }}{% endfor %}",
partials: { 'break' => "{% break %}" },
)
end
def test_render_tag_renders_error_with_template_name
assert_template_result(
'Liquid error (foo line 1): standard error',
"{% include 'foo' with errors %}",
{ 'errors' => ErrorDrop.new },
partials: { 'foo' => '{{ foo.standard_error }}' },
render_errors: true,
)
end
def test_render_tag_renders_error_with_template_name_from_template_factory
assert_template_result(
'Liquid error (some/path/foo line 1): standard error',
"{% include 'foo' with errors %}",
{ 'errors' => ErrorDrop.new },
partials: { 'foo' => '{{ foo.standard_error }}' },
template_factory: StubTemplateFactory.new,
render_errors: true,
)
end end
end # IncludeTagTest end # IncludeTagTest
+8 -13
View File
@@ -6,25 +6,20 @@ class IncrementTagTest < Minitest::Test
include Liquid include Liquid
def test_inc def test_inc
assert_template_result('0 1', '{%increment port %} {{ port }}') assert_template_result('0', '{%increment port %}', {})
assert_template_result(' 0 1 2', '{{port}} {%increment port %} {%increment port%} {{port}}') assert_template_result('0 1', '{%increment port %} {%increment port%}', {})
assert_template_result( assert_template_result('0 0 1 2 1',
'0 0 1 2 1',
'{%increment port %} {%increment starboard%} ' \ '{%increment port %} {%increment starboard%} ' \
'{%increment port %} {%increment port%} ' \ '{%increment port %} {%increment port%} ' \
'{%increment starboard %}', '{%increment starboard %}', {})
)
end end
def test_dec def test_dec
assert_template_result('-1 -1', '{%decrement port %} {{ port }}', { 'port' => 10 }) assert_template_result('9', '{%decrement port %}', { 'port' => 10 })
assert_template_result(' -1 -2 -2', '{{port}} {%decrement port %} {%decrement port%} {{port}}') assert_template_result('-1 -2', '{%decrement port %} {%decrement port%}', {})
assert_template_result( assert_template_result('1 5 2 2 5',
'0 1 2 0 3 1 1 3',
'{%increment starboard %} {%increment starboard%} {%increment starboard%} ' \
'{%increment port %} {%increment starboard%} ' \ '{%increment port %} {%increment starboard%} ' \
'{%increment port %} {%decrement port%} ' \ '{%increment port %} {%decrement port%} ' \
'{%decrement starboard %}', '{%decrement starboard %}', { 'port' => 1, 'starboard' => 5 })
)
end end
end end
-33
View File
@@ -113,37 +113,4 @@ class LiquidTagTest < Minitest::Test
{% raw %}{% liquid echo 'test' %}{% endraw %} {% raw %}{% liquid echo 'test' %}{% endraw %}
LIQUID LIQUID
end end
def test_nested_liquid_tags
assert_template_result('good', <<~LIQUID)
{%- liquid
liquid
if true
echo "good"
endif
-%}
LIQUID
end
def test_nested_liquid_tags_on_same_line
assert_template_result('good', <<~LIQUID)
{%- liquid liquid liquid echo "good" -%}
LIQUID
end
def test_nested_liquid_liquid_is_not_skipped_if_used_in_non_tag_position
assert_template_result('liquid', <<~LIQUID, { 'liquid' => 'liquid' })
{%- liquid liquid liquid echo liquid -%}
LIQUID
end
def test_next_liquid_with_unclosed_if_tag
assert_match_syntax_error("Liquid syntax error (line 2): 'if' tag was never closed", <<~LIQUID)
{%- liquid
liquid if true
echo "good"
endif
-%}
LIQUID
end
end end
+3 -7
View File
@@ -6,16 +6,12 @@ class RawTagTest < Minitest::Test
include Liquid include Liquid
def test_tag_in_raw def test_tag_in_raw
assert_template_result( assert_template_result('{% comment %} test {% endcomment %}',
'{% comment %} test {% endcomment %}', '{% raw %}{% comment %} test {% endcomment %}{% endraw %}')
'{% raw %}{% comment %} test {% endcomment %}{% endraw %}',
)
end end
def test_output_in_raw def test_output_in_raw
assert_template_result('>{{ test }}<', '> {%- raw -%}{{ test }}{%- endraw -%} <') assert_template_result('{{ test }}', '{% raw %}{{ test }}{% endraw %}')
assert_template_result("> inner <", "> {%- raw -%} inner {%- endraw %} <")
assert_template_result("> inner <", "> {%- raw -%} inner {%- endraw -%} <")
end end
def test_open_tag_in_raw def test_open_tag_in_raw
+94 -161
View File
@@ -6,79 +6,53 @@ class RenderTagTest < Minitest::Test
include Liquid include Liquid
def test_render_with_no_arguments def test_render_with_no_arguments
assert_template_result( Liquid::Template.file_system = StubFileSystem.new('source' => 'rendered content')
'rendered content', assert_template_result('rendered content', '{% render "source" %}')
'{% render "source" %}',
partials: { 'source' => 'rendered content' },
)
end end
def test_render_tag_looks_for_file_system_in_registers_first def test_render_tag_looks_for_file_system_in_registers_first
assert_template_result( file_system = StubFileSystem.new('pick_a_source' => 'from register file system')
'from register file system', assert_equal('from register file system',
'{% render "pick_a_source" %}', Template.parse('{% render "pick_a_source" %}').render!({}, registers: { file_system: file_system }))
partials: { 'pick_a_source' => 'from register file system' },
)
end end
def test_render_passes_named_arguments_into_inner_scope def test_render_passes_named_arguments_into_inner_scope
assert_template_result( Liquid::Template.file_system = StubFileSystem.new('product' => '{{ inner_product.title }}')
'My Product', assert_template_result('My Product', '{% render "product", inner_product: outer_product %}',
'{% render "product", inner_product: outer_product %}', { 'outer_product' => { 'title' => 'My Product' } })
{ 'outer_product' => { 'title' => 'My Product' } },
partials: { 'product' => '{{ inner_product.title }}' },
)
end end
def test_render_accepts_literals_as_arguments def test_render_accepts_literals_as_arguments
assert_template_result( Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ price }}')
'123', assert_template_result('123', '{% render "snippet", price: 123 %}')
'{% render "snippet", price: 123 %}',
partials: { 'snippet' => '{{ price }}' },
)
end end
def test_render_accepts_multiple_named_arguments def test_render_accepts_multiple_named_arguments
assert_template_result( Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ one }} {{ two }}')
'1 2', assert_template_result('1 2', '{% render "snippet", one: 1, two: 2 %}')
'{% render "snippet", one: 1, two: 2 %}',
partials: { 'snippet' => '{{ one }} {{ two }}' },
)
end end
def test_render_does_not_inherit_parent_scope_variables def test_render_does_not_inherit_parent_scope_variables
assert_template_result( Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ outer_variable }}')
'', assert_template_result('', '{% assign outer_variable = "should not be visible" %}{% render "snippet" %}')
'{% assign outer_variable = "should not be visible" %}{% render "snippet" %}',
partials: { 'snippet' => '{{ outer_variable }}' },
)
end end
def test_render_does_not_inherit_variable_with_same_name_as_snippet def test_render_does_not_inherit_variable_with_same_name_as_snippet
assert_template_result( Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ snippet }}')
'', assert_template_result('', "{% assign snippet = 'should not be visible' %}{% render 'snippet' %}")
"{% assign snippet = 'should not be visible' %}{% render 'snippet' %}",
partials: { 'snippet' => '{{ snippet }}' },
)
end end
def test_render_does_not_mutate_parent_scope def test_render_does_not_mutate_parent_scope
assert_template_result( Liquid::Template.file_system = StubFileSystem.new('snippet' => '{% assign inner = 1 %}')
'', assert_template_result('', "{% render 'snippet' %}{{ inner }}")
"{% render 'snippet' %}{{ inner }}",
partials: { 'snippet' => '{% assign inner = 1 %}' },
)
end end
def test_nested_render_tag def test_nested_render_tag
assert_template_result( Liquid::Template.file_system = StubFileSystem.new(
'one two', 'one' => "one {% render 'two' %}",
"{% render 'one' %}", 'two' => 'two'
partials: {
'one' => "one {% render 'two' %}",
'two' => 'two',
},
) )
assert_template_result('one two', "{% render 'one' %}")
end end
def test_recursively_rendered_template_does_not_produce_endless_loop def test_recursively_rendered_template_does_not_produce_endless_loop
@@ -99,75 +73,64 @@ class RenderTagTest < Minitest::Test
end end
def test_dynamically_choosen_templates_are_not_allowed def test_dynamically_choosen_templates_are_not_allowed
assert_syntax_error("{% assign name = 'snippet' %}{% render name %}") Liquid::Template.file_system = StubFileSystem.new('snippet' => 'should not be rendered')
assert_raises(Liquid::SyntaxError) do
Liquid::Template.parse("{% assign name = 'snippet' %}{% render name %}")
end
end end
def test_include_tag_caches_second_read_of_same_partial def test_include_tag_caches_second_read_of_same_partial
file_system = StubFileSystem.new('snippet' => 'echo') file_system = StubFileSystem.new('snippet' => 'echo')
assert_equal( assert_equal('echoecho',
'echoecho',
Template.parse('{% render "snippet" %}{% render "snippet" %}') Template.parse('{% render "snippet" %}{% render "snippet" %}')
.render!({}, registers: { file_system: file_system }), .render!({}, registers: { file_system: file_system }))
)
assert_equal(1, file_system.file_read_count) assert_equal(1, file_system.file_read_count)
end end
def test_render_tag_doesnt_cache_partials_across_renders def test_render_tag_doesnt_cache_partials_across_renders
file_system = StubFileSystem.new('snippet' => 'my message') file_system = StubFileSystem.new('snippet' => 'my message')
assert_equal( assert_equal('my message',
'my message', Template.parse('{% include "snippet" %}').render!({}, registers: { file_system: file_system }))
Template.parse('{% include "snippet" %}').render!({}, registers: { file_system: file_system }),
)
assert_equal(1, file_system.file_read_count) assert_equal(1, file_system.file_read_count)
assert_equal( assert_equal('my message',
'my message', Template.parse('{% include "snippet" %}').render!({}, registers: { file_system: file_system }))
Template.parse('{% include "snippet" %}').render!({}, registers: { file_system: file_system }),
)
assert_equal(2, file_system.file_read_count) assert_equal(2, file_system.file_read_count)
end end
def test_render_tag_within_if_statement def test_render_tag_within_if_statement
assert_template_result( Liquid::Template.file_system = StubFileSystem.new('snippet' => 'my message')
'my message', assert_template_result('my message', '{% if true %}{% render "snippet" %}{% endif %}')
'{% if true %}{% render "snippet" %}{% endif %}',
partials: { 'snippet' => 'my message' },
)
end end
def test_break_through_render def test_break_through_render
options = { partials: { 'break' => '{% break %}' } } Liquid::Template.file_system = StubFileSystem.new('break' => '{% break %}')
assert_template_result('1', '{% for i in (1..3) %}{{ i }}{% break %}{{ i }}{% endfor %}', **options) assert_template_result('1', '{% for i in (1..3) %}{{ i }}{% break %}{{ i }}{% endfor %}')
assert_template_result('112233', '{% for i in (1..3) %}{{ i }}{% render "break" %}{{ i }}{% endfor %}', **options) assert_template_result('112233', '{% for i in (1..3) %}{{ i }}{% render "break" %}{{ i }}{% endfor %}')
end end
def test_increment_is_isolated_between_renders def test_increment_is_isolated_between_renders
assert_template_result( Liquid::Template.file_system = StubFileSystem.new('incr' => '{% increment %}')
'010', assert_template_result('010', '{% increment %}{% increment %}{% render "incr" %}')
'{% increment %}{% increment %}{% render "incr" %}',
partials: { 'incr' => '{% increment %}' },
)
end end
def test_decrement_is_isolated_between_renders def test_decrement_is_isolated_between_renders
assert_template_result( Liquid::Template.file_system = StubFileSystem.new('decr' => '{% decrement %}')
'-1-2-1', assert_template_result('-1-2-1', '{% decrement %}{% decrement %}{% render "decr" %}')
'{% decrement %}{% decrement %}{% render "decr" %}',
partials: { 'decr' => '{% decrement %}' },
)
end end
def test_includes_will_not_render_inside_render_tag def test_includes_will_not_render_inside_render_tag
assert_template_result( Liquid::Template.file_system = StubFileSystem.new(
'Liquid error (test_include line 1): include usage is not allowed in this context', 'foo' => 'bar',
'{% render "test_include" %}', 'test_include' => '{% include "foo" %}'
render_errors: true,
partials: {
'foo' => 'bar',
'test_include' => '{% include "foo" %}',
},
) )
exc = assert_raises(Liquid::DisabledError) do
Liquid::Template.parse('{% render "test_include" %}').render!
end
assert_equal('Liquid error: include usage is not allowed in this context', exc.message)
end end
def test_includes_will_not_render_inside_nested_sibling_tags def test_includes_will_not_render_inside_nested_sibling_tags
@@ -180,109 +143,79 @@ class RenderTagTest < Minitest::Test
'nested_render_with_sibling_include' => '{% render "test_include" %}{% include "foo" %}', 'nested_render_with_sibling_include' => '{% render "test_include" %}{% include "foo" %}',
'test_include' => '{% include "foo" %}', 'test_include' => '{% include "foo" %}',
}, },
render_errors: true, render_errors: true
) )
end end
def test_render_tag_with def test_render_tag_with
assert_template_result( Liquid::Template.file_system = StubFileSystem.new(
"Product: Draft 151cm ", 'product' => "Product: {{ product.title }} ",
"{% render 'product' with products[0] %}", 'product_alias' => "Product: {{ product.title }} ",
{ "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] },
partials: {
'product' => "Product: {{ product.title }} ",
'product_alias' => "Product: {{ product.title }} ",
},
) )
assert_template_result("Product: Draft 151cm ",
"{% render 'product' with products[0] %}",
{ "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] })
end end
def test_render_tag_with_alias def test_render_tag_with_alias
assert_template_result( Liquid::Template.file_system = StubFileSystem.new(
"Product: Draft 151cm ", 'product' => "Product: {{ product.title }} ",
"{% render 'product_alias' with products[0] as product %}", 'product_alias' => "Product: {{ product.title }} ",
{ "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] },
partials: {
'product' => "Product: {{ product.title }} ",
'product_alias' => "Product: {{ product.title }} ",
},
) )
assert_template_result("Product: Draft 151cm ",
"{% render 'product_alias' with products[0] as product %}",
{ "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] })
end end
def test_render_tag_for_alias def test_render_tag_for_alias
assert_template_result( Liquid::Template.file_system = StubFileSystem.new(
"Product: Draft 151cm Product: Element 155cm ", 'product' => "Product: {{ product.title }} ",
"{% render 'product_alias' for products as product %}", 'product_alias' => "Product: {{ product.title }} ",
{ "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] },
partials: {
'product' => "Product: {{ product.title }} ",
'product_alias' => "Product: {{ product.title }} ",
},
) )
assert_template_result("Product: Draft 151cm Product: Element 155cm ",
"{% render 'product_alias' for products as product %}",
{ "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] })
end end
def test_render_tag_for def test_render_tag_for
assert_template_result( Liquid::Template.file_system = StubFileSystem.new(
"Product: Draft 151cm Product: Element 155cm ", 'product' => "Product: {{ product.title }} ",
"{% render 'product' for products %}", 'product_alias' => "Product: {{ product.title }} ",
{ "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] },
partials: {
'product' => "Product: {{ product.title }} ",
'product_alias' => "Product: {{ product.title }} ",
},
) )
assert_template_result("Product: Draft 151cm Product: Element 155cm ",
"{% render 'product' for products %}",
{ "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] })
end end
def test_render_tag_forloop def test_render_tag_forloop
assert_template_result( Liquid::Template.file_system = StubFileSystem.new(
"Product: Draft 151cm first index:1 Product: Element 155cm last index:2 ", 'product' => "Product: {{ product.title }} {% if forloop.first %}first{% endif %} {% if forloop.last %}last{% endif %} index:{{ forloop.index }} ",
"{% render 'product' for products %}",
{ "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] },
partials: {
'product' => "Product: {{ product.title }} {% if forloop.first %}first{% endif %} {% if forloop.last %}last{% endif %} index:{{ forloop.index }} ",
},
) )
assert_template_result("Product: Draft 151cm first index:1 Product: Element 155cm last index:2 ",
"{% render 'product' for products %}",
{ "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] })
end end
def test_render_tag_for_drop def test_render_tag_for_drop
assert_template_result( Liquid::Template.file_system = StubFileSystem.new(
"123", 'loop' => "{{ value.foo }}",
"{% render 'loop' for loop as value %}",
{ "loop" => TestEnumerable.new },
partials: {
'loop' => "{{ value.foo }}",
},
) )
assert_template_result("123",
"{% render 'loop' for loop as value %}", { "loop" => TestEnumerable.new })
end end
def test_render_tag_with_drop def test_render_tag_with_drop
assert_template_result( Liquid::Template.file_system = StubFileSystem.new(
"TestEnumerable", 'loop' => "{{ value }}",
"{% render 'loop' with loop as value %}",
{ "loop" => TestEnumerable.new },
partials: {
'loop' => "{{ value }}",
},
) )
end
def test_render_tag_renders_error_with_template_name assert_template_result("TestEnumerable",
assert_template_result( "{% render 'loop' with loop as value %}", { "loop" => TestEnumerable.new })
'Liquid error (foo line 1): standard error',
"{% render 'foo' with errors %}",
{ 'errors' => ErrorDrop.new },
partials: { 'foo' => '{{ foo.standard_error }}' },
render_errors: true,
)
end
def test_render_tag_renders_error_with_template_name_from_template_factory
assert_template_result(
'Liquid error (some/path/foo line 1): standard error',
"{% render 'foo' with errors %}",
{ 'errors' => ErrorDrop.new },
partials: { 'foo' => '{{ foo.standard_error }}' },
template_factory: StubTemplateFactory.new,
render_errors: true,
)
end end
end end
+52 -107
View File
@@ -6,10 +6,8 @@ class StandardTagTest < Minitest::Test
include Liquid include Liquid
def test_no_transform def test_no_transform
assert_template_result( assert_template_result('this text should come out of the template without change...',
'this text should come out of the template without change...', 'this text should come out of the template without change...')
'this text should come out of the template without change...',
)
assert_template_result('blah', 'blah') assert_template_result('blah', 'blah')
assert_template_result('<blah>', '<blah>') assert_template_result('<blah>', '<blah>')
@@ -22,10 +20,8 @@ class StandardTagTest < Minitest::Test
end end
def test_has_a_block_which_does_nothing def test_has_a_block_which_does_nothing
assert_template_result( assert_template_result(%(the comment block should be removed .. right?),
%(the comment block should be removed .. right?), %(the comment block should be removed {%comment%} be gone.. {%endcomment%} .. right?))
%(the comment block should be removed {%comment%} be gone.. {%endcomment%} .. right?),
)
assert_template_result('', '{%comment%}{%endcomment%}') assert_template_result('', '{%comment%}{%endcomment%}')
assert_template_result('', '{%comment%}{% endcomment %}') assert_template_result('', '{%comment%}{% endcomment %}')
@@ -40,8 +36,6 @@ class StandardTagTest < Minitest::Test
assert_template_result('', '{%comment%}{% endif %}{%endcomment%}') assert_template_result('', '{%comment%}{% endif %}{%endcomment%}')
assert_template_result('', '{% comment %}{% endwhatever %}{% endcomment %}') assert_template_result('', '{% comment %}{% endwhatever %}{% endcomment %}')
assert_template_result('', '{% comment %}{% raw %} {{%%%%}} }} { {% endcomment %} {% comment {% endraw %} {% endcomment %}') assert_template_result('', '{% comment %}{% raw %} {{%%%%}} }} { {% endcomment %} {% comment {% endraw %} {% endcomment %}')
assert_template_result('', '{% comment %}{% " %}{% endcomment %}')
assert_template_result('', '{% comment %}{%%}{% endcomment %}')
assert_template_result('foobar', 'foo{%comment%}comment{%endcomment%}bar') assert_template_result('foobar', 'foo{%comment%}comment{%endcomment%}bar')
assert_template_result('foobar', 'foo{% comment %}comment{% endcomment %}bar') assert_template_result('foobar', 'foo{% comment %}comment{% endcomment %}bar')
@@ -68,81 +62,61 @@ class StandardTagTest < Minitest::Test
def test_capture def test_capture
assigns = { 'var' => 'content' } assigns = { 'var' => 'content' }
assert_template_result( assert_template_result('content foo content foo ',
'content foo content foo ',
'{{ var2 }}{% capture var2 %}{{ var }} foo {% endcapture %}{{ var2 }}{{ var2 }}', '{{ var2 }}{% capture var2 %}{{ var }} foo {% endcapture %}{{ var2 }}{{ var2 }}',
assigns, assigns)
)
end end
def test_capture_detects_bad_syntax def test_capture_detects_bad_syntax
assert_raises(SyntaxError) do assert_raises(SyntaxError) do
assert_template_result( assert_template_result('content foo content foo ',
'content foo content foo ',
'{{ var2 }}{% capture %}{{ var }} foo {% endcapture %}{{ var2 }}{{ var2 }}', '{{ var2 }}{% capture %}{{ var }} foo {% endcapture %}{{ var2 }}{{ var2 }}',
{ 'var' => 'content' }, { 'var' => 'content' })
)
end end
end end
def test_case def test_case
assigns = { 'condition' => 2 } assigns = { 'condition' => 2 }
assert_template_result( assert_template_result(' its 2 ',
' its 2 ',
'{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}', '{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}',
assigns, assigns)
)
assigns = { 'condition' => 1 } assigns = { 'condition' => 1 }
assert_template_result( assert_template_result(' its 1 ',
' its 1 ',
'{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}', '{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}',
assigns, assigns)
)
assigns = { 'condition' => 3 } assigns = { 'condition' => 3 }
assert_template_result( assert_template_result('',
'',
'{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}', '{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}',
assigns, assigns)
)
assigns = { 'condition' => "string here" } assigns = { 'condition' => "string here" }
assert_template_result( assert_template_result(' hit ',
' hit ',
'{% case condition %}{% when "string here" %} hit {% endcase %}', '{% case condition %}{% when "string here" %} hit {% endcase %}',
assigns, assigns)
)
assigns = { 'condition' => "bad string here" } assigns = { 'condition' => "bad string here" }
assert_template_result( assert_template_result('',
'',
'{% case condition %}{% when "string here" %} hit {% endcase %}',\ '{% case condition %}{% when "string here" %} hit {% endcase %}',\
assigns, assigns)
)
end end
def test_case_with_else def test_case_with_else
assigns = { 'condition' => 5 } assigns = { 'condition' => 5 }
assert_template_result( assert_template_result(' hit ',
' hit ',
'{% case condition %}{% when 5 %} hit {% else %} else {% endcase %}', '{% case condition %}{% when 5 %} hit {% else %} else {% endcase %}',
assigns, assigns)
)
assigns = { 'condition' => 6 } assigns = { 'condition' => 6 }
assert_template_result( assert_template_result(' else ',
' else ',
'{% case condition %}{% when 5 %} hit {% else %} else {% endcase %}', '{% case condition %}{% when 5 %} hit {% else %} else {% endcase %}',
assigns, assigns)
)
assigns = { 'condition' => 6 } assigns = { 'condition' => 6 }
assert_template_result( assert_template_result(' else ',
' else ',
'{% case condition %} {% when 5 %} hit {% else %} else {% endcase %}', '{% case condition %} {% when 5 %} hit {% else %} else {% endcase %}',
assigns, assigns)
)
end end
def test_case_on_size def test_case_on_size
@@ -155,67 +129,47 @@ class StandardTagTest < Minitest::Test
end end
def test_case_on_size_with_else def test_case_on_size_with_else
assert_template_result( assert_template_result('else',
'else',
'{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}',
{ 'a' => [] }, { 'a' => [] })
)
assert_template_result( assert_template_result('1',
'1',
'{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}',
{ 'a' => [1] }, { 'a' => [1] })
)
assert_template_result( assert_template_result('2',
'2',
'{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}',
{ 'a' => [1, 1] }, { 'a' => [1, 1] })
)
assert_template_result( assert_template_result('else',
'else',
'{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}',
{ 'a' => [1, 1, 1] }, { 'a' => [1, 1, 1] })
)
assert_template_result( assert_template_result('else',
'else',
'{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}',
{ 'a' => [1, 1, 1, 1] }, { 'a' => [1, 1, 1, 1] })
)
assert_template_result( assert_template_result('else',
'else',
'{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}',
{ 'a' => [1, 1, 1, 1, 1] }, { 'a' => [1, 1, 1, 1, 1] })
)
end end
def test_case_on_length_with_else def test_case_on_length_with_else
assert_template_result( assert_template_result('else',
'else',
'{% case a.empty? %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', '{% case a.empty? %}{% when true %}true{% when false %}false{% else %}else{% endcase %}',
{}, {})
)
assert_template_result( assert_template_result('false',
'false',
'{% case false %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', '{% case false %}{% when true %}true{% when false %}false{% else %}else{% endcase %}',
{}, {})
)
assert_template_result( assert_template_result('true',
'true',
'{% case true %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', '{% case true %}{% when true %}true{% when false %}false{% else %}else{% endcase %}',
{}, {})
)
assert_template_result( assert_template_result('else',
'else',
'{% case NULL %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', '{% case NULL %}{% when true %}true{% when false %}false{% else %}else{% endcase %}',
{}, {})
)
end end
def test_assign_from_case def test_assign_from_case
@@ -297,33 +251,24 @@ class StandardTagTest < Minitest::Test
assert_template_result('one two one', '{%cycle "one", "two"%} {%cycle "one", "two"%} {%cycle "one", "two"%}') assert_template_result('one two one', '{%cycle "one", "two"%} {%cycle "one", "two"%} {%cycle "one", "two"%}')
assert_template_result( assert_template_result('text-align: left text-align: right',
'text-align: left text-align: right', '{%cycle "text-align: left", "text-align: right" %} {%cycle "text-align: left", "text-align: right"%}')
'{%cycle "text-align: left", "text-align: right" %} {%cycle "text-align: left", "text-align: right"%}',
)
end end
def test_multiple_cycles def test_multiple_cycles
assert_template_result( assert_template_result('1 2 1 1 2 3 1',
'1 2 1 1 2 3 1', '{%cycle 1,2%} {%cycle 1,2%} {%cycle 1,2%} {%cycle 1,2,3%} {%cycle 1,2,3%} {%cycle 1,2,3%} {%cycle 1,2,3%}')
'{%cycle 1,2%} {%cycle 1,2%} {%cycle 1,2%} {%cycle 1,2,3%} {%cycle 1,2,3%} {%cycle 1,2,3%} {%cycle 1,2,3%}',
)
end end
def test_multiple_named_cycles def test_multiple_named_cycles
assert_template_result( assert_template_result('one one two two one one',
'one one two two one one', '{%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %} {%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %} {%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %}')
'{%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %} {%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %} {%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %}',
)
end end
def test_multiple_named_cycles_with_names_from_context def test_multiple_named_cycles_with_names_from_context
assigns = { "var1" => 1, "var2" => 2 } assigns = { "var1" => 1, "var2" => 2 }
assert_template_result( assert_template_result('one one two two one one',
'one one two two one one', '{%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %} {%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %} {%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %}', assigns)
'{%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %} {%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %} {%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %}',
assigns,
)
end end
def test_size_of_array def test_size_of_array
+18 -208
View File
@@ -18,241 +18,51 @@ class TableRowTest < Minitest::Test
end end
def test_table_row def test_table_row
assert_template_result( assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
"<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
'{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}', '{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}',
{ 'numbers' => [1, 2, 3, 4, 5, 6] }, { 'numbers' => [1, 2, 3, 4, 5, 6] })
)
assert_template_result( assert_template_result("<tr class=\"row1\">\n</tr>\n",
"<tr class=\"row1\">\n</tr>\n",
'{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}', '{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}',
{ 'numbers' => [] }, { 'numbers' => [] })
)
end end
def test_table_row_with_different_cols def test_table_row_with_different_cols
assert_template_result( assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td><td class=\"col4\"> 4 </td><td class=\"col5\"> 5 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 6 </td></tr>\n",
"<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td><td class=\"col4\"> 4 </td><td class=\"col5\"> 5 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 6 </td></tr>\n",
'{% tablerow n in numbers cols:5%} {{n}} {% endtablerow %}', '{% tablerow n in numbers cols:5%} {{n}} {% endtablerow %}',
{ 'numbers' => [1, 2, 3, 4, 5, 6] }, { 'numbers' => [1, 2, 3, 4, 5, 6] })
)
end end
def test_table_col_counter def test_table_col_counter
assert_template_result( assert_template_result("<tr class=\"row1\">\n<td class=\"col1\">1</td><td class=\"col2\">2</td></tr>\n<tr class=\"row2\"><td class=\"col1\">1</td><td class=\"col2\">2</td></tr>\n<tr class=\"row3\"><td class=\"col1\">1</td><td class=\"col2\">2</td></tr>\n",
"<tr class=\"row1\">\n<td class=\"col1\">1</td><td class=\"col2\">2</td></tr>\n<tr class=\"row2\"><td class=\"col1\">1</td><td class=\"col2\">2</td></tr>\n<tr class=\"row3\"><td class=\"col1\">1</td><td class=\"col2\">2</td></tr>\n",
'{% tablerow n in numbers cols:2%}{{tablerowloop.col}}{% endtablerow %}', '{% tablerow n in numbers cols:2%}{{tablerowloop.col}}{% endtablerow %}',
{ 'numbers' => [1, 2, 3, 4, 5, 6] }, { 'numbers' => [1, 2, 3, 4, 5, 6] })
)
end end
def test_quoted_fragment def test_quoted_fragment
assert_template_result( assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
"<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
"{% tablerow n in collections.frontpage cols:3%} {{n}} {% endtablerow %}", "{% tablerow n in collections.frontpage cols:3%} {{n}} {% endtablerow %}",
{ 'collections' => { 'frontpage' => [1, 2, 3, 4, 5, 6] } }, { 'collections' => { 'frontpage' => [1, 2, 3, 4, 5, 6] } })
) assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
assert_template_result(
"<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
"{% tablerow n in collections['frontpage'] cols:3%} {{n}} {% endtablerow %}", "{% tablerow n in collections['frontpage'] cols:3%} {{n}} {% endtablerow %}",
{ 'collections' => { 'frontpage' => [1, 2, 3, 4, 5, 6] } }, { 'collections' => { 'frontpage' => [1, 2, 3, 4, 5, 6] } })
)
end end
def test_enumerable_drop def test_enumerable_drop
assert_template_result( assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
"<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
'{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}', '{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}',
{ 'numbers' => ArrayDrop.new([1, 2, 3, 4, 5, 6]) }, { 'numbers' => ArrayDrop.new([1, 2, 3, 4, 5, 6]) })
)
end end
def test_offset_and_limit def test_offset_and_limit
assert_template_result( assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
"<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
'{% tablerow n in numbers cols:3 offset:1 limit:6%} {{n}} {% endtablerow %}', '{% tablerow n in numbers cols:3 offset:1 limit:6%} {{n}} {% endtablerow %}',
{ 'numbers' => [0, 1, 2, 3, 4, 5, 6, 7] }, { 'numbers' => [0, 1, 2, 3, 4, 5, 6, 7] })
)
end end
def test_blank_string_not_iterable def test_blank_string_not_iterable
assert_template_result( assert_template_result("<tr class=\"row1\">\n</tr>\n",
"<tr class=\"row1\">\n</tr>\n",
"{% tablerow char in characters cols:3 %}I WILL NOT BE OUTPUT{% endtablerow %}", "{% tablerow char in characters cols:3 %}I WILL NOT BE OUTPUT{% endtablerow %}",
{ 'characters' => '' }, { 'characters' => '' })
)
end
def test_cols_nil_constant_same_as_evaluated_nil_expression
expect = "<tr class=\"row1\">\n" \
"<td class=\"col1\">false</td>" \
"<td class=\"col2\">false</td>" \
"</tr>\n"
assert_template_result(
expect,
"{% tablerow i in (1..2) cols:nil %}{{ tablerowloop.col_last }}{% endtablerow %}",
)
assert_template_result(
expect,
"{% tablerow i in (1..2) cols:var %}{{ tablerowloop.col_last }}{% endtablerow %}",
{ "var" => nil },
)
end
def test_nil_limit_is_treated_as_zero
expect = "<tr class=\"row1\">\n" \
"</tr>\n"
assert_template_result(
expect,
"{% tablerow i in (1..2) limit:nil %}{{ i }}{% endtablerow %}",
)
assert_template_result(
expect,
"{% tablerow i in (1..2) limit:var %}{{ i }}{% endtablerow %}",
{ "var" => nil },
)
end
def test_nil_offset_is_treated_as_zero
expect = "<tr class=\"row1\">\n" \
"<td class=\"col1\">1:false</td>" \
"<td class=\"col2\">2:true</td>" \
"</tr>\n"
assert_template_result(
expect,
"{% tablerow i in (1..2) offset:nil %}{{ i }}:{{ tablerowloop.col_last }}{% endtablerow %}",
)
assert_template_result(
expect,
"{% tablerow i in (1..2) offset:var %}{{ i }}:{{ tablerowloop.col_last }}{% endtablerow %}",
{ "var" => nil },
)
end
def test_tablerow_loop_drop_attributes
template = <<~LIQUID.chomp
{% tablerow i in (1...2) %}
col: {{ tablerowloop.col }}
col0: {{ tablerowloop.col0 }}
col_first: {{ tablerowloop.col_first }}
col_last: {{ tablerowloop.col_last }}
first: {{ tablerowloop.first }}
index: {{ tablerowloop.index }}
index0: {{ tablerowloop.index0 }}
last: {{ tablerowloop.last }}
length: {{ tablerowloop.length }}
rindex: {{ tablerowloop.rindex }}
rindex0: {{ tablerowloop.rindex0 }}
row: {{ tablerowloop.row }}
{% endtablerow %}
LIQUID
expected_output = <<~OUTPUT
<tr class="row1">
<td class="col1">
col: 1
col0: 0
col_first: true
col_last: false
first: true
index: 1
index0: 0
last: false
length: 2
rindex: 2
rindex0: 1
row: 1
</td><td class="col2">
col: 2
col0: 1
col_first: false
col_last: true
first: false
index: 2
index0: 1
last: true
length: 2
rindex: 1
rindex0: 0
row: 1
</td></tr>
OUTPUT
assert_template_result(expected_output, template)
end
def test_table_row_renders_correct_error_message_for_invalid_parameters
assert_template_result(
"Liquid error (line 1): invalid integer",
'{% tablerow n in (1...10) limit:true %} {{n}} {% endtablerow %}',
render_errors: true,
)
assert_template_result(
"Liquid error (line 1): invalid integer",
'{% tablerow n in (1...10) offset:true %} {{n}} {% endtablerow %}',
render_errors: true,
)
assert_template_result(
"Liquid error (line 1): invalid integer",
'{% tablerow n in (1...10) cols:true %} {{n}} {% endtablerow %}',
render_errors: true,
)
end
def test_table_row_handles_interrupts
assert_template_result(
"<tr class=\"row1\">\n<td class=\"col1\"> 1 </td></tr>\n",
'{% tablerow n in (1...3) cols:2 %} {{n}} {% break %} {{n}} {% endtablerow %}',
)
assert_template_result(
"<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 3 </td></tr>\n",
'{% tablerow n in (1...3) cols:2 %} {{n}} {% continue %} {{n}} {% endtablerow %}',
)
end
def test_table_row_does_not_leak_interrupts
template = <<~LIQUID
{% for i in (1..2) -%}
{% for j in (1..2) -%}
{% tablerow k in (1..3) %}{% break %}{% endtablerow -%}
loop j={{ j }}
{% endfor -%}
loop i={{ i }}
{% endfor -%}
after loop
LIQUID
expected = <<~STR
<tr class="row1">
<td class="col1"></td></tr>
loop j=1
<tr class="row1">
<td class="col1"></td></tr>
loop j=2
loop i=1
<tr class="row1">
<td class="col1"></td></tr>
loop j=1
<tr class="row1">
<td class="col1"></td></tr>
loop j=2
loop i=2
after loop
STR
assert_template_result(
expected,
template,
)
end end
end end
@@ -7,10 +7,8 @@ class UnlessElseTagTest < Minitest::Test
def test_unless def test_unless
assert_template_result(' ', ' {% unless true %} this text should not go into the output {% endunless %} ') assert_template_result(' ', ' {% unless true %} this text should not go into the output {% endunless %} ')
assert_template_result( assert_template_result(' this text should go into the output ',
' this text should go into the output ', ' {% unless false %} this text should go into the output {% endunless %} ')
' {% unless false %} this text should go into the output {% endunless %} ',
)
assert_template_result(' you rock ?', '{% unless true %} you suck {% endunless %} {% unless false %} you rock {% endunless %}?') assert_template_result(' you rock ?', '{% unless true %} you suck {% endunless %} {% unless false %} you rock {% endunless %}?')
end end
+4 -24
View File
@@ -93,12 +93,10 @@ class TemplateTest < Minitest::Test
def test_lambda_is_called_once_from_custom_assigns_over_multiple_parses_and_renders def test_lambda_is_called_once_from_custom_assigns_over_multiple_parses_and_renders
t = Template.new t = Template.new
assigns = { assigns = { 'number' => -> {
'number' => -> { @global ||= 0
@global ||= 0 @global += 1
@global += 1 } }
},
}
assert_equal('1', t.parse("{{number}}").render!(assigns)) assert_equal('1', t.parse("{{number}}").render!(assigns))
assert_equal('1', t.parse("{{number}}").render!(assigns)) assert_equal('1', t.parse("{{number}}").render!(assigns))
assert_equal('1', t.render!(assigns)) assert_equal('1', t.render!(assigns))
@@ -337,22 +335,4 @@ class TemplateTest < Minitest::Test
assert_equal("x=2", output) assert_equal("x=2", output)
assert_instance_of(String, output) assert_instance_of(String, output)
end end
def test_raises_error_with_invalid_utf8
e = assert_raises(TemplateEncodingError) do
Template.parse(<<~LIQUID)
{% comment %}
\xC0
{% endcomment %}
LIQUID
end
assert_equal('Liquid error: Invalid template encoding', e.message)
end
def test_allows_non_string_values_as_source
assert_equal('', Template.parse(nil).render)
assert_equal('1', Template.parse(1).render)
assert_equal('true', Template.parse(true).render)
end
end end
-75
View File
@@ -1,7 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'test_helper' require 'test_helper'
require 'timeout'
class VariableTest < Minitest::Test class VariableTest < Minitest::Test
include Liquid include Liquid
@@ -25,23 +24,17 @@ class VariableTest < Minitest::Test
def test_if_tag_calls_to_liquid_value def test_if_tag_calls_to_liquid_value
assert_template_result('one', '{% if foo == 1 %}one{% endif %}', { 'foo' => IntegerDrop.new('1') }) assert_template_result('one', '{% if foo == 1 %}one{% endif %}', { 'foo' => IntegerDrop.new('1') })
assert_template_result('one', '{% if foo == eqv %}one{% endif %}', { 'foo' => IntegerDrop.new(1), 'eqv' => IntegerDrop.new(1) })
assert_template_result('one', '{% if 0 < foo %}one{% endif %}', { 'foo' => IntegerDrop.new('1') }) assert_template_result('one', '{% if 0 < foo %}one{% endif %}', { 'foo' => IntegerDrop.new('1') })
assert_template_result('one', '{% if foo > 0 %}one{% endif %}', { 'foo' => IntegerDrop.new('1') }) assert_template_result('one', '{% if foo > 0 %}one{% endif %}', { 'foo' => IntegerDrop.new('1') })
assert_template_result('one', '{% if b > a %}one{% endif %}', { 'b' => IntegerDrop.new(1), 'a' => IntegerDrop.new(0) })
assert_template_result('true', '{% if foo == true %}true{% endif %}', { 'foo' => BooleanDrop.new(true) }) assert_template_result('true', '{% if foo == true %}true{% endif %}', { 'foo' => BooleanDrop.new(true) })
assert_template_result('true', '{% if foo %}true{% endif %}', { 'foo' => BooleanDrop.new(true) }) assert_template_result('true', '{% if foo %}true{% endif %}', { 'foo' => BooleanDrop.new(true) })
assert_template_result('', '{% if foo %}true{% endif %}', { 'foo' => BooleanDrop.new(false) }) assert_template_result('', '{% if foo %}true{% endif %}', { 'foo' => BooleanDrop.new(false) })
assert_template_result('', '{% if foo == true %}True{% endif %}', { 'foo' => BooleanDrop.new(false) }) assert_template_result('', '{% if foo == true %}True{% endif %}', { 'foo' => BooleanDrop.new(false) })
assert_template_result('', '{% if foo and true %}SHOULD NOT HAPPEN{% endif %}', { 'foo' => BooleanDrop.new(false) })
assert_template_result('one', '{% if a contains x %}one{% endif %}', { 'a' => [1], 'x' => IntegerDrop.new(1) })
end end
def test_unless_tag_calls_to_liquid_value def test_unless_tag_calls_to_liquid_value
assert_template_result('', '{% unless foo %}true{% endunless %}', { 'foo' => BooleanDrop.new(true) }) assert_template_result('', '{% unless foo %}true{% endunless %}', { 'foo' => BooleanDrop.new(true) })
assert_template_result('true', '{% unless foo %}true{% endunless %}', { 'foo' => BooleanDrop.new(false) })
end end
def test_case_tag_calls_to_liquid_value def test_case_tag_calls_to_liquid_value
@@ -137,72 +130,4 @@ class VariableTest < Minitest::Test
def test_raw_value_variable def test_raw_value_variable
assert_template_result('bar', '{{ [key] }}', { 'key' => 'foo', 'foo' => 'bar' }) assert_template_result('bar', '{{ [key] }}', { 'key' => 'foo', 'foo' => 'bar' })
end end
def test_dynamic_find_var_with_drop
assert_template_result(
'bar',
'{{ [list[settings.zero]] }}',
{
'list' => ['foo'],
'settings' => SettingsDrop.new("zero" => 0),
'foo' => 'bar',
},
)
assert_template_result(
'foo',
'{{ [list[settings.zero]["foo"]] }}',
{
'list' => [{ 'foo' => 'bar' }],
'settings' => SettingsDrop.new("zero" => 0),
'bar' => 'foo',
},
)
end
def test_double_nested_variable_lookup
assert_template_result(
'bar',
'{{ list[list[settings.zero]]["foo"] }}',
{
'list' => [1, { 'foo' => 'bar' }],
'settings' => SettingsDrop.new("zero" => 0),
'bar' => 'foo',
},
)
end
def test_variable_lookup_should_not_hang_with_invalid_syntax
Timeout.timeout(1) do
assert_template_result(
'bar',
"{{['foo'}}",
{
'foo' => 'bar',
},
error_mode: :lax,
)
end
very_long_key = "1234567890" * 100
template_list = [
"{{['#{very_long_key}']}}", # valid
"{{['#{very_long_key}'}}", # missing closing bracket
"{{[['#{very_long_key}']}}", # extra open bracket
]
template_list.each do |template|
Timeout.timeout(1) do
assert_template_result(
'bar',
template,
{
very_long_key => 'bar',
},
error_mode: :lax,
)
end
end
end
end end
+42 -36
View File
@@ -39,14 +39,12 @@ module Minitest
def assert_template_result( def assert_template_result(
expected, template, assigns = {}, expected, template, assigns = {},
message: nil, partials: nil, error_mode: nil, render_errors: false, message: nil, partials: nil, error_mode: nil, render_errors: false
template_factory: nil
) )
file_system = StubFileSystem.new(partials || {}) template = Liquid::Template.parse(template, line_numbers: true, error_mode: error_mode&.to_sym)
environment = Liquid::Environment.build(file_system: file_system) file_system = StubFileSystem.new(partials) if partials
template = Liquid::Template.parse(template, line_numbers: true, error_mode: error_mode&.to_sym, environment: environment) registers = Liquid::Registers.new(file_system: file_system)
registers = Liquid::Registers.new(file_system: file_system, template_factory: template_factory) context = Liquid::Context.build(environments: assigns, rethrow_errors: !render_errors, registers: registers)
context = Liquid::Context.build(static_environments: assigns, rethrow_errors: !render_errors, registers: registers, environment: environment)
output = template.render(context) output = template.render(context)
assert_equal(expected, output, message) assert_equal(expected, output, message)
end end
@@ -58,10 +56,6 @@ module Minitest
assert_match(match, exception.message) assert_match(match, exception.message)
end end
def assert_syntax_error(template, error_mode: nil)
assert_match_syntax_error("", template, error_mode: error_mode)
end
def assert_usage_increment(name, times: 1) def assert_usage_increment(name, times: 1)
old_method = Liquid::Usage.method(:increment) old_method = Liquid::Usage.method(:increment)
calls = 0 calls = 0
@@ -79,12 +73,22 @@ module Minitest
assert_equal(times, calls, "Number of calls to Usage.increment with #{name.inspect}") assert_equal(times, calls, "Number of calls to Usage.increment with #{name.inspect}")
end end
def with_global_filter(*globals, &blk) def with_global_filter(*globals)
environment = Liquid::Environment.build do |w| original_global_cache = Liquid::StrainerFactory::GlobalCache
w.register_filters(globals) Liquid::StrainerFactory.send(:remove_const, :GlobalCache)
end Liquid::StrainerFactory.const_set(:GlobalCache, Class.new(Liquid::StrainerTemplate))
Environment.dangerously_override(environment, &blk) globals.each do |global|
Liquid::Template.register_filter(global)
end
Liquid::StrainerFactory.send(:strainer_class_cache).clear
begin
yield
ensure
Liquid::StrainerFactory.send(:remove_const, :GlobalCache)
Liquid::StrainerFactory.const_set(:GlobalCache, original_global_cache)
Liquid::StrainerFactory.send(:strainer_class_cache).clear
end
end end
def with_error_mode(mode) def with_error_mode(mode)
@@ -95,11 +99,18 @@ module Minitest
Liquid::Template.error_mode = old_mode Liquid::Template.error_mode = old_mode
end end
def with_custom_tag(tag_name, tag_class, &block) def with_custom_tag(tag_name, tag_class)
environment = Liquid::Environment.default.dup old_tag = Liquid::Template.tags[tag_name]
environment.register_tag(tag_name, tag_class) begin
Liquid::Template.register_tag(tag_name, tag_class)
Environment.dangerously_override(environment, &block) yield
ensure
if old_tag
Liquid::Template.tags[tag_name] = old_tag
else
Liquid::Template.tags.delete(tag_name)
end
end
end end
end end
end end
@@ -110,23 +121,16 @@ class ThingWithToLiquid
end end
end end
class SettingsDrop < Liquid::Drop
def initialize(settings)
super()
@settings = settings
end
def liquid_method_missing(key)
@settings[key]
end
end
class IntegerDrop < Liquid::Drop class IntegerDrop < Liquid::Drop
def initialize(value) def initialize(value)
super() super()
@value = value.to_i @value = value.to_i
end end
def ==(other)
@value == other
end
def to_s def to_s
@value.to_s @value.to_s
end end
@@ -142,6 +146,10 @@ class BooleanDrop < Liquid::Drop
@value = value @value = value
end end
def ==(other)
@value == other
end
def to_liquid_value def to_liquid_value
@value @value
end end
@@ -194,10 +202,8 @@ class StubTemplateFactory
@count = 0 @count = 0
end end
def for(template_name) def for(_template_name)
@count += 1 @count += 1
template = Liquid::Template.new Liquid::Template.new
template.name = "some/path/" + template_name
template
end end
end end
+2 -10
View File
@@ -32,19 +32,11 @@ class BlockUnitTest < Minitest::Test
assert_equal(String, template.root.nodelist[2].class) assert_equal(String, template.root.nodelist[2].class)
end end
def test_variable_with_multibyte_character
template = Liquid::Template.parse("{{ '❤️' }}")
assert_equal(1, template.root.nodelist.size)
assert_equal(Variable, template.root.nodelist[0].class)
end
def test_variable_many_embedded_fragments def test_variable_many_embedded_fragments
template = Liquid::Template.parse(" {{funk}} {{so}} {{brother}} ") template = Liquid::Template.parse(" {{funk}} {{so}} {{brother}} ")
assert_equal(7, template.root.nodelist.size) assert_equal(7, template.root.nodelist.size)
assert_equal( assert_equal([String, Variable, String, Variable, String, Variable, String],
[String, Variable, String, Variable, String, Variable, String], block_types(template.root.nodelist))
block_types(template.root.nodelist),
)
end end
def test_with_block def test_with_block
+4 -13
View File
@@ -55,11 +55,6 @@ class ConditionUnitTest < Minitest::Test
assert_evaluates_false('bob', 'contains', '---') assert_evaluates_false('bob', 'contains', '---')
end end
def test_contains_binary_encoding_compatibility_with_utf8
assert_evaluates_true('🙈'.b, 'contains', '🙈')
assert_evaluates_true('🙈', 'contains', '🙈'.b)
end
def test_invalid_comparation_operator def test_invalid_comparation_operator
assert_evaluates_argument_error(1, '~~', 0) assert_evaluates_argument_error(1, '~~', 0)
end end
@@ -169,17 +164,13 @@ class ConditionUnitTest < Minitest::Test
private private
def assert_evaluates_true(left, op, right) def assert_evaluates_true(left, op, right)
assert( assert(Condition.new(left, op, right).evaluate(@context),
Condition.new(left, op, right).evaluate(@context), "Evaluated false: #{left} #{op} #{right}")
"Evaluated false: #{left.inspect} #{op} #{right.inspect}",
)
end end
def assert_evaluates_false(left, op, right) def assert_evaluates_false(left, op, right)
assert( assert(!Condition.new(left, op, right).evaluate(@context),
!Condition.new(left, op, right).evaluate(@context), "Evaluated true: #{left} #{op} #{right}")
"Evaluated true: #{left.inspect} #{op} #{right.inspect}",
)
end end
def assert_evaluates_argument_error(left, op, right) def assert_evaluates_argument_error(left, op, right)
-10
View File
@@ -50,14 +50,4 @@ class LexerUnitTest < Minitest::Test
Lexer.new("%").tokenize Lexer.new("%").tokenize
end end
end end
def test_negative_numbers
tokens = Lexer.new("foo | default: -1").tokenize
assert_equal([[:id, 'foo'], [:pipe, '|'], [:id, 'default'], [:colon, ":"], [:number, '-1'], [:end_of_string]], tokens)
end
def test_greater_than_two_digits
tokens = Lexer.new("foo > 12").tokenize
assert_equal([[:id, 'foo'], [:comparison, '>'], [:number, '12'], [:end_of_string]], tokens)
end
end end
+36 -36
View File
@@ -8,245 +8,245 @@ class ParseTreeVisitorTest < Minitest::Test
def test_variable def test_variable
assert_equal( assert_equal(
["test"], ["test"],
visit(%({{ test }})), visit(%({{ test }}))
) )
end end
def test_varible_with_filter def test_varible_with_filter
assert_equal( assert_equal(
["test", "infilter"], ["test", "infilter"],
visit(%({{ test | split: infilter }})), visit(%({{ test | split: infilter }}))
) )
end end
def test_dynamic_variable def test_dynamic_variable
assert_equal( assert_equal(
["test", "inlookup"], ["test", "inlookup"],
visit(%({{ test[inlookup] }})), visit(%({{ test[inlookup] }}))
) )
end end
def test_echo def test_echo
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% echo test %})), visit(%({% echo test %}))
) )
end end
def test_if_condition def test_if_condition
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% if test %}{% endif %})), visit(%({% if test %}{% endif %}))
) )
end end
def test_complex_if_condition def test_complex_if_condition
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% if 1 == 1 and 2 == test %}{% endif %})), visit(%({% if 1 == 1 and 2 == test %}{% endif %}))
) )
end end
def test_if_body def test_if_body
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% if 1 == 1 %}{{ test }}{% endif %})), visit(%({% if 1 == 1 %}{{ test }}{% endif %}))
) )
end end
def test_unless_condition def test_unless_condition
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% unless test %}{% endunless %})), visit(%({% unless test %}{% endunless %}))
) )
end end
def test_complex_unless_condition def test_complex_unless_condition
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% unless 1 == 1 and 2 == test %}{% endunless %})), visit(%({% unless 1 == 1 and 2 == test %}{% endunless %}))
) )
end end
def test_unless_body def test_unless_body
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% unless 1 == 1 %}{{ test }}{% endunless %})), visit(%({% unless 1 == 1 %}{{ test }}{% endunless %}))
) )
end end
def test_elsif_condition def test_elsif_condition
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% if 1 == 1 %}{% elsif test %}{% endif %})), visit(%({% if 1 == 1 %}{% elsif test %}{% endif %}))
) )
end end
def test_complex_elsif_condition def test_complex_elsif_condition
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% if 1 == 1 %}{% elsif 1 == 1 and 2 == test %}{% endif %})), visit(%({% if 1 == 1 %}{% elsif 1 == 1 and 2 == test %}{% endif %}))
) )
end end
def test_elsif_body def test_elsif_body
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% if 1 == 1 %}{% elsif 2 == 2 %}{{ test }}{% endif %})), visit(%({% if 1 == 1 %}{% elsif 2 == 2 %}{{ test }}{% endif %}))
) )
end end
def test_else_body def test_else_body
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% if 1 == 1 %}{% else %}{{ test }}{% endif %})), visit(%({% if 1 == 1 %}{% else %}{{ test }}{% endif %}))
) )
end end
def test_case_left def test_case_left
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% case test %}{% endcase %})), visit(%({% case test %}{% endcase %}))
) )
end end
def test_case_condition def test_case_condition
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% case 1 %}{% when test %}{% endcase %})), visit(%({% case 1 %}{% when test %}{% endcase %}))
) )
end end
def test_case_when_body def test_case_when_body
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% case 1 %}{% when 2 %}{{ test }}{% endcase %})), visit(%({% case 1 %}{% when 2 %}{{ test }}{% endcase %}))
) )
end end
def test_case_else_body def test_case_else_body
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% case 1 %}{% else %}{{ test }}{% endcase %})), visit(%({% case 1 %}{% else %}{{ test }}{% endcase %}))
) )
end end
def test_for_in def test_for_in
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% for x in test %}{% endfor %})), visit(%({% for x in test %}{% endfor %}))
) )
end end
def test_for_limit def test_for_limit
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% for x in (1..5) limit: test %}{% endfor %})), visit(%({% for x in (1..5) limit: test %}{% endfor %}))
) )
end end
def test_for_offset def test_for_offset
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% for x in (1..5) offset: test %}{% endfor %})), visit(%({% for x in (1..5) offset: test %}{% endfor %}))
) )
end end
def test_for_body def test_for_body
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% for x in (1..5) %}{{ test }}{% endfor %})), visit(%({% for x in (1..5) %}{{ test }}{% endfor %}))
) )
end end
def test_for_range def test_for_range
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% for x in (1..test) %}{% endfor %})), visit(%({% for x in (1..test) %}{% endfor %}))
) )
end end
def test_tablerow_in def test_tablerow_in
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% tablerow x in test %}{% endtablerow %})), visit(%({% tablerow x in test %}{% endtablerow %}))
) )
end end
def test_tablerow_limit def test_tablerow_limit
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% tablerow x in (1..5) limit: test %}{% endtablerow %})), visit(%({% tablerow x in (1..5) limit: test %}{% endtablerow %}))
) )
end end
def test_tablerow_offset def test_tablerow_offset
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% tablerow x in (1..5) offset: test %}{% endtablerow %})), visit(%({% tablerow x in (1..5) offset: test %}{% endtablerow %}))
) )
end end
def test_tablerow_body def test_tablerow_body
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% tablerow x in (1..5) %}{{ test }}{% endtablerow %})), visit(%({% tablerow x in (1..5) %}{{ test }}{% endtablerow %}))
) )
end end
def test_cycle def test_cycle
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% cycle test %})), visit(%({% cycle test %}))
) )
end end
def test_assign def test_assign
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% assign x = test %})), visit(%({% assign x = test %}))
) )
end end
def test_capture def test_capture
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% capture x %}{{ test }}{% endcapture %})), visit(%({% capture x %}{{ test }}{% endcapture %}))
) )
end end
def test_include def test_include
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% include test %})), visit(%({% include test %}))
) )
end end
def test_include_with def test_include_with
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% include "hai" with test %})), visit(%({% include "hai" with test %}))
) )
end end
def test_include_for def test_include_for
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% include "hai" for test %})), visit(%({% include "hai" for test %}))
) )
end end
def test_render_with def test_render_with
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% render "hai" with test %})), visit(%({% render "hai" with test %}))
) )
end end
def test_render_for def test_render_for
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% render "hai" for test %})), visit(%({% render "hai" for test %}))
) )
end end
@@ -257,7 +257,7 @@ class ParseTreeVisitorTest < Minitest::Test
["test", []], ["test", []],
["xs", []], ["xs", []],
]]], ]]],
traversal(%({% for x in xs offset: test %}{{ other }}{% endfor %})).visit, traversal(%({% for x in xs offset: test %}{{ other }}{% endfor %})).visit
) )
end end
+20 -61
View File
@@ -7,13 +7,13 @@ class PartialCacheUnitTest < Minitest::Test
context = Liquid::Context.build( context = Liquid::Context.build(
registers: { registers: {
file_system: StubFileSystem.new('my_partial' => 'my partial body'), file_system: StubFileSystem.new('my_partial' => 'my partial body'),
}, }
) )
partial = Liquid::PartialCache.load( partial = Liquid::PartialCache.load(
'my_partial', 'my_partial',
context: context, context: context,
parse_context: Liquid::ParseContext.new, parse_context: Liquid::ParseContext.new
) )
assert_equal('my partial body', partial.render) assert_equal('my partial body', partial.render)
@@ -22,14 +22,14 @@ class PartialCacheUnitTest < Minitest::Test
def test_reads_from_the_file_system_only_once_per_file def test_reads_from_the_file_system_only_once_per_file
file_system = StubFileSystem.new('my_partial' => 'some partial body') file_system = StubFileSystem.new('my_partial' => 'some partial body')
context = Liquid::Context.build( context = Liquid::Context.build(
registers: { file_system: file_system }, registers: { file_system: file_system }
) )
2.times do 2.times do
Liquid::PartialCache.load( Liquid::PartialCache.load(
'my_partial', 'my_partial',
context: context, context: context,
parse_context: Liquid::ParseContext.new, parse_context: Liquid::ParseContext.new
) )
end end
@@ -39,31 +39,31 @@ class PartialCacheUnitTest < Minitest::Test
def test_cache_state_is_stored_per_context def test_cache_state_is_stored_per_context
parse_context = Liquid::ParseContext.new parse_context = Liquid::ParseContext.new
shared_file_system = StubFileSystem.new( shared_file_system = StubFileSystem.new(
'my_partial' => 'my shared value', 'my_partial' => 'my shared value'
) )
context_one = Liquid::Context.build( context_one = Liquid::Context.build(
registers: { registers: {
file_system: shared_file_system, file_system: shared_file_system,
}, }
) )
context_two = Liquid::Context.build( context_two = Liquid::Context.build(
registers: { registers: {
file_system: shared_file_system, file_system: shared_file_system,
}, }
) )
2.times do 2.times do
Liquid::PartialCache.load( Liquid::PartialCache.load(
'my_partial', 'my_partial',
context: context_one, context: context_one,
parse_context: parse_context, parse_context: parse_context
) )
end end
Liquid::PartialCache.load( Liquid::PartialCache.load(
'my_partial', 'my_partial',
context: context_two, context: context_two,
parse_context: parse_context, parse_context: parse_context
) )
assert_equal(2, shared_file_system.file_read_count) assert_equal(2, shared_file_system.file_read_count)
@@ -72,18 +72,18 @@ class PartialCacheUnitTest < Minitest::Test
def test_cache_is_not_broken_when_a_different_parse_context_is_used def test_cache_is_not_broken_when_a_different_parse_context_is_used
file_system = StubFileSystem.new('my_partial' => 'some partial body') file_system = StubFileSystem.new('my_partial' => 'some partial body')
context = Liquid::Context.build( context = Liquid::Context.build(
registers: { file_system: file_system }, registers: { file_system: file_system }
) )
Liquid::PartialCache.load( Liquid::PartialCache.load(
'my_partial', 'my_partial',
context: context, context: context,
parse_context: Liquid::ParseContext.new(my_key: 'value one'), parse_context: Liquid::ParseContext.new(my_key: 'value one')
) )
Liquid::PartialCache.load( Liquid::PartialCache.load(
'my_partial', 'my_partial',
context: context, context: context,
parse_context: Liquid::ParseContext.new(my_key: 'value two'), parse_context: Liquid::ParseContext.new(my_key: 'value two')
) )
# Technically what we care about is that the file was parsed twice, # Technically what we care about is that the file was parsed twice,
@@ -95,13 +95,13 @@ class PartialCacheUnitTest < Minitest::Test
context = Liquid::Context.build( context = Liquid::Context.build(
registers: { registers: {
file_system: StubFileSystem.new('my_partial' => 'my partial body'), file_system: StubFileSystem.new('my_partial' => 'my partial body'),
}, }
) )
partial = Liquid::PartialCache.load( partial = Liquid::PartialCache.load(
'my_partial', 'my_partial',
context: context, context: context,
parse_context: Liquid::ParseContext.new, parse_context: Liquid::ParseContext.new
) )
assert_equal('my partial body', partial.render) assert_equal('my partial body', partial.render)
@@ -113,13 +113,13 @@ class PartialCacheUnitTest < Minitest::Test
registers: { registers: {
file_system: StubFileSystem.new('my_partial' => 'my partial body'), file_system: StubFileSystem.new('my_partial' => 'my partial body'),
template_factory: template_factory, template_factory: template_factory,
}, }
) )
partial = Liquid::PartialCache.load( partial = Liquid::PartialCache.load(
'my_partial', 'my_partial',
context: context, context: context,
parse_context: Liquid::ParseContext.new, parse_context: Liquid::ParseContext.new
) )
assert_equal('my partial body', partial.render) assert_equal('my partial body', partial.render)
@@ -129,12 +129,12 @@ class PartialCacheUnitTest < Minitest::Test
def test_cache_state_is_shared_for_subcontexts def test_cache_state_is_shared_for_subcontexts
parse_context = Liquid::ParseContext.new parse_context = Liquid::ParseContext.new
shared_file_system = StubFileSystem.new( shared_file_system = StubFileSystem.new(
'my_partial' => 'my shared value', 'my_partial' => 'my shared value'
) )
context = Liquid::Context.build( context = Liquid::Context.build(
registers: Liquid::Registers.new( registers: Liquid::Registers.new(
file_system: shared_file_system, file_system: shared_file_system,
), )
) )
subcontext = context.new_isolated_subcontext subcontext = context.new_isolated_subcontext
@@ -144,57 +144,16 @@ class PartialCacheUnitTest < Minitest::Test
Liquid::PartialCache.load( Liquid::PartialCache.load(
'my_partial', 'my_partial',
context: context, context: context,
parse_context: parse_context, parse_context: parse_context
) )
Liquid::PartialCache.load( Liquid::PartialCache.load(
'my_partial', 'my_partial',
context: subcontext, context: subcontext,
parse_context: parse_context, parse_context: parse_context
) )
end end
assert_equal(1, shared_file_system.file_read_count) assert_equal(1, shared_file_system.file_read_count)
end end
def test_uses_template_name_from_template_factory
template_factory = StubTemplateFactory.new
context = Liquid::Context.build(
registers: {
file_system: StubFileSystem.new('my_partial' => 'my partial body'),
template_factory: template_factory,
},
)
partial = Liquid::PartialCache.load(
'my_partial',
context: context,
parse_context: Liquid::ParseContext.new,
)
assert_equal('some/path/my_partial', partial.name)
end
def test_includes_error_mode_into_template_cache
template_factory = StubTemplateFactory.new
context = Liquid::Context.build(
registers: {
file_system: StubFileSystem.new('my_partial' => 'my partial body'),
template_factory: template_factory,
},
)
[:lax, :warn, :strict].each do |error_mode|
Liquid::PartialCache.load(
'my_partial',
context: context,
parse_context: Liquid::ParseContext.new(error_mode: error_mode),
)
end
assert_equal(
["my_partial:lax", "my_partial:warn", "my_partial:strict"],
context.registers[:cached_partials].keys,
)
end
end end
-13
View File
@@ -1,7 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'test_helper' require 'test_helper'
require 'timeout'
class RegexpUnitTest < Minitest::Test class RegexpUnitTest < Minitest::Test
include Liquid include Liquid
@@ -38,22 +37,10 @@ class RegexpUnitTest < Minitest::Test
def test_variable_parser def test_variable_parser
assert_equal(['var'], 'var'.scan(VariableParser)) assert_equal(['var'], 'var'.scan(VariableParser))
assert_equal(['[var]'], '[var]'.scan(VariableParser))
assert_equal(['var', 'method'], 'var.method'.scan(VariableParser)) assert_equal(['var', 'method'], 'var.method'.scan(VariableParser))
assert_equal(['var', '[method]'], 'var[method]'.scan(VariableParser)) assert_equal(['var', '[method]'], 'var[method]'.scan(VariableParser))
assert_equal(['var', '[method]', '[0]'], 'var[method][0]'.scan(VariableParser)) assert_equal(['var', '[method]', '[0]'], 'var[method][0]'.scan(VariableParser))
assert_equal(['var', '["method"]', '[0]'], 'var["method"][0]'.scan(VariableParser)) assert_equal(['var', '["method"]', '[0]'], 'var["method"][0]'.scan(VariableParser))
assert_equal(['var', '[method]', '[0]', 'method'], 'var[method][0].method'.scan(VariableParser)) assert_equal(['var', '[method]', '[0]', 'method'], 'var[method][0].method'.scan(VariableParser))
end end
def test_variable_parser_with_large_input
Timeout.timeout(1) { assert_equal(['[var]'], '[var]'.scan(VariableParser)) }
very_long_string = "foo" * 1000
# valid dynamic lookup
Timeout.timeout(1) { assert_equal(["[#{very_long_string}]"], "[#{very_long_string}]".scan(VariableParser)) }
# invalid dynamic lookup with missing closing bracket
Timeout.timeout(1) { assert_equal([very_long_string], "[#{very_long_string}".scan(VariableParser)) }
end
end # RegexpTest end # RegexpTest
+1 -1
View File
@@ -50,7 +50,7 @@ class StrainerFactoryUnitTest < Minitest::Test
assert_match( assert_match(
/\ALiquid error: wrong number of arguments \((1 for 0|given 1, expected 0)\)\z/, /\ALiquid error: wrong number of arguments \((1 for 0|given 1, expected 0)\)\z/,
exception.message, exception.message
) )
source = AccessScopeFilters.instance_method(:public_filter).source_location source = AccessScopeFilters.instance_method(:public_filter).source_location
assert_equal(source.map(&:to_s), exception.backtrace[0].split(':')[0..1]) assert_equal(source.map(&:to_s), exception.backtrace[0].split(':')[0..1])
+2 -3
View File
@@ -58,9 +58,8 @@ class StrainerTemplateUnitTest < Minitest::Test
def test_add_filter_does_not_raise_when_module_overrides_previously_registered_method def test_add_filter_does_not_raise_when_module_overrides_previously_registered_method
with_global_filter do with_global_filter do
context = Context.new strainer = Context.new.strainer
context.add_filters([PublicMethodOverrideFilter]) strainer.class.add_filter(PublicMethodOverrideFilter)
strainer = context.strainer
assert(strainer.class.send(:filter_methods).include?('public_filter')) assert(strainer.class.send(:filter_methods).include?('public_filter'))
end end
end end
-9
View File
@@ -20,13 +20,4 @@ class TagUnitTest < Minitest::Test
tag = Tag.parse("some_tag", "", Tokenizer.new(""), ParseContext.new) tag = Tag.parse("some_tag", "", Tokenizer.new(""), ParseContext.new)
assert_equal('some_tag', tag.tag_name) assert_equal('some_tag', tag.tag_name)
end end
class CustomTag < Liquid::Tag
def render(_context); end
end
def test_tag_render_to_output_buffer_nil_value
custom_tag = CustomTag.parse("some_tag", "", Tokenizer.new(""), ParseContext.new)
assert_equal('some string', custom_tag.render_to_output_buffer(Context.new, "some string"))
end
end end
-202
View File
@@ -1,202 +0,0 @@
# frozen_string_literal: true
require 'test_helper'
class CommentTagUnitTest < Minitest::Test
def test_comment_inside_liquid_tag
assert_template_result("", <<~LIQUID.chomp)
{% liquid
if 1 != 1
comment
else
echo 123
endcomment
endif
%}
LIQUID
end
def test_does_not_parse_nodes_inside_a_comment
assert_template_result("", <<~LIQUID.chomp)
{% comment %}
{% if true %}
{% if ... %}
{%- for ? -%}
{% while true %}
{%
unless if
%}
{% endcase %}
{% endcomment %}
LIQUID
end
def test_allows_unclosed_tags
assert_template_result('', <<~LIQUID.chomp)
{% comment %}
{% if true %}
{% endcomment %}
LIQUID
end
def test_open_tags_in_comment
assert_template_result('', <<~LIQUID.chomp)
{% comment %}
{% assign a = 123 {% comment %}
{% endcomment %}
LIQUID
assert_raises(Liquid::SyntaxError) do
assert_template_result("", <<~LIQUID.chomp)
{% comment %}
{% assign foo = "1"
{% endcomment %}
LIQUID
end
assert_raises(Liquid::SyntaxError) do
assert_template_result("", <<~LIQUID.chomp)
{% comment %}
{% comment %}
{% invalid
{% endcomment %}
{% endcomment %}
LIQUID
end
assert_raises(Liquid::SyntaxError) do
assert_template_result("", <<~LIQUID.chomp)
{% comment %}
{% {{ {%- endcomment %}
LIQUID
end
end
def test_child_comment_tags_need_to_be_closed
assert_template_result("", <<~LIQUID.chomp)
{% comment %}
{% comment %}
{% comment %}{% endcomment %}
{% endcomment %}
{% endcomment %}
LIQUID
assert_raises(Liquid::SyntaxError) do
assert_template_result("", <<~LIQUID.chomp)
{% comment %}
{% comment %}
{% comment %}
{% endcomment %}
{% endcomment %}
LIQUID
end
end
def test_child_raw_tags_need_to_be_closed
assert_template_result("", <<~LIQUID.chomp)
{% comment %}
{% raw %}
{% endcomment %}
{% endraw %}
{% endcomment %}
LIQUID
assert_raises(Liquid::SyntaxError) do
Liquid::Template.parse(<<~LIQUID.chomp)
{% comment %}
{% raw %}
{% endcomment %}
{% endcomment %}
LIQUID
end
end
def test_error_line_number_is_correct
template = Liquid::Template.parse(<<~LIQUID.chomp, line_numbers: true)
{% comment %}
{% if true %}
{% endcomment %}
{{ errors.standard_error }}
LIQUID
output = template.render('errors' => ErrorDrop.new)
expected = <<~TEXT.chomp
Liquid error (line 4): standard error
TEXT
assert_equal(expected, output)
end
def test_comment_tag_delimiter_with_extra_strings
assert_template_result(
'',
<<~LIQUID.chomp,
{% comment %}
{% comment %}
{% endcomment
{% if true %}
{% endif %}
{% endcomment %}
LIQUID
)
end
def test_nested_comment_tag_with_extra_strings
assert_template_result(
'',
<<~LIQUID.chomp,
{% comment %}
{% comment
{% assign foo = 1 %}
{% endcomment
{% assign foo = 1 %}
{% endcomment %}
LIQUID
)
end
def test_ignores_delimiter_with_extra_strings
assert_template_result(
'',
<<~LIQUID.chomp,
{% if true %}
{% comment %}
{% commentXXXXX %}wut{% endcommentXXXXX %}
{% endcomment %}
{% endif %}
LIQUID
)
end
def test_delimiter_can_have_extra_strings
assert_template_result('', "{% comment %}123{% endcomment xyz %}")
assert_template_result('', "{% comment %}123{% endcomment\txyz %}")
assert_template_result('', "{% comment %}123{% endcomment\nxyz %}")
assert_template_result('', "{% comment %}123{% endcomment\n xyz endcomment %}")
assert_template_result('', "{%comment}{% assign a = 1 %}{%endcomment}{% endif %}")
end
def test_with_whitespace_control
assert_template_result("Hello!", " {%- comment -%}123{%- endcomment -%}Hello!")
assert_template_result("Hello!", "{%- comment -%}123{%- endcomment -%} Hello!")
assert_template_result("Hello!", " {%- comment -%}123{%- endcomment -%} Hello!")
assert_template_result("Hello!", <<~LIQUID.chomp)
{%- comment %}Whitespace control!{% endcomment -%}
Hello!
LIQUID
end
def test_dont_override_liquid_tag_whitespace_control
assert_template_result("Hello!World!", <<~LIQUID.chomp)
Hello!
{%- liquid
comment
this is inside a liquid tag
endcomment
-%}
World!
LIQUID
end
end
+21 -10
View File
@@ -36,6 +36,7 @@ class TemplateUnitTest < Minitest::Test
assert(Template.tags['custom'].equal?(original_klass)) assert(Template.tags['custom'].equal?(original_klass))
ensure ensure
Object.send(:remove_const, :CustomTag) Object.send(:remove_const, :CustomTag)
Template.tags.delete('custom')
Liquid.cache_classes = original_cache_setting Liquid.cache_classes = original_cache_setting
end end
@@ -45,26 +46,36 @@ class TemplateUnitTest < Minitest::Test
original_klass = Class.new original_klass = Class.new
Object.send(:const_set, :CustomTag, original_klass) Object.send(:const_set, :CustomTag, original_klass)
with_custom_tag('custom', CustomTag) do Template.register_tag('custom', CustomTag)
Object.send(:remove_const, :CustomTag)
new_klass = Class.new Object.send(:remove_const, :CustomTag)
Object.send(:const_set, :CustomTag, new_klass)
assert(Template.tags['custom'].equal?(new_klass)) new_klass = Class.new
end Object.send(:const_set, :CustomTag, new_klass)
assert(Template.tags['custom'].equal?(new_klass))
ensure ensure
Object.send(:remove_const, :CustomTag) Object.send(:remove_const, :CustomTag)
Template.tags.delete('custom')
Liquid.cache_classes = original_cache_setting Liquid.cache_classes = original_cache_setting
end end
class FakeTag; end class FakeTag; end
def test_tags_delete
Template.register_tag('fake', FakeTag)
assert_equal(FakeTag, Template.tags['fake'])
Template.tags.delete('fake')
assert_nil(Template.tags['fake'])
end
def test_tags_can_be_looped_over def test_tags_can_be_looped_over
with_custom_tag('fake', FakeTag) do Template.register_tag('fake', FakeTag)
result = Template.tags.map { |name, klass| [name, klass] } result = Template.tags.map { |name, klass| [name, klass] }
assert(result.include?(["fake", "TemplateUnitTest::FakeTag"])) assert(result.include?(["fake", "TemplateUnitTest::FakeTag"]))
end ensure
Template.tags.delete('fake')
end end
class TemplateSubclass < Liquid::Template class TemplateSubclass < Liquid::Template