Compare commits

..
Author SHA1 Message Date
Richard Monette 570d42ea99 add JSON encode and decode 2023-03-13 11:09:54 -04:00
75 changed files with 466 additions and 1597 deletions
+4 -5
View File
@@ -11,10 +11,9 @@ jobs:
strategy:
matrix:
entry:
- { ruby: 3.0, allowed-failure: false } # minimum supported
- { ruby: 3.2, allowed-failure: false }
- { ruby: 3.3, allowed-failure: false } # latest
- { ruby: ruby-head, allowed-failure: false }
- { ruby: 2.7, allowed-failure: false } # minimum supported
- { ruby: 3.2, allowed-failure: false } # latest
- { ruby: ruby-head, allowed-failure: true }
name: Test Ruby ${{ matrix.entry.ruby }}
steps:
- uses: actions/checkout@v3
@@ -22,7 +21,6 @@ jobs:
with:
ruby-version: ${{ matrix.entry.ruby }}
bundler-cache: true
bundler: latest
- run: bundle exec rake
continue-on-error: ${{ matrix.entry.allowed-failure }}
@@ -32,5 +30,6 @@ jobs:
- uses: actions/checkout@v3
- uses: ruby/setup-ruby@v1
with:
ruby-version: 2.7
bundler-cache: true
- run: bundle exec rake memory_profile:run
+2 -1
View File
@@ -4,6 +4,7 @@
pkg
*.rbc
.rvmrc
.ruby-version
Gemfile.lock
.bundle
.byebug_history
Gemfile.lock
+1
View File
@@ -10,6 +10,7 @@ Performance:
Enabled: true
AllCops:
TargetRubyVersion: 2.7
NewCops: disable
SuggestExtensions: false
Exclude:
-1
View File
@@ -1 +0,0 @@
3.3.4
+2 -4
View File
@@ -7,8 +7,6 @@ end
gemspec
gem "base64"
group :benchmark, :test do
gem 'benchmark-ips'
gem 'memory_profiler'
@@ -20,11 +18,11 @@ group :benchmark, :test do
end
group :test do
gem 'rubocop', '~> 1.61.0'
gem 'rubocop', '~> 1.44.0'
gem 'rubocop-shopify', '~> 2.12.0', require: false
gem 'rubocop-performance', require: false
platform :mri, :truffleruby do
gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'main'
gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'master'
end
end
-4
View File
@@ -1,9 +1,5 @@
# Liquid Change Log
## 5.5.0 2024-03-21
Please reference the GitHub release for more information.
## 5.4.0 2022-07-29
### 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)
# Liquid template engine
* [Contributing guidelines](CONTRIBUTING.md)
* [Version history](History.md)
* [Liquid documentation from Shopify](https://shopify.dev/docs/api/liquid)
* [Liquid documentation from Shopify](https://shopify.dev/docs/docs/api/liquid)
* [Liquid Wiki at GitHub](https://github.com/Shopify/liquid/wiki)
* [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"
```
### 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
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:
```ruby
Liquid::Environment.default.error_mode = :strict
Liquid::Environment.default.error_mode = :strict # Raises a SyntaxError when invalid syntax is used
Liquid::Environment.default.error_mode = :warn # Adds strict errors to template.errors but continues as normal
Liquid::Environment.default.error_mode = :lax # The default mode, accepts almost anything.
Liquid::Template.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::Template.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`:
@@ -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.
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
ruby "./performance/benchmark.rb strict"
end
desc "Run unit benchmarks"
task :unit do
Dir["./performance/unit/*_benchmark.rb"].each do |file|
puts "🧪 Running #{file}"
ruby file
end
end
end
namespace :profile do
+14 -10
View File
@@ -44,20 +44,13 @@ module Liquid
VariableParser = /\[(?>[^\[\]]+|\g<0>)*\]|#{VariableSegment}+\??/o
RAISE_EXCEPTION_LAMBDA = ->(_e) { raise }
singleton_class.send(:attr_accessor, :cache_classes)
self.cache_classes = true
end
require "liquid/version"
require "liquid/deprecations"
require "liquid/const"
require 'liquid/standardfilters'
require 'liquid/file_system'
require 'liquid/parser_switching'
require 'liquid/tag'
require 'liquid/block'
require 'liquid/parse_tree_visitor'
require 'liquid/interrupts'
require 'liquid/tags'
require "liquid/environment"
require 'liquid/lexer'
require 'liquid/parser'
require 'liquid/i18n'
@@ -68,16 +61,23 @@ require 'liquid/extensions'
require 'liquid/errors'
require 'liquid/interrupts'
require 'liquid/strainer_template'
require 'liquid/strainer_factory'
require 'liquid/expression'
require 'liquid/context'
require 'liquid/parser_switching'
require 'liquid/tag'
require 'liquid/tag/disabler'
require 'liquid/tag/disableable'
require 'liquid/block'
require 'liquid/block_body'
require 'liquid/document'
require 'liquid/variable'
require 'liquid/variable_lookup'
require 'liquid/range_lookup'
require 'liquid/file_system'
require 'liquid/resource_limits'
require 'liquid/template'
require 'liquid/standardfilters'
require 'liquid/condition'
require 'liquid/utils'
require 'liquid/tokenizer'
@@ -86,3 +86,7 @@ require 'liquid/partial_cache'
require 'liquid/usage'
require 'liquid/registers'
require 'liquid/template_factory'
# Load all the tags of the standard library
#
Dir["#{__dir__}/liquid/tags/*.rb"].each { |f| require f }
+8 -18
View File
@@ -6,7 +6,6 @@ module Liquid
class BlockBody
LiquidTagToken = /\A\s*(#{TagName})\s*(.*?)\z/o
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
WhitespaceOrNothing = /\A\s*\z/
TAGSTART = "{%"
@@ -46,13 +45,7 @@ module Liquid
end
tag_name = Regexp.last_match(1)
markup = Regexp.last_match(2)
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))
unless (tag = registered_tags[tag_name])
# end parsing if we reach an unknown tag and let the caller decide
# determine how to proceed
return yield tag_name, markup
@@ -147,7 +140,7 @@ module Liquid
next
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
# determine how to proceed
return yield tag_name, markup
@@ -246,17 +239,10 @@ module Liquid
end
def create_variable(token, parse_context)
if token.end_with?("}}")
i = 2
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)
if token =~ ContentOfVariable
markup = Regexp.last_match(1)
return Variable.new(markup, parse_context)
end
BlockBody.raise_missing_variable_terminator(token, parse_context)
end
@@ -269,5 +255,9 @@ module Liquid
def raise_missing_variable_terminator(token, parse_context)
BlockBody.raise_missing_variable_terminator(token, parse_context)
end
def registered_tags
Template.tags
end
end
end
+2 -5
View File
@@ -24,9 +24,6 @@ module Liquid
else
false
end
rescue Encoding::CompatibilityError
# "✅".b.include?("✅") raises Encoding::CompatibilityError despite being materially equal
left.b.include?(right.b)
end,
}
@@ -72,9 +69,9 @@ module Liquid
case condition.child_relation
when :or
break if Liquid::Utils.to_liquid_value(result)
break if result
when :and
break unless Liquid::Utils.to_liquid_value(result)
break unless result
else
break
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 -17
View File
@@ -15,25 +15,24 @@ module Liquid
# context['bob'] #=> nil class Context
class Context
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
def self.build(environment: Environment.default, environments: {}, outer_scope: {}, registers: {}, rethrow_errors: false, resource_limits: nil, static_environments: {}, &block)
new(environments, outer_scope, registers, rethrow_errors, resource_limits, static_environments, environment, &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)
end
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil, static_environments = {}, environment = Environment.default)
@environment = environment
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil, static_environments = {})
@environments = [environments]
@environments.flatten!
@static_environments = [static_environments].flatten(1).freeze
@scopes = [outer_scope || {}]
@scopes = [(outer_scope || {})]
@registers = registers.is_a?(Registers) ? registers : Registers.new(registers)
@errors = []
@partial = 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
@interrupts = []
@filters = []
@@ -41,10 +40,10 @@ module Liquid
@disabled_tags = {}
@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
self.exception_renderer = environment.exception_renderer
self.exception_renderer = Template.default_exception_renderer
if rethrow_errors
self.exception_renderer = Liquid::RAISE_EXCEPTION_LAMBDA
end
@@ -61,7 +60,7 @@ module Liquid
end
def strainer
@strainer ||= @environment.create_strainer(self, @filters)
@strainer ||= StrainerFactory.create(self, @filters)
end
# Adds filters to this context.
@@ -143,7 +142,6 @@ module Liquid
check_overflow
self.class.build(
environment: @environment,
resource_limits: resource_limits,
static_environments: static_environments,
registers: Registers.new(registers),
@@ -199,14 +197,10 @@ module Liquid
try_variable_find_in_environments(key, raise_on_not_found: raise_on_not_found)
end
# update variable's context before invoking #to_liquid
variable = variable.to_liquid
variable.context = self if variable.respond_to?(:context=)
liquid_variable = variable.to_liquid
liquid_variable.context = self if variable != liquid_variable && liquid_variable.respond_to?(:context=)
liquid_variable
variable
end
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
-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 = 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 = Tags::STANDARD_TAGS.dup
@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
ArgumentError = Class.new(Error)
ContextError = Class.new(Error)
FileSystemError = Class.new(Error)
StandardError = Class.new(Error)
SyntaxError = Class.new(Error)
StackLevelError = Class.new(Error)
MemoryError = Class.new(Error)
ZeroDivisionError = Class.new(Error)
FloatDomainError = Class.new(Error)
UndefinedVariable = Class.new(Error)
UndefinedDropMethod = Class.new(Error)
UndefinedFilter = Class.new(Error)
MethodOverrideError = Class.new(Error)
DisabledError = Class.new(Error)
InternalError = Class.new(Error)
TemplateEncodingError = Class.new(Error)
ArgumentError = Class.new(Error)
ContextError = Class.new(Error)
FileSystemError = Class.new(Error)
StandardError = Class.new(Error)
SyntaxError = Class.new(Error)
StackLevelError = Class.new(Error)
MemoryError = Class.new(Error)
ZeroDivisionError = Class.new(Error)
FloatDomainError = Class.new(Error)
UndefinedVariable = Class.new(Error)
UndefinedDropMethod = Class.new(Error)
UndefinedFilter = Class.new(Error)
MethodOverrideError = Class.new(Error)
DisabledError = Class.new(Error)
InternalError = Class.new(Error)
end
+1 -177
View File
@@ -1,9 +1,8 @@
# frozen_string_literal: true
require "strscan"
module Liquid
class Lexer1
class Lexer
SPECIALS = {
'|' => :pipe,
'.' => :dot,
@@ -59,179 +58,4 @@ module Liquid
@output << [:end_of_string]
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
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*/
SINGLE_COMPARISON_TOKENS = [].tap do |table|
table["<".ord] = COMPARISON_LESS_THAN
table[">".ord] = COMPARISON_GREATER_THAN
end
TWO_CHARS_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
end
COMPARISON_JUMP_TABLE = [].tap do |table|
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?
start_pos = @ss.pos
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 (peeked_byte = @ss.peek_byte) && NUMBER_TABLE[peeked_byte]
@ss.pos -= 1
@output << [:number, @ss.scan(NUMBER_LITERAL)]
else
@output << special
end
else
@output << special
end
elsif (sub_table = TWO_CHARS_COMPARISON_JUMP_TABLE[peeked])
@ss.scan_byte
if (peeked_byte = @ss.peek_byte) && (found = sub_table[peeked_byte])
@output << found
@ss.scan_byte
else
raise_syntax_error(start_pos)
end
elsif (sub_table = COMPARISON_JUMP_TABLE[peeked])
@ss.scan_byte
if (peeked_byte = @ss.peek_byte) && (found = sub_table[peeked_byte])
@output << found
@ss.scan_byte
else
@output << SINGLE_COMPARISON_TOKENS[peeked]
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" && @output.last&.first != :dot
COMPARISON_CONTAINS
else
[type, t]
end
else
raise_syntax_error(start_pos)
end
end
end
# rubocop:enable Metrics/BlockNesting
@output << EOS
end
def raise_syntax_error(start_pos)
@ss.pos = start_pos
# the character could be a UTF-8 character, use getch to get all the bytes
raise SyntaxError, "Unexpected character #{@ss.getch}"
end
end
Lexer = StringScanner.instance_methods.include?(:scan_byte) ? Lexer2 : Lexer1
end
-1
View File
@@ -15,7 +15,6 @@
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"
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"
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"
+3 -4
View File
@@ -3,10 +3,9 @@
module Liquid
class ParseContext
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)
@environment = options.fetch(:environment, Environment.default)
def initialize(options = {})
@template_options = options ? options.dup : {}
@locale = @template_options[:locale] ||= I18n.new
@@ -36,7 +35,7 @@ module Liquid
@partial = value
@options = value ? partial_options : @template_options
@error_mode = @options[:error_mode] || @environment.error_mode
@error_mode = @options[:error_mode] || Template.error_mode
end
def partial_options
+1 -1
View File
@@ -36,7 +36,7 @@ module Liquid
protected
def children
@node.respond_to?(:nodelist) ? Array(@node.nodelist) : Const::EMPTY_ARRAY
@node.respond_to?(:nodelist) ? Array(@node.nodelist) : []
end
end
end
+1 -1
View File
@@ -53,7 +53,7 @@ module Liquid
str = consume
str << variable_lookups
when :open_square
str = consume.dup
str = consume
str << expression
str << consume(:close_square)
str << variable_lookups
+2 -3
View File
@@ -4,8 +4,7 @@ module Liquid
class PartialCache
def self.load(template_name, context:, parse_context:)
cached_partials = context.registers[:cached_partials]
cache_key = "#{template_name}:#{parse_context.error_mode}"
cached = cached_partials[cache_key]
cached = cached_partials[template_name]
return cached if cached
file_system = context.registers[:file_system]
@@ -25,7 +24,7 @@ module Liquid
partial.name ||= template_name
cached_partials[cache_key] = partial
cached_partials[template_name] = partial
ensure
parse_context.partial = false
end
+31 -51
View File
@@ -3,6 +3,7 @@
require 'cgi'
require 'base64'
require 'bigdecimal'
require 'json'
module Liquid
module StandardFilters
@@ -29,19 +30,6 @@ module Liquid
)
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_type filter
# @liquid_category array
@@ -82,7 +70,7 @@ module Liquid
# @liquid_type filter
# @liquid_category string
# @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_return [string]
def capitalize(input)
@@ -112,6 +100,30 @@ module Liquid
input.to_s.gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE)
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# JSON encodes the input.
# @liquid_syntax variable | json_encode.
# @liquid_return [string]
def json_encode(input)
JSON.generate(input)
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# JSON decodes an input.
# @liquid_syntax string | json_decode.
# @liquid_return [string]
def json_decode(input)
JSON.parse(input.to_s) unless input.nil?
rescue JSON::ParserError, TypeError => e
raise Liquid::ArgumentError, e.message
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
@@ -163,8 +175,7 @@ module Liquid
# @liquid_syntax string | base64_decode
# @liquid_return [string]
def base64_decode(input)
input = input.to_s
StandardFilters.try_coerce_encoding(Base64.strict_decode64(input), encoding: input.encoding)
Base64.strict_decode64(input.to_s)
rescue ::ArgumentError
raise Liquid::ArgumentError, "invalid base64 provided to base64_decode"
end
@@ -188,8 +199,7 @@ module Liquid
# @liquid_syntax string | base64_url_safe_decode
# @liquid_return [string]
def base64_url_safe_decode(input)
input = input.to_s
StandardFilters.try_coerce_encoding(Base64.urlsafe_decode64(input), encoding: input.encoding)
Base64.urlsafe_decode64(input.to_s)
rescue ::ArgumentError
raise Liquid::ArgumentError, "invalid base64 provided to base64_url_safe_decode"
end
@@ -877,43 +887,13 @@ module Liquid
# - [`nil`](/docs/api/liquid/basics#nil)
# @liquid_syntax variable | default: variable
# @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 = {})
options = {} unless options.is_a?(Hash)
false_check = options['allow_false'] ? input.nil? : !Liquid::Utils.to_liquid_value(input)
false_check || (input.respond_to?(:empty?) && input.empty?) ? default_value : input
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
attr_reader :context
@@ -944,8 +924,6 @@ module Liquid
def nil_safe_casecmp(a, b)
if !a.nil? && !b.nil?
a.to_s.casecmp(b.to_s)
elsif a.nil? && b.nil?
0
else
a.nil? ? 1 : -1
end
@@ -1001,4 +979,6 @@ module Liquid
end
end
end
Template.register_filter(StandardFilters)
end
+41
View File
@@ -0,0 +1,41 @@
# frozen_string_literal: true
module Liquid
# StrainerFactory is the factory for the filters system.
module StrainerFactory
extend self
def add_global_filter(filter)
strainer_class_cache.clear
GlobalCache.add_filter(filter)
end
def create(context, filters = [])
strainer_from_cache(filters).new(context)
end
def global_filter_names
GlobalCache.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
+1 -5
View File
@@ -1,8 +1,5 @@
# frozen_string_literal: true
require 'liquid/tag/disabler'
require 'liquid/tag/disableable'
module Liquid
class Tag
attr_reader :nodelist, :tag_name, :line_number, :parse_context
@@ -57,8 +54,7 @@ module Liquid
# of the `render_to_output_buffer` method will become the default and the `render`
# method will be removed.
def render_to_output_buffer(context, output)
render_result = render(context)
output << render_result if render_result
output << render(context)
output
end
-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
+2
View File
@@ -72,4 +72,6 @@ module Liquid
end
end
end
Template.register_tag('assign', Assign)
end
+2
View File
@@ -26,4 +26,6 @@ module Liquid
output
end
end
Template.register_tag('break', Break)
end
+2
View File
@@ -39,4 +39,6 @@ module Liquid
true
end
end
Template.register_tag('capture', Capture)
end
+2
View File
@@ -123,4 +123,6 @@ module Liquid
end
end
end
Template.register_tag('case', Case)
end
+2 -59
View File
@@ -25,64 +25,7 @@ module Liquid
def blank?
true
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
Template.register_tag('comment', Comment)
end
+2
View File
@@ -17,4 +17,6 @@ module Liquid
output
end
end
Template.register_tag('continue', Continue)
end
+2 -6
View File
@@ -26,20 +26,14 @@ module Liquid
when NamedSyntax
@variables = variables_from_string(Regexp.last_match(2))
@name = parse_expression(Regexp.last_match(1))
@is_named = true
when SimpleSyntax
@variables = variables_from_string(markup)
@name = @variables.to_s
@is_named = !@name.match?(/\w+:0x\h{8}/)
else
raise SyntaxError, options[:locale].t("errors.syntax.cycle")
end
end
def named?
@is_named
end
def render_to_output_buffer(context, output)
context.registers[:cycle] ||= {}
@@ -78,4 +72,6 @@ module Liquid
end
end
end
Template.register_tag('cycle', Cycle)
end
+2
View File
@@ -35,4 +35,6 @@ module Liquid
output
end
end
Template.register_tag('decrement', Decrement)
end
+2
View File
@@ -36,4 +36,6 @@ module Liquid
end
end
end
Template.register_tag('echo', Echo)
end
+2
View File
@@ -201,4 +201,6 @@ module Liquid
end
end
end
Template.register_tag('for', For)
end
+3 -1
View File
@@ -111,7 +111,7 @@ module Liquid
def parse_binary_comparisons(p)
condition = parse_comparison(p)
first_condition = condition
while (op = p.id?('and') || p.id?('or'))
while (op = (p.id?('and') || p.id?('or')))
child_condition = parse_comparison(p)
condition.send(op, child_condition)
condition = child_condition
@@ -135,4 +135,6 @@ module Liquid
end
end
end
Template.register_tag('if', If)
end
+2
View File
@@ -14,4 +14,6 @@ module Liquid
output
end
end
Template.register_tag('ifchanged', Ifchanged)
end
+2
View File
@@ -110,4 +110,6 @@ module Liquid
end
end
end
Template.register_tag('include', Include)
end
+2
View File
@@ -35,4 +35,6 @@ module Liquid
output
end
end
Template.register_tag('increment', Increment)
end
+2
View File
@@ -25,4 +25,6 @@ module Liquid
true
end
end
Template.register_tag('#', InlineComment)
end
+4 -2
View File
@@ -14,6 +14,7 @@ module Liquid
# @liquid_syntax_keyword expression The expression to be output without being rendered.
class Raw < Block
Syntax = /\A\s*\z/
FullTokenPossiblyInvalid = /\A(.*)#{TagStart}#{WhitespaceControl}?\s*(\w+)\s*(.*)?#{WhitespaceControl}?#{TagEnd}\z/om
def initialize(tag_name, markup, parse_context)
super
@@ -24,8 +25,7 @@ module Liquid
def parse(tokens)
@body = +''
while (token = tokens.shift)
if token =~ BlockBody::FullTokenPossiblyInvalid && block_delimiter == Regexp.last_match(2)
parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
if token =~ FullTokenPossiblyInvalid && block_delimiter == Regexp.last_match(2)
@body << Regexp.last_match(1) if Regexp.last_match(1) != ""
return
end
@@ -56,4 +56,6 @@ module Liquid
end
end
end
Template.register_tag('raw', Raw)
end
+2
View File
@@ -108,4 +108,6 @@ module Liquid
end
end
end
Template.register_tag('render', Render)
end
+2 -6
View File
@@ -65,12 +65,6 @@ module Liquid
super
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
output << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">"
end
@@ -97,4 +91,6 @@ module Liquid
raise Liquid::ArgumentError, "invalid integer"
end
end
Template.register_tag('tablerow', TableRow)
end
+2
View File
@@ -44,4 +44,6 @@ module Liquid
output
end
end
Template.register_tag('unless', Unless)
end
+60 -61
View File
@@ -18,6 +18,42 @@ module Liquid
attr_accessor :root, :name
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
class << self
@@ -25,83 +61,52 @@ module Liquid
# :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.
# :strict will enforce correct syntax.
def error_mode=(mode)
Deprecations.warn("Template.error_mode=", "Environment#error_mode=")
Environment.default.error_mode = mode
attr_accessor :error_mode
Template.error_mode = :lax
attr_accessor :default_exception_renderer
Template.default_exception_renderer = lambda do |exception|
exception
end
def error_mode
Environment.default.error_mode
end
attr_accessor :file_system
Template.file_system = BlankFileSystem.new
def default_exception_renderer=(renderer)
Deprecations.warn("Template.default_exception_renderer=", "Environment#exception_renderer=")
Environment.default.exception_renderer = renderer
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
attr_accessor :tags
Template.tags = TagRegistry.new
private :tags=
def register_tag(name, klass)
Deprecations.warn("Template.register_tag", "Environment#register_tag")
Environment.default.register_tag(name, klass)
tags[name.to_s] = klass
end
# Pass a module with filter methods which should be available
# to all liquid views. Good for registering the standard library
def register_filter(mod)
Deprecations.warn("Template.register_filter", "Environment#register_filter")
Environment.default.register_filter(mod)
StrainerFactory.add_global_filter(mod)
end
private def default_resource_limits=(limits)
Deprecations.warn("Template.default_resource_limits=", "Environment#default_resource_limits=")
Environment.default.default_resource_limits = limits
end
def default_resource_limits
Environment.default.default_resource_limits
end
attr_accessor :default_resource_limits
Template.default_resource_limits = {}
private :default_resource_limits=
# creates a new <tt>Template</tt> object from liquid source code
# To enable profiling, pass in <tt>profile: true</tt> as an option.
# See Liquid::Profiler for more information
def parse(source, options = {})
environment = options[:environment] || Environment.default
new(environment: environment).parse(source, options)
new.parse(source, options)
end
end
def initialize(environment: Environment.default)
@environment = environment
def initialize
@rethrow_errors = false
@resource_limits = ResourceLimits.new(environment.default_resource_limits)
@resource_limits = ResourceLimits.new(Template.default_resource_limits)
end
# Parse source code.
# Returns self for easy chaining
def parse(source, 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)
@root = Document.parse(tokenizer, parse_context)
self
@@ -151,11 +156,11 @@ module Liquid
c
when Liquid::Drop
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
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
Context.new(assigns, instance_assigns, registers, @rethrow_errors, @resource_limits, {}, @environment)
Context.new(assigns, instance_assigns, registers, @rethrow_errors, @resource_limits)
else
raise ArgumentError, "Expected Hash or Liquid::Context as parameter"
end
@@ -215,14 +220,8 @@ module Liquid
@options = options
@profiling = profiling
@line_numbers = options[:line_numbers] || @profiling
parse_context = if options.is_a?(ParseContext)
options
else
opts = options.key?(:environment) ? options : options.merge(environment: @environment)
ParseContext.new(opts)
end
@warnings = parse_context.warnings
parse_context = options.is_a?(ParseContext) ? options : ParseContext.new(options)
@warnings = parse_context.warnings
parse_context
end
+1 -1
View File
@@ -5,7 +5,7 @@ module Liquid
attr_reader :line_number, :for_liquid_tag
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)
@for_liquid_tag = for_liquid_tag
@offset = 0
+7 -13
View File
@@ -68,7 +68,7 @@ module Liquid
@name = parse_context.parse_expression(p.expression)
while p.consume?(:pipe)
filtername = p.consume(:id)
filterargs = p.consume?(:colon) ? parse_filterargs(p) : Const::EMPTY_ARRAY
filterargs = p.consume?(:colon) ? parse_filterargs(p) : []
@filters << parse_filter_expressions(filtername, filterargs)
end
p.consume(:end_of_string)
@@ -95,21 +95,15 @@ module Liquid
def render_to_output_buffer(context, output)
obj = render(context)
render_obj_to_output(obj, output)
output
end
def render_obj_to_output(obj, output)
case obj
when NilClass
# Do nothing
when Array
obj.each do |o|
render_obj_to_output(o, output)
end
when
if obj.is_a?(Array)
output << obj.join
elsif obj.nil?
else
output << obj.to_s
end
output
end
def disabled?(_context)
+1 -1
View File
@@ -2,5 +2,5 @@
# frozen_string_literal: true
module Liquid
VERSION = "5.6.0.rc1"
VERSION = "5.4.0"
end
+1 -4
View File
@@ -17,7 +17,7 @@ Gem::Specification.new do |s|
s.license = "MIT"
# s.description = "A secure, non-evaling end user template engine with aesthetic markup."
s.required_ruby_version = ">= 3.0.0"
s.required_ruby_version = ">= 2.7.0"
s.required_rubygems_version = ">= 1.3.7"
s.metadata['allowed_push_host'] = 'https://rubygems.org'
@@ -28,9 +28,6 @@ Gem::Specification.new do |s|
s.require_path = "lib"
s.add_dependency("strscan")
s.add_dependency("bigdecimal")
s.add_development_dependency('rake', '~> 13.0')
s.add_development_dependency('minitest')
end
+1 -3
View File
@@ -3,9 +3,7 @@
require 'benchmark/ips'
require_relative 'theme_runner'
RubyVM::YJIT.enable if defined?(RubyVM::YJIT)
Liquid::Environment.default.error_mode = ARGV.first.to_sym if ARGV.first
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new
Benchmark.ips do |x|
+7 -8
View File
@@ -11,12 +11,11 @@ require_relative 'shop_filter'
require_relative 'tag_filter'
require_relative 'weight_filter'
default_environment = Liquid::Environment.default
default_environment.register_tag('paginate', Paginate)
default_environment.register_tag('form', CommentForm)
Liquid::Template.register_tag('paginate', Paginate)
Liquid::Template.register_tag('form', CommentForm)
default_environment.register_filter(JsonFilter)
default_environment.register_filter(MoneyFilter)
default_environment.register_filter(WeightFilter)
default_environment.register_filter(ShopFilter)
default_environment.register_filter(TagFilter)
Liquid::Template.register_filter(JsonFilter)
Liquid::Template.register_filter(MoneyFilter)
Liquid::Template.register_filter(WeightFilter)
Liquid::Template.register_filter(ShopFilter)
Liquid::Template.register_filter(TagFilter)
-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
-52
View File
@@ -36,24 +36,6 @@ class Category
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
attr_accessor :category, :context
@@ -653,40 +635,6 @@ class ContextTest < Minitest::Test
assert_equal(:my_value, c.registers[:my_register])
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
def test_new_isolated_context_inherits_parent_environment
global_environment = Liquid::Environment.build(tags: {})
context = Context.build(environment: global_environment)
subcontext = context.new_isolated_subcontext
assert_equal(global_environment, subcontext.environment)
end
def test_newly_built_context_inherits_parent_environment
global_environment = Liquid::Environment.build(tags: {})
context = Context.build(environment: global_environment)
assert_equal(global_environment, context.environment)
assert(context.environment.tags.each.to_a.empty?)
end
private
def assert_no_object_allocations
+14 -22
View File
@@ -203,34 +203,20 @@ class ErrorHandlingTest < Minitest::Test
end
def test_setting_default_exception_renderer
old_exception_renderer = Liquid::Template.default_exception_renderer
exceptions = []
default_exception_renderer = ->(e) {
Liquid::Template.default_exception_renderer = ->(e) {
exceptions << e
''
}
env = Liquid::Environment.build(exception_renderer: default_exception_renderer)
template = Liquid::Template.parse('This is a runtime error: {{ errors.argument_error }}', environment: env)
template = Liquid::Template.parse('This is a runtime error: {{ errors.argument_error }}')
output = template.render('errors' => ErrorDrop.new)
assert_equal('This is a runtime error: ', output)
assert_equal([Liquid::ArgumentError], template.errors.map(&:class))
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))
ensure
Liquid::Template.default_exception_renderer = old_exception_renderer if old_exception_renderer
end
def test_exception_renderer_exposing_non_liquid_error
@@ -256,10 +242,16 @@ class ErrorHandlingTest < Minitest::Test
end
def test_included_template_name_with_line_numbers
environment = Liquid::Environment.build(file_system: TestFileSystem.new)
template = Liquid::Template.parse("Argument error:\n{% include 'product' %}", line_numbers: true, environment: environment)
page = template.render('errors' => ErrorDrop.new)
old_file_system = Liquid::Template.file_system
begin
Liquid::Template.file_system = TestFileSystem.new
template = Liquid::Template.parse("Argument error:\n{% include 'product' %}", line_numbers: true)
page = template.render('errors' => ErrorDrop.new)
ensure
Liquid::Template.file_system = old_file_system
end
assert_equal("Argument error:\nLiquid error (product line 1): argument error", page)
assert_equal("product", template.errors.first.template_name)
end
-12
View File
@@ -131,16 +131,4 @@ class ParsingQuirksTest < Minitest::Test
def test_contains_in_id
assert_template_result(' YES ', '{% if containsallshipments == true %} YES {% endif %}', { 'containsallshipments' => true })
end
def test_incomplete_expression
with_error_mode(:lax) do
assert_template_result("false", "{% liquid assign foo = false -\n%}{{ foo }}")
assert_template_result("false", "{% liquid assign foo = false >\n%}{{ foo }}")
assert_template_result("false", "{% liquid assign foo = false <\n%}{{ foo }}")
assert_template_result("false", "{% liquid assign foo = false =\n%}{{ foo }}")
assert_template_result("false", "{% liquid assign foo = false !\n%}{{ foo }}")
assert_template_result("false", "{% liquid assign foo = false 1\n%}{{ foo }}")
assert_template_result("false", "{% liquid assign foo = false a\n%}{{ foo }}")
end
end
end # ParsingQuirksTest
+1 -1
View File
@@ -33,7 +33,7 @@ class ProfilerTest < Minitest::Test
end
def setup
Liquid::Environment.default.file_system = ProfilingFileSystem.new
Liquid::Template.file_system = ProfilingFileSystem.new
end
def test_template_allows_flagging_profiling
+28 -127
View File
@@ -32,7 +32,7 @@ class TestDrop < Liquid::Drop
attr_reader :value
def registers
"{#{@value.inspect}=>#{@context.registers[@value].inspect}}"
{ @value => @context.registers[@value] }
end
end
@@ -170,23 +170,37 @@ class StandardFiltersTest < Minitest::Test
assert_equal('&lt;strong&gt;Hulk&lt;/strong&gt;', @filters.escape_once('&lt;strong&gt;Hulk</strong>'))
end
def test_json_encode
assert_equal('{"name":"Fizz Buzz","id":1235}', @filters.json_encode({"name"=>"Fizz Buzz", "id"=>1235}))
assert_equal('[1,2,3,5]', @filters.json_encode([1, 2, 3, 5]))
assert_equal("null", @filters.json_encode(nil))
assert_equal("1", @filters.json_encode(1))
end
def test_json_decode
assert_equal({"name"=>"Fizz Buzz", "id"=>1235}, @filters.json_decode('{"name":"Fizz Buzz","id":1235}'))
assert_template_result('{"foo"=>{"bar"=>[1, 2, 3]}}', '{% assign json_result = json_data | json_decode %}{{ json_result }}', {"json_data" => '{"foo":{"bar":[1,2,3]}}'})
assert_template_result('{"bar"=>[1, 2, 3]}', '{% assign json_result = json_data | json_decode %}{{ json_result.foo }}', {"json_data" => '{"foo":{"bar":[1,2,3]}}'})
assert_template_result('123', '{% assign json_result = json_data | json_decode %}{{ json_result.foo.bar }}', {"json_data" => '{"foo":{"bar":[1,2,3]}}'})
assert_template_result('123', '{% assign json_result = json_data | json_decode %}{{ json_result.foo["bar"] }}', {"json_data" => '{"foo":{"bar":[1,2,3]}}'})
assert_template_result('1', '{% assign json_result = json_data | json_decode %}{{ json_result.foo.bar[0] }}', {"json_data" => '{"foo":{"bar":[1,2,3]}}'})
assert_equal(1, @filters.json_decode(1))
assert_nil(@filters.json_decode(nil))
assert_raises(Liquid::ArgumentError) do
@filters.json_decode('{"name":"BrokenFragment')
end
end
def test_base64_encode
assert_equal('b25lIHR3byB0aHJlZQ==', @filters.base64_encode('one two three'))
assert_equal('', @filters.base64_encode(nil))
end
def test_base64_decode
decoded = @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)
assert_equal('one two three', @filters.base64_decode('b25lIHR3byB0aHJlZQ=='))
exception = assert_raises(Liquid::ArgumentError) do
@filters.base64_decode("invalidbase64")
@@ -204,21 +218,10 @@ class StandardFiltersTest < Minitest::Test
end
def test_base64_url_safe_decode
decoded = @filters.base64_url_safe_decode('YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXogQUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVogMTIzNDU2Nzg5MCAhQCMkJV4mKigpLT1fKy8_Ljo7W117fVx8')
assert_equal(
'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
@filters.base64_url_safe_decode("invalidbase64")
end
@@ -331,8 +334,8 @@ class StandardFiltersTest < Minitest::Test
{ "price" => "1", "handle" => "gamma" },
{ "price" => 2, "handle" => "epsilon" },
{ "price" => "4", "handle" => "alpha" },
{ "handle" => "beta" },
{ "handle" => "delta" },
{ "handle" => "beta" },
]
assert_equal(expectation, @filters.sort_natural(input, "price"))
end
@@ -949,108 +952,6 @@ class StandardFiltersTest < Minitest::Test
assert_equal([{ "foo" => true }, { "foo" => "for sure" }], @filters.where(input, "foo"))
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
def with_timezone(tz)
+20 -17
View File
@@ -49,6 +49,14 @@ end
class IncludeTagTest < Minitest::Test
include Liquid
def setup
@default_file_system = Liquid::Template.file_system
end
def teardown
Liquid::Template.file_system = @default_file_system
end
def test_include_tag_looks_for_file_system_in_registers_first
assert_equal(
'from OtherFileSystem',
@@ -174,10 +182,10 @@ class IncludeTagTest < Minitest::Test
end
end
env = Liquid::Environment.build(file_system: infinite_file_system.new)
Liquid::Template.file_system = infinite_file_system.new
assert_raises(Liquid::StackLevelError) do
Template.parse("{% include 'loop' %}", environment: env).render!
Template.parse("{% include 'loop' %}").render!
end
end
@@ -206,10 +214,9 @@ class IncludeTagTest < Minitest::Test
def test_include_tag_caches_second_read_of_same_partial
file_system = CountingFileSystem.new
environment = Liquid::Environment.build(file_system: file_system)
assert_equal(
'from CountingFileSystemfrom CountingFileSystem',
Template.parse("{% include 'pick_a_source' %}{% include 'pick_a_source' %}", environment: environment).render!({}, registers: { file_system: file_system }),
Template.parse("{% include 'pick_a_source' %}{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system }),
)
assert_equal(1, file_system.count)
end
@@ -264,27 +271,26 @@ class IncludeTagTest < Minitest::Test
end
def test_does_not_add_error_in_strict_mode_for_missing_variable
env = Liquid::Environment.build(file_system: TestFileSystem.new)
Liquid::Template.file_system = TestFileSystem.new
a = Liquid::Template.parse(' {% include "nested_template" %}', environment: env)
a = Liquid::Template.parse(' {% include "nested_template" %}')
a.render!
assert_empty(a.errors)
end
def test_passing_options_to_included_templates
env = Liquid::Environment.build(file_system: TestFileSystem.new)
Liquid::Template.file_system = TestFileSystem.new
assert_raises(Liquid::SyntaxError) do
Template.parse("{% include template %}", error_mode: :strict, environment: env).render!("template" => '{{ "X" || downcase }}')
Template.parse("{% include template %}", error_mode: :strict).render!("template" => '{{ "X" || downcase }}')
end
with_error_mode(:lax) do
assert_equal('x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: true, environment: env).render!("template" => '{{ "X" || downcase }}'))
assert_equal('x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: true).render!("template" => '{{ "X" || downcase }}'))
end
assert_raises(Liquid::SyntaxError) do
Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:locale], environment: env).render!("template" => '{{ "X" || downcase }}')
Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:locale]).render!("template" => '{{ "X" || downcase }}')
end
with_error_mode(:lax) do
assert_equal('x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:error_mode], environment: env).render!("template" => '{{ "X" || downcase }}'))
assert_equal('x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:error_mode]).render!("template" => '{{ "X" || downcase }}'))
end
end
@@ -335,11 +341,8 @@ class IncludeTagTest < Minitest::Test
end
def test_including_with_strict_variables
env = Liquid::Environment.build(
file_system: StubFileSystem.new('simple' => 'simple'),
)
template = Liquid::Template.parse("{% include 'simple' %}", error_mode: :warn, environment: env)
Liquid::Template.file_system = StubFileSystem.new({ "simple" => "simple" })
template = Liquid::Template.parse("{% include 'simple' %}", error_mode: :warn)
template.render(nil, strict_variables: true)
assert_equal([], template.errors)
-33
View File
@@ -113,37 +113,4 @@ class LiquidTagTest < Minitest::Test
{% raw %}{% liquid echo 'test' %}{% endraw %}
LIQUID
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
+1 -2
View File
@@ -13,9 +13,8 @@ class RawTagTest < Minitest::Test
end
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
def test_open_tag_in_raw
+5 -8
View File
@@ -82,22 +82,19 @@ class RenderTagTest < Minitest::Test
end
def test_recursively_rendered_template_does_not_produce_endless_loop
env = Liquid::Environment.build(
file_system: StubFileSystem.new('loop' => '{% render "loop" %}'),
)
Liquid::Template.file_system = StubFileSystem.new('loop' => '{% render "loop" %}')
assert_raises(Liquid::StackLevelError) do
Template.parse('{% render "loop" %}', environment: env).render!
Template.parse('{% render "loop" %}').render!
end
end
def test_sub_contexts_count_towards_the_same_recursion_limit
env = Liquid::Environment.build(
file_system: StubFileSystem.new('loop_render' => '{% render "loop_render" %}'),
Liquid::Template.file_system = StubFileSystem.new(
'loop_render' => '{% render "loop_render" %}',
)
assert_raises(Liquid::StackLevelError) do
Template.parse('{% render "loop_render" %}', environment: env).render!
Template.parse('{% render "loop_render" %}').render!
end
end
-48
View File
@@ -207,52 +207,4 @@ class TableRowTest < Minitest::Test
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
-18
View File
@@ -337,22 +337,4 @@ class TemplateTest < Minitest::Test
assert_equal("x=2", output)
assert_instance_of(String, output)
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
-5
View File
@@ -34,7 +34,6 @@ class VariableTest < Minitest::Test
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 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
@@ -130,10 +129,6 @@ class VariableTest < Minitest::Test
assert_template_result('bar', '{{ foo }}', { 'foo' => :bar })
end
def test_nested_array
assert_template_result('', '{{ foo }}', { 'foo' => [[nil]] })
end
def test_dynamic_find_var
assert_template_result('bar', '{{ [key] }}', { 'key' => 'foo', 'foo' => 'bar' })
end
+33 -17
View File
@@ -13,7 +13,7 @@ if (env_mode = ENV['LIQUID_PARSER_MODE'])
puts "-- #{env_mode.upcase} ERROR MODE"
mode = env_mode.to_sym
end
Liquid::Environment.default.error_mode = mode
Liquid::Template.error_mode = mode
if ENV['LIQUID_C'] == '1'
puts "-- LIQUID C"
@@ -42,11 +42,10 @@ module Minitest
message: nil, partials: nil, error_mode: nil, render_errors: false,
template_factory: nil
)
template = Liquid::Template.parse(template, line_numbers: true, error_mode: error_mode&.to_sym)
file_system = StubFileSystem.new(partials || {})
environment = Liquid::Environment.build(file_system: file_system)
template = Liquid::Template.parse(template, line_numbers: true, error_mode: error_mode&.to_sym, environment: environment)
registers = Liquid::Registers.new(file_system: file_system, template_factory: template_factory)
context = Liquid::Context.build(static_environments: assigns, rethrow_errors: !render_errors, registers: registers, environment: environment)
context = Liquid::Context.build(static_environments: assigns, rethrow_errors: !render_errors, registers: registers)
output = template.render(context)
assert_equal(expected, output, message)
end
@@ -79,27 +78,44 @@ module Minitest
assert_equal(times, calls, "Number of calls to Usage.increment with #{name.inspect}")
end
def with_global_filter(*globals, &blk)
environment = Liquid::Environment.build do |w|
w.register_filters(globals)
end
def with_global_filter(*globals)
original_global_cache = Liquid::StrainerFactory::GlobalCache
Liquid::StrainerFactory.send(:remove_const, :GlobalCache)
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
def with_error_mode(mode)
old_mode = Liquid::Environment.default.error_mode
Liquid::Environment.default.error_mode = mode
old_mode = Liquid::Template.error_mode
Liquid::Template.error_mode = mode
yield
ensure
Liquid::Environment.default.error_mode = old_mode
Liquid::Template.error_mode = old_mode
end
def with_custom_tag(tag_name, tag_class, &block)
environment = Liquid::Environment.default.dup
environment.register_tag(tag_name, tag_class)
Environment.dangerously_override(environment, &block)
def with_custom_tag(tag_name, tag_class)
old_tag = Liquid::Template.tags[tag_name]
begin
Liquid::Template.register_tag(tag_name, tag_class)
yield
ensure
if old_tag
Liquid::Template.tags[tag_name] = old_tag
else
Liquid::Template.tags.delete(tag_name)
end
end
end
end
end
-6
View File
@@ -32,12 +32,6 @@ class BlockUnitTest < Minitest::Test
assert_equal(String, template.root.nodelist[2].class)
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
template = Liquid::Template.parse(" {{funk}} {{so}} {{brother}} ")
assert_equal(7, template.root.nodelist.size)
+2 -7
View File
@@ -55,11 +55,6 @@ class ConditionUnitTest < Minitest::Test
assert_evaluates_false('bob', 'contains', '---')
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
assert_evaluates_argument_error(1, '~~', 0)
end
@@ -171,14 +166,14 @@ class ConditionUnitTest < Minitest::Test
def assert_evaluates_true(left, op, right)
assert(
Condition.new(left, op, right).evaluate(@context),
"Evaluated false: #{left.inspect} #{op} #{right.inspect}",
"Evaluated false: #{left} #{op} #{right}",
)
end
def assert_evaluates_false(left, op, right)
assert(
!Condition.new(left, op, right).evaluate(@context),
"Evaluated true: #{left.inspect} #{op} #{right.inspect}",
"Evaluated true: #{left} #{op} #{right}",
)
end
+19 -105
View File
@@ -6,134 +6,48 @@ class LexerUnitTest < Minitest::Test
include Liquid
def test_strings
assert_equal(
[[:string, %('this is a test""')], [:string, %("wat 'lol'")], [:end_of_string]],
tokenize(%( 'this is a test""' "wat 'lol'")),
)
tokens = Lexer.new(%( 'this is a test""' "wat 'lol'")).tokenize
assert_equal([[:string, %('this is a test""')], [:string, %("wat 'lol'")], [:end_of_string]], tokens)
end
def test_integer
assert_equal(
[[:id, 'hi'], [:number, '50'], [:end_of_string]],
tokenize('hi 50'),
)
tokens = Lexer.new('hi 50').tokenize
assert_equal([[:id, 'hi'], [:number, '50'], [:end_of_string]], tokens)
end
def test_float
assert_equal(
[[:id, 'hi'], [:number, '5.0'], [:end_of_string]],
tokenize('hi 5.0'),
)
tokens = Lexer.new('hi 5.0').tokenize
assert_equal([[:id, 'hi'], [:number, '5.0'], [:end_of_string]], tokens)
end
def test_comparison
assert_equal(
[[:comparison, '=='], [:comparison, '<>'], [:comparison, 'contains'], [:end_of_string]],
tokenize('== <> contains '),
)
end
def test_comparison_without_whitespace
assert_equal(
[[:number, '1'], [:comparison, '>'], [:number, '0'], [:end_of_string]],
tokenize('1>0'),
)
end
def test_comparison_with_negative_number
assert_equal(
[[:number, '1'], [:comparison, '>'], [:number, '-1'], [:end_of_string]],
tokenize('1>-1'),
)
end
def test_raise_for_invalid_comparison
assert_raises(SyntaxError) do
tokenize('1>!1')
end
assert_raises(SyntaxError) do
tokenize('1=<1')
end
assert_raises(SyntaxError) do
tokenize('1!!1')
end
tokens = Lexer.new('== <> contains ').tokenize
assert_equal([[:comparison, '=='], [:comparison, '<>'], [:comparison, 'contains'], [:end_of_string]], tokens)
end
def test_specials
assert_equal(
[[:pipe, '|'], [:dot, '.'], [:colon, ':'], [:end_of_string]],
tokenize('| .:'),
)
assert_equal(
[[:open_square, '['], [:comma, ','], [:close_square, ']'], [:end_of_string]],
tokenize('[,]'),
)
tokens = Lexer.new('| .:').tokenize
assert_equal([[:pipe, '|'], [:dot, '.'], [:colon, ':'], [:end_of_string]], tokens)
tokens = Lexer.new('[,]').tokenize
assert_equal([[:open_square, '['], [:comma, ','], [:close_square, ']'], [:end_of_string]], tokens)
end
def test_fancy_identifiers
assert_equal([[:id, 'hi'], [:id, 'five?'], [:end_of_string]], tokenize('hi five?'))
tokens = Lexer.new('hi five?').tokenize
assert_equal([[:id, 'hi'], [:id, 'five?'], [:end_of_string]], tokens)
assert_equal([[:number, '2'], [:id, 'foo'], [:end_of_string]], tokenize('2foo'))
tokens = Lexer.new('2foo').tokenize
assert_equal([[:number, '2'], [:id, 'foo'], [:end_of_string]], tokens)
end
def test_whitespace
assert_equal(
[[:id, 'five'], [:pipe, '|'], [:comparison, '=='], [:end_of_string]],
tokenize("five|\n\t =="),
)
tokens = Lexer.new("five|\n\t ==").tokenize
assert_equal([[:id, 'five'], [:pipe, '|'], [:comparison, '=='], [:end_of_string]], tokens)
end
def test_unexpected_character
assert_raises(SyntaxError) do
tokenize("%")
Lexer.new("%").tokenize
end
end
def test_negative_numbers
assert_equal(
[[:id, 'foo'], [:pipe, '|'], [:id, 'default'], [:colon, ":"], [:number, '-1'], [:end_of_string]],
tokenize("foo | default: -1"),
)
end
def test_greater_than_two_digits
assert_equal(
[[:id, 'foo'], [:comparison, '>'], [:number, '12'], [:end_of_string]],
tokenize("foo > 12"),
)
end
def test_error_with_utf8_character
error = assert_raises(SyntaxError) do
tokenize("1 < 1Ø")
end
assert_equal(
'Liquid syntax error: Unexpected character Ø',
error.message,
)
end
def test_contains_as_attribute_name
assert_equal(
[[:id, "a"], [:dot, "."], [:id, "contains"], [:dot, "."], [:id, "b"], [:end_of_string]],
tokenize("a.contains.b"),
)
end
def test_tokenize_incomplete_expression
assert_equal([[:id, "false"], [:dash, "-"], [:end_of_string]], tokenize("false -"))
assert_equal([[:id, "false"], [:comparison, "<"], [:end_of_string]], tokenize("false <"))
assert_equal([[:id, "false"], [:comparison, ">"], [:end_of_string]], tokenize("false >"))
assert_equal([[:id, "false"], [:number, "1"], [:end_of_string]], tokenize("false 1"))
end
private
def tokenize(input)
Lexer.new(input).tokenize
end
end
-23
View File
@@ -174,27 +174,4 @@ class PartialCacheUnitTest < Minitest::Test
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
@@ -2,7 +2,7 @@
require 'test_helper'
class EnvironmentFilterTest < Minitest::Test
class StrainerFactoryUnitTest < Minitest::Test
include Liquid
module AccessScopeFilters
@@ -16,6 +16,8 @@ class EnvironmentFilterTest < Minitest::Test
private :private_filter
end
StrainerFactory.add_global_filter(AccessScopeFilters)
module LateAddedFilter
def late_added_filter(_input)
"filtered"
@@ -23,28 +25,24 @@ class EnvironmentFilterTest < Minitest::Test
end
def setup
@environment = Liquid::Environment.build do |env|
env.register_filter(AccessScopeFilters)
end
@context = Context.build(environment: @environment)
@context = Context.build
end
def test_strainer
strainer = @environment.create_strainer(@context)
strainer = StrainerFactory.create(@context)
assert_equal(5, strainer.invoke('size', 'input'))
assert_equal("public", strainer.invoke("public_filter"))
end
def test_stainer_raises_argument_error
strainer = @environment.create_strainer(@context)
strainer = StrainerFactory.create(@context)
assert_raises(Liquid::ArgumentError) do
strainer.invoke("public_filter", 1)
end
end
def test_stainer_argument_error_contains_backtrace
strainer = @environment.create_strainer(@context)
strainer = StrainerFactory.create(@context)
exception = assert_raises(Liquid::ArgumentError) do
strainer.invoke("public_filter", 1)
@@ -59,7 +57,7 @@ class EnvironmentFilterTest < Minitest::Test
end
def test_strainer_only_invokes_public_filter_methods
strainer = @environment.create_strainer(@context)
strainer = StrainerFactory.create(@context)
assert_equal(false, strainer.class.invokable?('__test__'))
assert_equal(false, strainer.class.invokable?('test'))
assert_equal(false, strainer.class.invokable?('instance_eval'))
@@ -68,18 +66,18 @@ class EnvironmentFilterTest < Minitest::Test
end
def test_strainer_returns_nil_if_no_filter_method_found
strainer = @environment.create_strainer(@context)
strainer = StrainerFactory.create(@context)
assert_nil(strainer.invoke("private_filter"))
assert_nil(strainer.invoke("undef_the_filter"))
end
def test_strainer_returns_first_argument_if_no_method_and_arguments_given
strainer = @environment.create_strainer(@context)
strainer = StrainerFactory.create(@context)
assert_equal("password", strainer.invoke("undef_the_method", "password"))
end
def test_strainer_only_allows_methods_defined_in_filters
strainer = @environment.create_strainer(@context)
strainer = StrainerFactory.create(@context)
assert_equal("1 + 1", strainer.invoke("instance_eval", "1 + 1"))
assert_equal("puts", strainer.invoke("__send__", "puts", "Hi Mom"))
assert_equal("has_method?", strainer.invoke("invoke", "has_method?", "invoke"))
@@ -88,9 +86,7 @@ class EnvironmentFilterTest < Minitest::Test
def test_strainer_uses_a_class_cache_to_avoid_method_cache_invalidation
a = Module.new
b = Module.new
strainer = @environment.create_strainer(@context, [a, b])
strainer = StrainerFactory.create(@context, [a, b])
assert_kind_of(StrainerTemplate, strainer)
assert_kind_of(a, strainer)
assert_kind_of(b, strainer)
@@ -98,10 +94,8 @@ class EnvironmentFilterTest < Minitest::Test
end
def test_add_global_filter_clears_cache
assert_equal('input', @environment.create_strainer(@context).invoke('late_added_filter', 'input'))
@environment.register_filter(LateAddedFilter)
assert_equal('filtered', @environment.create_strainer(nil).invoke('late_added_filter', 'input'))
assert_equal('input', StrainerFactory.create(@context).invoke('late_added_filter', 'input'))
StrainerFactory.add_global_filter(LateAddedFilter)
assert_equal('filtered', StrainerFactory.create(nil).invoke('late_added_filter', 'input'))
end
end
+10 -15
View File
@@ -25,13 +25,11 @@ class StrainerTemplateUnitTest < Minitest::Test
end
def test_add_filter_raises_when_module_privately_overrides_registered_public_methods
error = assert_raises(Liquid::MethodOverrideError) do
Liquid::Environment.build do |env|
env.register_filter(PublicMethodOverrideFilter)
env.register_filter(PrivateMethodOverrideFilter)
end
end
strainer = Context.new.strainer
error = assert_raises(Liquid::MethodOverrideError) do
strainer.class.add_filter(PrivateMethodOverrideFilter)
end
assert_equal('Liquid error: Filter overrides registered public methods as non public: public_filter', error.message)
end
@@ -44,13 +42,11 @@ class StrainerTemplateUnitTest < Minitest::Test
end
def test_add_filter_raises_when_module_overrides_registered_public_method_as_protected
error = assert_raises(Liquid::MethodOverrideError) do
Liquid::Environment.build do |env|
env.register_filter(PublicMethodOverrideFilter)
env.register_filter(ProtectedMethodOverrideFilter)
end
end
strainer = Context.new.strainer
error = assert_raises(Liquid::MethodOverrideError) do
strainer.class.add_filter(ProtectedMethodOverrideFilter)
end
assert_equal('Liquid error: Filter overrides registered public methods as non public: public_filter', error.message)
end
@@ -62,9 +58,8 @@ class StrainerTemplateUnitTest < Minitest::Test
def test_add_filter_does_not_raise_when_module_overrides_previously_registered_method
with_global_filter do
context = Context.new
context.add_filters([PublicMethodOverrideFilter])
strainer = context.strainer
strainer = Context.new.strainer
strainer.class.add_filter(PublicMethodOverrideFilter)
assert(strainer.class.send(:filter_methods).include?('public_filter'))
end
end
-9
View File
@@ -20,13 +20,4 @@ class TagUnitTest < Minitest::Test
tag = Tag.parse("some_tag", "", Tokenizer.new(""), ParseContext.new)
assert_equal('some_tag', tag.tag_name)
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
-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
+53 -4
View File
@@ -20,13 +20,62 @@ class TemplateUnitTest < Minitest::Test
assert_equal(fixture("en_locale.yml"), locale.path)
end
def test_with_cache_classes_tags_returns_the_same_class
original_cache_setting = Liquid.cache_classes
Liquid.cache_classes = true
original_klass = Class.new
Object.send(:const_set, :CustomTag, original_klass)
Template.register_tag('custom', CustomTag)
Object.send(:remove_const, :CustomTag)
new_klass = Class.new
Object.send(:const_set, :CustomTag, new_klass)
assert(Template.tags['custom'].equal?(original_klass))
ensure
Object.send(:remove_const, :CustomTag)
Template.tags.delete('custom')
Liquid.cache_classes = original_cache_setting
end
def test_without_cache_classes_tags_reloads_the_class
original_cache_setting = Liquid.cache_classes
Liquid.cache_classes = false
original_klass = Class.new
Object.send(:const_set, :CustomTag, original_klass)
Template.register_tag('custom', CustomTag)
Object.send(:remove_const, :CustomTag)
new_klass = Class.new
Object.send(:const_set, :CustomTag, new_klass)
assert(Template.tags['custom'].equal?(new_klass))
ensure
Object.send(:remove_const, :CustomTag)
Template.tags.delete('custom')
Liquid.cache_classes = original_cache_setting
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
with_custom_tag('fake', FakeTag) do
result = Template.tags.map { |name, klass| [name, klass] }
assert(result.include?(["fake", TemplateUnitTest::FakeTag]))
end
Template.register_tag('fake', FakeTag)
result = Template.tags.map { |name, klass| [name, klass] }
assert(result.include?(["fake", "TemplateUnitTest::FakeTag"]))
ensure
Template.tags.delete('fake')
end
class TemplateSubclass < Liquid::Template