```
## How to use Liquid
Install Liquid by adding `gem 'liquid'` to your gemfile.
Liquid supports a very simple API based around the Liquid::Template class.
For standard use you can just pass it the content of a file and call render with a parameters hash.
```ruby
@template = Liquid::Template.parse("hi {{name}}") # Parses and compiles the template
@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. For smaller projects, a global environment is available via `Liquid::Environment.default`.
### Error Modes
Setting the error mode of Liquid lets you specify how strictly you want your templates to be interpreted.
Normally the parser is very lax and will accept almost anything without error. Unfortunately this can make
it very hard to debug and can lead to unexpected behaviour.
Liquid also comes with different parsers that can be used when editing templates to give better error messages
when templates are invalid. You can enable this new parser like this:
```ruby
Liquid::Environment.default.error_mode = :strict2 # Raises a SyntaxError when invalid syntax is used in all tags
Liquid::Environment.default.error_mode = :strict # Raises a SyntaxError when invalid syntax is used in some tags
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.
```
If you want to set the error mode only on specific templates you can pass `:error_mode` as an option to `parse`:
```ruby
Liquid::Template.parse(source, error_mode: :strict)
```
This is useful for doing things like enabling strict mode only in the theme editor.
It is recommended that you enable `:strict` or `:warn` mode on new apps to stop invalid templates from being created.
It is also recommended that you use it in the template editors of existing apps to give editors better error messages.
### Undefined variables and filters
By default, the renderer doesn't raise or in any other way notify you if some variables or filters are missing, i.e. not passed to the `render` method.
You can improve this situation by passing `strict_variables: true` and/or `strict_filters: true` options to the `render` method.
When one of these options is set to true, all errors about undefined variables and undefined filters will be stored in `errors` array of a `Liquid::Template` instance.
Here are some examples:
```ruby
template = Liquid::Template.parse("{{x}} {{y}} {{z.a}} {{z.b}}")
template.render({ 'x' => 1, 'z' => { 'a' => 2 } }, { strict_variables: true })
#=> '1 2 ' # when a variable is undefined, it's rendered as nil
template.errors
#=> [#, #]
```
```ruby
template = Liquid::Template.parse("{{x | filter1 | upcase}}")
template.render({ 'x' => 'foo' }, { strict_filters: true })
#=> '' # when at least one filter in the filter chain is undefined, a whole expression is rendered as nil
template.errors
#=> [#]
```
If you want to raise on a first exception instead of pushing all of them in `errors`, you can use `render!` method:
```ruby
template = Liquid::Template.parse("{{x}} {{y}}")
template.render!({ 'x' => 1}, { strict_variables: true })
#=> Liquid::UndefinedVariable: Liquid error: undefined variable y
```
### Usage tracking
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.
================================================
FILE: Rakefile
================================================
# frozen_string_literal: true
require 'rake'
require 'rake/testtask'
$LOAD_PATH.unshift(File.expand_path("../lib", __FILE__))
require "liquid/version"
task(default: [:test, :rubocop])
desc('run test suite with default parser')
Rake::TestTask.new(:base_test) do |t|
t.libs << 'lib' << 'test'
t.test_files = FileList['test/{integration,unit}/**/*_test.rb']
t.verbose = false
end
Rake::TestTask.new(:integration_test) do |t|
t.libs << 'lib' << 'test'
t.test_files = FileList['test/integration/**/*_test.rb']
t.verbose = false
end
desc('run test suite with warn error mode')
task :warn_test do
ENV['LIQUID_PARSER_MODE'] = 'warn'
Rake::Task['base_test'].invoke
end
task :rubocop do
if RUBY_ENGINE == 'ruby'
require 'rubocop/rake_task'
RuboCop::RakeTask.new
end
end
desc('runs test suite with lax, strict, and strict2 parsers')
task :test do
ENV['LIQUID_PARSER_MODE'] = 'lax'
Rake::Task['base_test'].invoke
ENV['LIQUID_PARSER_MODE'] = 'strict'
Rake::Task['base_test'].reenable
Rake::Task['base_test'].invoke
ENV['LIQUID_PARSER_MODE'] = 'strict2'
Rake::Task['base_test'].reenable
Rake::Task['base_test'].invoke
if RUBY_ENGINE == 'ruby' || RUBY_ENGINE == 'truffleruby'
ENV['LIQUID_PARSER_MODE'] = 'lax'
Rake::Task['integration_test'].reenable
Rake::Task['integration_test'].invoke
ENV['LIQUID_PARSER_MODE'] = 'strict'
Rake::Task['integration_test'].reenable
Rake::Task['integration_test'].invoke
ENV['LIQUID_PARSER_MODE'] = 'strict2'
Rake::Task['integration_test'].reenable
Rake::Task['integration_test'].invoke
end
end
task(gem: :build)
task :build do
system "gem build liquid.gemspec"
end
task install: :build do
system "gem install liquid-#{Liquid::VERSION}.gem"
end
task release: :build do
system "git tag -a v#{Liquid::VERSION} -m 'Tagging #{Liquid::VERSION}'"
system "git push --tags"
system "gem push liquid-#{Liquid::VERSION}.gem"
system "rm liquid-#{Liquid::VERSION}.gem"
end
namespace :benchmark do
desc "Run the liquid benchmark with lax parsing"
task :lax do
ruby "./performance/benchmark.rb lax"
end
desc "Run the liquid benchmark with strict parsing"
task :strict do
ruby "./performance/benchmark.rb strict"
end
desc "Run the liquid benchmark with strict2 parsing"
task :strict2 do
ruby "./performance/benchmark.rb strict2"
end
desc "Run the liquid benchmark with lax, strict, and strict2 parsing"
task run: [:lax, :strict, :strict2]
desc "Run unit benchmarks"
namespace :unit do
task :all do
Dir["./performance/unit/*_benchmark.rb"].each do |file|
puts "🧪 Running #{file}"
ruby file
end
end
task :lexer do
Dir["./performance/unit/lexer_benchmark.rb"].each do |file|
puts "🧪 Running #{file}"
ruby file
end
end
task :expression do
Dir["./performance/unit/expression_benchmark.rb"].each do |file|
puts "🧪 Running #{file}"
ruby file
end
end
end
end
namespace :profile do
desc "Run the liquid profile/performance coverage"
task :run do
ruby "./performance/profile.rb"
end
desc "Run the liquid profile/performance coverage with strict parsing"
task :strict do
ruby "./performance/profile.rb strict"
end
end
namespace :memory_profile do
desc "Run memory profiler"
task :run do
ruby "./performance/memory_profile.rb"
end
end
desc("Run example")
task :example do
ruby "-w -d -Ilib example/server/server.rb"
end
task :console do
exec 'irb -I lib -r liquid'
end
desc('run liquid-spec suite across all adapters')
task :spec do
adapters = Dir['./spec/*.rb'].join(',')
sh "bundle exec liquid-spec matrix --adapters=#{adapters} --reference=ruby_liquid"
end
================================================
FILE: bin/render
================================================
#!/usr/bin/env ruby
# frozen_string_literal: true
require 'bundler/setup'
require 'liquid'
class VirtualFileSystem
def initialize
snippet_1 = <<~LIQUID
================================================
FILE: lib/liquid/block.rb
================================================
# frozen_string_literal: true
module Liquid
class Block < Tag
MAX_DEPTH = 100
def initialize(tag_name, markup, options)
super
@blank = true
end
def parse(tokens)
@body = new_body
while parse_body(@body, tokens)
end
@body.freeze
end
# For backwards compatibility
def render(context)
@body.render(context)
end
def blank?
@blank
end
def nodelist
@body.nodelist
end
def unknown_tag(tag_name, _markup, _tokenizer)
Block.raise_unknown_tag(tag_name, block_name, block_delimiter, parse_context)
end
# @api private
def self.raise_unknown_tag(tag, block_name, block_delimiter, parse_context)
if tag == 'else'
raise SyntaxError, parse_context.locale.t(
"errors.syntax.unexpected_else",
block_name: block_name,
)
elsif tag.start_with?('end')
raise SyntaxError, parse_context.locale.t(
"errors.syntax.invalid_delimiter",
tag: tag,
block_name: block_name,
block_delimiter: block_delimiter,
)
else
raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag", tag: tag)
end
end
def raise_tag_never_closed(block_name)
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_never_closed", block_name: block_name)
end
def block_name
@tag_name
end
def block_delimiter
@block_delimiter ||= "end#{block_name}"
end
private
# @api public
def new_body
parse_context.new_block_body
end
# @api public
def parse_body(body, tokens)
if parse_context.depth >= MAX_DEPTH
raise StackLevelError, "Nesting too deep"
end
parse_context.depth += 1
begin
body.parse(tokens, parse_context) do |end_tag_name, end_tag_params|
@blank &&= body.blank?
return false if end_tag_name == block_delimiter
raise_tag_never_closed(block_name) unless end_tag_name
# this tag is not registered with the system
# pass it to the current block for special handling or error reporting
unknown_tag(end_tag_name, end_tag_params, tokens)
end
ensure
parse_context.depth -= 1
end
true
end
end
end
================================================
FILE: lib/liquid/block_body.rb
================================================
# frozen_string_literal: true
require 'English'
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 = "{%"
VARSTART = "{{"
attr_reader :nodelist
def initialize
@nodelist = []
@blank = true
end
def parse(tokenizer, parse_context, &block)
raise FrozenError, "can't modify frozen Liquid::BlockBody" if frozen?
parse_context.line_number = tokenizer.line_number
if tokenizer.for_liquid_tag
parse_for_liquid_tag(tokenizer, parse_context, &block)
else
parse_for_document(tokenizer, parse_context, &block)
end
end
def freeze
@nodelist.freeze
super
end
private def parse_for_liquid_tag(tokenizer, parse_context)
while (token = tokenizer.shift)
unless token.empty? || token.match?(WhitespaceOrNothing)
unless token =~ LiquidTagToken
# line isn't empty but didn't match tag syntax, yield and let the
# caller raise a syntax error
return yield token, token
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))
# end parsing if we reach an unknown tag and let the caller decide
# determine how to proceed
return yield tag_name, markup
end
new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
@blank &&= new_tag.blank?
@nodelist << new_tag
end
parse_context.line_number = tokenizer.line_number
end
yield nil, nil
end
# @api private
def self.unknown_tag_in_liquid_tag(tag, parse_context)
Block.raise_unknown_tag(tag, 'liquid', '%}', parse_context)
end
# @api private
def self.raise_missing_tag_terminator(token, parse_context)
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_termination", token: token, tag_end: TagEnd.inspect)
end
# @api private
def self.raise_missing_variable_terminator(token, parse_context)
raise SyntaxError, parse_context.locale.t("errors.syntax.variable_termination", token: token, tag_end: VariableEnd.inspect)
end
# @api private
def self.render_node(context, output, node)
node.render_to_output_buffer(context, output)
rescue => exc
blank_tag = !node.instance_of?(Variable) && node.blank?
rescue_render_node(context, output, node.line_number, exc, blank_tag)
end
# @api private
def self.rescue_render_node(context, output, line_number, exc, blank_tag)
case exc
when MemoryError
raise
when UndefinedVariable, UndefinedDropMethod, UndefinedFilter
context.handle_error(exc, line_number)
else
error_message = context.handle_error(exc, line_number)
unless blank_tag # conditional for backwards compatibility
output << error_message
end
end
end
private def parse_liquid_tag(markup, parse_context)
liquid_tag_tokenizer = parse_context.new_tokenizer(
markup, start_line_number: parse_context.line_number, for_liquid_tag: true
)
parse_for_liquid_tag(liquid_tag_tokenizer, parse_context) do |end_tag_name, _end_tag_markup|
if end_tag_name
BlockBody.unknown_tag_in_liquid_tag(end_tag_name, parse_context)
end
end
end
private def handle_invalid_tag_token(token, 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)
next if token.empty?
case
when token.start_with?(TAGSTART)
whitespace_handler(token, parse_context)
unless token =~ FullToken
return handle_invalid_tag_token(token, parse_context, &block)
end
tag_name = Regexp.last_match(2)
markup = Regexp.last_match(4)
if parse_context.line_number
# newlines inside the tag should increase the line number,
# particularly important for multiline {% liquid %} tags
parse_context.line_number += Regexp.last_match(1).count("\n") + Regexp.last_match(3).count("\n")
end
if tag_name == 'liquid'
parse_liquid_tag(markup, parse_context)
next
end
unless (tag = parse_context.environment.tag_for_name(tag_name))
# end parsing if we reach an unknown tag and let the caller decide
# determine how to proceed
return yield tag_name, markup
end
new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
@blank &&= new_tag.blank?
@nodelist << new_tag
when token.start_with?(VARSTART)
whitespace_handler(token, parse_context)
@nodelist << create_variable(token, parse_context)
@blank = false
else
if parse_context.trim_whitespace
token.lstrip!
end
parse_context.trim_whitespace = false
@nodelist << token
@blank &&= token.match?(WhitespaceOrNothing)
end
parse_context.line_number = tokenizer.line_number
end
yield nil, nil
end
def whitespace_handler(token, parse_context)
if token[2] == WhitespaceControl
previous_token = @nodelist.last
if previous_token.is_a?(String)
first_byte = previous_token.getbyte(0)
previous_token.rstrip!
if previous_token.empty? && parse_context[:bug_compatible_whitespace_trimming] && first_byte
previous_token << first_byte
end
end
end
parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
end
def blank?
@blank
end
# Remove blank strings in the block body for a control flow tag (e.g. `if`, `for`, `case`, `unless`)
# with a blank body.
#
# For example, in a conditional assignment like the following
#
# ```
# {% if size > max_size %}
# {% assign size = max_size %}
# {% endif %}
# ```
#
# we assume the intention wasn't to output the blank spaces in the `if` tag's block body, so this method
# will remove them to reduce the render output size.
#
# Note that it is now preferred to use the `liquid` tag for this use case.
def remove_blank_strings
raise "remove_blank_strings only support being called on a blank block body" unless @blank
@nodelist.reject! { |node| node.instance_of?(String) }
end
def render(context)
render_to_output_buffer(context, +'')
end
def render_to_output_buffer(context, output)
freeze unless frozen?
context.resource_limits.increment_render_score(@nodelist.length)
idx = 0
while (node = @nodelist[idx])
if node.instance_of?(String)
output << node
else
render_node(context, output, node)
# If we get an Interrupt that means the block must stop processing. An
# Interrupt is any command that stops block execution such as {% break %}
# or {% continue %}. These tags may also occur through Block or Include tags.
break if context.interrupt? # might have happened in a for-block
end
idx += 1
context.resource_limits.increment_write_score(output)
end
output
end
private
def render_node(context, output, node)
BlockBody.render_node(context, output, node)
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)
return Variable.new(markup, parse_context)
end
BlockBody.raise_missing_variable_terminator(token, parse_context)
end
# @deprecated Use {.raise_missing_tag_terminator} instead
def raise_missing_tag_terminator(token, parse_context)
BlockBody.raise_missing_tag_terminator(token, parse_context)
end
# @deprecated Use {.raise_missing_variable_terminator} instead
def raise_missing_variable_terminator(token, parse_context)
BlockBody.raise_missing_variable_terminator(token, parse_context)
end
end
end
================================================
FILE: lib/liquid/condition.rb
================================================
# frozen_string_literal: true
module Liquid
# Container for liquid nodes which conveniently wraps decision making logic
#
# Example:
#
# c = Condition.new(1, '==', 1)
# c.evaluate #=> true
#
class Condition # :nodoc:
@@operators = {
'==' => ->(cond, left, right) { cond.send(:equal_variables, left, right) },
'!=' => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
'<>' => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
'<' => :<,
'>' => :>,
'>=' => :>=,
'<=' => :<=,
'contains' => lambda do |_cond, left, right|
if left && right && left.respond_to?(:include?)
right = right.to_s if left.is_a?(String)
left.include?(right)
else
false
end
rescue Encoding::CompatibilityError
# "✅".b.include?("✅") raises Encoding::CompatibilityError despite being materially equal
left.b.include?(right.b)
end,
}
class MethodLiteral
attr_reader :method_name, :to_s
def initialize(method_name, to_s)
@method_name = method_name
@to_s = to_s
end
end
@@method_literals = {
'blank' => MethodLiteral.new(:blank?, '').freeze,
'empty' => MethodLiteral.new(:empty?, '').freeze,
}
def self.operators
@@operators
end
def self.parse_expression(parse_context, markup, safe: false)
@@method_literals[markup] || parse_context.parse_expression(markup, safe: safe)
end
attr_reader :attachment, :child_condition
attr_accessor :left, :operator, :right
def initialize(left = nil, operator = nil, right = nil)
@left = left
@operator = operator
@right = right
@child_relation = nil
@child_condition = nil
end
def evaluate(context = deprecated_default_context)
condition = self
result = nil
loop do
result = interpret_condition(condition.left, condition.right, condition.operator, context)
case condition.child_relation
when :or
break if Liquid::Utils.to_liquid_value(result)
when :and
break unless Liquid::Utils.to_liquid_value(result)
else
break
end
condition = condition.child_condition
end
result
end
def or(condition)
@child_relation = :or
@child_condition = condition
end
def and(condition)
@child_relation = :and
@child_condition = condition
end
def attach(attachment)
@attachment = attachment
end
def else?
false
end
def inspect
"#"
end
protected
attr_reader :child_relation
private
def equal_variables(left, right)
if left.is_a?(MethodLiteral)
return call_method_literal(left, right)
end
if right.is_a?(MethodLiteral)
return call_method_literal(right, left)
end
left == right
end
def call_method_literal(literal, value)
method_name = literal.method_name
# If the object responds to the method (e.g., ActiveSupport is loaded), use it
if value.respond_to?(method_name)
value.send(method_name)
else
# Emulate ActiveSupport's blank?/empty? to make Liquid invariant
# to whether ActiveSupport is loaded or not
case method_name
when :blank?
liquid_blank?(value)
when :empty?
liquid_empty?(value)
else
false
end
end
end
# Implement blank? semantics matching ActiveSupport
# blank? returns true for nil, false, empty strings, whitespace-only strings,
# empty arrays, and empty hashes
def liquid_blank?(value)
case value
when NilClass, FalseClass
true
when TrueClass, Numeric
false
when String
# Blank if empty or whitespace only (matches ActiveSupport)
value.empty? || value.match?(/\A\s*\z/)
when Array, Hash
value.empty?
else
# Fall back to empty? if available, otherwise false
value.respond_to?(:empty?) ? value.empty? : false
end
end
# Implement empty? semantics
# Note: nil is NOT empty. empty? checks if a collection has zero elements.
def liquid_empty?(value)
case value
when String, Array, Hash
value.empty?
else
value.respond_to?(:empty?) ? value.empty? : false
end
end
def interpret_condition(left, right, op, context)
# If the operator is empty this means that the decision statement is just
# a single variable. We can just poll this variable from the context and
# return this as the result.
return context.evaluate(left) if op.nil?
left = Liquid::Utils.to_liquid_value(context.evaluate(left))
right = Liquid::Utils.to_liquid_value(context.evaluate(right))
operation = self.class.operators[op] || raise(Liquid::ArgumentError, "Unknown operator #{op}")
if operation.respond_to?(:call)
operation.call(self, left, right)
elsif left.respond_to?(operation) && right.respond_to?(operation) && !left.is_a?(Hash) && !right.is_a?(Hash)
begin
left.send(operation, right)
rescue ::ArgumentError => e
raise Liquid::ArgumentError, e.message
end
end
end
def deprecated_default_context
warn("DEPRECATION WARNING: Condition#evaluate without a context argument is deprecated " \
"and will be removed from Liquid 6.0.0.")
Context.new
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[
@node.left,
@node.right,
@node.child_condition,
@node.attachment
].compact
end
end
end
class ElseCondition < Condition
def else?
true
end
def evaluate(_context)
true
end
end
end
================================================
FILE: lib/liquid/const.rb
================================================
# frozen_string_literal: true
module Liquid
module Const
EMPTY_HASH = {}.freeze
EMPTY_ARRAY = [].freeze
end
end
================================================
FILE: lib/liquid/context.rb
================================================
# frozen_string_literal: true
module Liquid
# Context keeps the variable stack and resolves variables, as well as keywords
#
# context['variable'] = 'testing'
# context['variable'] #=> 'testing'
# context['true'] #=> true
# context['10.2232'] #=> 10.2232
#
# context.stack do
# context['bob'] = 'bobsen'
# end
#
# 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
# 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)
end
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil, static_environments = {}, environment = Environment.default)
@environment = environment
@environments = [environments]
@environments.flatten!
@static_environments = [static_environments].flatten(1).freeze
@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)
@base_scope_depth = 0
@interrupts = []
@filters = []
@global_filter = nil
@disabled_tags = {}
# Instead of constructing new StringScanner objects for each Expression parse,
# we recycle the same one.
@string_scanner = StringScanner.new("")
@registers.static[:cached_partials] ||= {}
@registers.static[:file_system] ||= environment.file_system
@registers.static[:template_factory] ||= Liquid::TemplateFactory.new
self.exception_renderer = environment.exception_renderer
if rethrow_errors
self.exception_renderer = Liquid::RAISE_EXCEPTION_LAMBDA
end
yield self if block_given?
# Do this last, since it could result in this object being passed to a Proc in the environment
squash_instance_assigns_with_environments
end
# rubocop:enable Metrics/ParameterLists
def warnings
@warnings ||= []
end
def strainer
@strainer ||= @environment.create_strainer(self, @filters)
end
# Adds filters to this context.
#
# Note that this does not register the filters with the main Template object. see Template.register_filter
# for that
def add_filters(filters)
filters = [filters].flatten.compact
@filters += filters
@strainer = nil
end
def apply_global_filter(obj)
global_filter.nil? ? obj : global_filter.call(obj)
end
# are there any not handled interrupts?
def interrupt?
!@interrupts.empty?
end
# push an interrupt to the stack. this interrupt is considered not handled.
def push_interrupt(e)
@interrupts.push(e)
end
# pop an interrupt from the stack
def pop_interrupt
@interrupts.pop
end
def handle_error(e, line_number = nil)
e = internal_error unless e.is_a?(Liquid::Error)
e.template_name ||= template_name
e.line_number ||= line_number
errors.push(e)
exception_renderer.call(e).to_s
end
def invoke(method, *args)
strainer.invoke(method, *args).to_liquid
end
# Push new local scope on the stack. use Context#stack instead
def push(new_scope = {})
@scopes.unshift(new_scope)
check_overflow
end
# Merge a hash of variables in the current local scope
def merge(new_scopes)
@scopes[0].merge!(new_scopes)
end
# Pop from the stack. use Context#stack instead
def pop
raise ContextError if @scopes.size == 1
@scopes.shift
end
# Pushes a new local scope on the stack, pops it at the end of the block
#
# Example:
# context.stack do
# context['var'] = 'hi'
# end
#
# context['var'] #=> nil
def stack(new_scope = {})
push(new_scope)
yield
ensure
pop
end
# Creates a new context inheriting resource limits, filters, environment etc.,
# but with an isolated scope.
def new_isolated_subcontext
check_overflow
self.class.build(
environment: @environment,
resource_limits: resource_limits,
static_environments: static_environments,
registers: Registers.new(registers),
).tap do |subcontext|
subcontext.base_scope_depth = base_scope_depth + 1
subcontext.exception_renderer = exception_renderer
subcontext.filters = @filters
subcontext.strainer = nil
subcontext.errors = errors
subcontext.warnings = warnings
subcontext.disabled_tags = @disabled_tags
end
end
def clear_instance_assigns
@scopes[0] = {}
end
# Only allow String, Numeric, Hash, Array, Proc, Boolean or Liquid::Drop
def []=(key, value)
@scopes[0][key] = value
end
# Look up variable, either resolve directly after considering the name. We can directly handle
# Strings, digits, floats and booleans (true,false).
# If no match is made we lookup the variable in the current scope and
# later move up to the parent blocks to see if we can resolve the variable somewhere up the tree.
# Some special keywords return symbols. Those symbols are to be called on the rhs object in expressions
#
# Example:
# products == empty #=> products.empty?
def [](expression)
evaluate(Expression.parse(expression, @string_scanner))
end
def key?(key)
find_variable(key, raise_on_not_found: false) != nil
end
def evaluate(object)
object.respond_to?(:evaluate) ? object.evaluate(self) : object
end
# Fetches an object starting at the local scope and then moving up the hierachy
def find_variable(key, raise_on_not_found: true)
# This was changed from find() to find_index() because this is a very hot
# path and find_index() is optimized in MRI to reduce object allocation
index = @scopes.find_index { |s| s.key?(key) }
variable = if index
lookup_and_evaluate(@scopes[index], key, raise_on_not_found: raise_on_not_found)
else
try_variable_find_in_environments(key, raise_on_not_found: raise_on_not_found)
end
# update variable's context before invoking #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
end
def lookup_and_evaluate(obj, key, raise_on_not_found: true)
if @strict_variables && raise_on_not_found && obj.respond_to?(:key?) && !obj.key?(key)
raise Liquid::UndefinedVariable, "undefined variable #{key}"
end
value = obj[key]
if value.is_a?(Proc) && obj.respond_to?(:[]=)
obj[key] = value.arity == 0 ? value.call : value.call(self)
else
value
end
end
def with_disabled_tags(tag_names)
tag_names.each do |name|
@disabled_tags[name] = @disabled_tags.fetch(name, 0) + 1
end
yield
ensure
tag_names.each do |name|
@disabled_tags[name] -= 1
end
end
def tag_disabled?(tag_name)
@disabled_tags.fetch(tag_name, 0) > 0
end
protected
attr_writer :base_scope_depth, :warnings, :errors, :strainer, :filters, :disabled_tags
private
attr_reader :base_scope_depth
def try_variable_find_in_environments(key, raise_on_not_found:)
@environments.each do |environment|
found_variable = lookup_and_evaluate(environment, key, raise_on_not_found: raise_on_not_found)
if !found_variable.nil? || @strict_variables && raise_on_not_found
return found_variable
end
end
@static_environments.each do |environment|
found_variable = lookup_and_evaluate(environment, key, raise_on_not_found: raise_on_not_found)
if !found_variable.nil? || @strict_variables && raise_on_not_found
return found_variable
end
end
nil
end
def check_overflow
raise StackLevelError, "Nesting too deep" if overflow?
end
def overflow?
base_scope_depth + @scopes.length > Block::MAX_DEPTH
end
def internal_error
# raise and catch to set backtrace and cause on exception
raise Liquid::InternalError, 'internal'
rescue Liquid::InternalError => exc
exc
end
def squash_instance_assigns_with_environments
@scopes.last.each_key do |k|
@environments.each do |env|
if env.key?(k)
scopes.last[k] = lookup_and_evaluate(env, k)
break
end
end
end
end # squash_instance_assigns_with_environments
end # Context
end # Liquid
================================================
FILE: lib/liquid/deprecations.rb
================================================
# 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
================================================
FILE: lib/liquid/document.rb
================================================
# frozen_string_literal: true
module Liquid
class Document
def self.parse(tokens, parse_context)
doc = new(parse_context)
doc.parse(tokens, parse_context)
doc
end
attr_reader :parse_context, :body
def initialize(parse_context)
@parse_context = parse_context
@body = new_body
end
def nodelist
@body.nodelist
end
def parse(tokenizer, parse_context)
while parse_body(tokenizer)
end
@body.freeze
rescue SyntaxError => e
e.line_number ||= parse_context.line_number
raise
end
def unknown_tag(tag, _markup, _tokenizer)
case tag
when 'else', 'end'
raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_outer_tag", tag: tag)
else
raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag", tag: tag)
end
end
def render_to_output_buffer(context, output)
@body.render_to_output_buffer(context, output)
end
def render(context)
render_to_output_buffer(context, +'')
end
private
def new_body
parse_context.new_block_body
end
def parse_body(tokenizer)
@body.parse(tokenizer, parse_context) do |unknown_tag_name, unknown_tag_markup|
if unknown_tag_name
unknown_tag(unknown_tag_name, unknown_tag_markup, tokenizer)
true
else
false
end
end
end
end
end
================================================
FILE: lib/liquid/drop.rb
================================================
# frozen_string_literal: true
require 'set'
module Liquid
# A drop in liquid is a class which allows you to export DOM like things to liquid.
# Methods of drops are callable.
# The main use for liquid drops is to implement lazy loaded objects.
# If you would like to make data available to the web designers which you don't want loaded unless needed then
# a drop is a great way to do that.
#
# Example:
#
# class ProductDrop < Liquid::Drop
# def top_sales
# Shop.current.products.find(:all, :order => 'sales', :limit => 10 )
# end
# end
#
# tmpl = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {%endfor%} ' )
# tmpl.render('product' => ProductDrop.new ) # will invoke top_sales query.
#
# Your drop can either implement the methods sans any parameters
# or implement the liquid_method_missing(name) method which is a catch all.
class Drop
attr_writer :context
def initialize
@context = nil
end
# Catch all for the method
def liquid_method_missing(method)
return unless @context&.strict_variables
raise Liquid::UndefinedDropMethod, "undefined method #{method}"
end
# called by liquid to invoke a drop
def invoke_drop(method_or_key)
if self.class.invokable?(method_or_key)
send(method_or_key)
else
liquid_method_missing(method_or_key)
end
end
def key?(_name)
true
end
def inspect
self.class.to_s
end
def to_liquid
self
end
def to_s
self.class.name
end
alias_method :[], :invoke_drop
# Check for method existence without invoking respond_to?, which creates symbols
def self.invokable?(method_name)
invokable_methods.include?(method_name.to_s)
end
def self.invokable_methods
@invokable_methods ||= begin
blacklist = Liquid::Drop.public_instance_methods + [:each]
if include?(Enumerable)
blacklist += Enumerable.public_instance_methods
blacklist -= [:sort, :count, :first, :min, :max]
end
whitelist = [:to_liquid] + (public_instance_methods - blacklist)
Set.new(whitelist.map(&:to_s))
end
end
end
end
================================================
FILE: lib/liquid/environment.rb
================================================
# 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 :strict2, :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] 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] 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] 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
================================================
FILE: lib/liquid/errors.rb
================================================
# frozen_string_literal: true
module Liquid
class Error < ::StandardError
attr_accessor :line_number
attr_accessor :template_name
attr_accessor :markup_context
def to_s(with_prefix = true)
str = +""
str << message_prefix if with_prefix
str << super()
if markup_context
str << " "
str << markup_context
end
str
end
private
def message_prefix
str = +""
str << if is_a?(SyntaxError)
"Liquid syntax error"
else
"Liquid error"
end
if line_number
str << " ("
str << template_name << " " if template_name
str << "line " << line_number.to_s << ")"
end
str << ": "
str
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)
end
================================================
FILE: lib/liquid/expression.rb
================================================
# frozen_string_literal: true
module Liquid
class Expression
LITERALS = {
nil => nil,
'nil' => nil,
'null' => nil,
'' => nil,
'true' => true,
'false' => false,
'blank' => '',
'empty' => '',
# in lax mode, minus sign can be a VariableLookup
# For simplicity and performace, we treat it like a literal
'-' => VariableLookup.parse("-", nil).freeze,
}.freeze
DOT = ".".ord
ZERO = "0".ord
NINE = "9".ord
DASH = "-".ord
# Use an atomic group (?>...) to avoid pathological backtracing from
# malicious input as described in https://github.com/Shopify/liquid/issues/1357
RANGES_REGEX = /\A\(\s*(?>(\S+)\s*\.\.)\s*(\S+)\s*\)\z/
INTEGER_REGEX = /\A(-?\d+)\z/
FLOAT_REGEX = /\A(-?\d+)\.\d+\z/
class << self
def safe_parse(parser, ss = StringScanner.new(""), cache = nil)
parse(parser.expression, ss, cache)
end
def parse(markup, ss = StringScanner.new(""), cache = nil)
return unless markup
markup = markup.strip # markup can be a frozen string
if (markup.start_with?('"') && markup.end_with?('"')) ||
(markup.start_with?("'") && markup.end_with?("'"))
return markup[1..-2]
elsif LITERALS.key?(markup)
return LITERALS[markup]
end
# Cache only exists during parsing
if cache
return cache[markup] if cache.key?(markup)
cache[markup] = inner_parse(markup, ss, cache).freeze
else
inner_parse(markup, ss, nil).freeze
end
end
def inner_parse(markup, ss, cache)
if markup.start_with?("(") && markup.end_with?(")") && markup =~ RANGES_REGEX
return RangeLookup.parse(
Regexp.last_match(1),
Regexp.last_match(2),
ss,
cache,
)
end
if (num = parse_number(markup, ss))
num
else
VariableLookup.parse(markup, ss, cache)
end
end
def parse_number(markup, ss)
# check if the markup is simple integer or float
case markup
when INTEGER_REGEX
return Integer(markup, 10)
when FLOAT_REGEX
return markup.to_f
end
ss.string = markup
# the first byte must be a digit or a dash
byte = ss.scan_byte
return false if byte != DASH && (byte < ZERO || byte > NINE)
if byte == DASH
peek_byte = ss.peek_byte
# if it starts with a dash, the next byte must be a digit
return false if peek_byte.nil? || !(peek_byte >= ZERO && peek_byte <= NINE)
end
# The markup could be a float with multiple dots
first_dot_pos = nil
num_end_pos = nil
while (byte = ss.scan_byte)
return false if byte != DOT && (byte < ZERO || byte > NINE)
# we found our number and now we are just scanning the rest of the string
next if num_end_pos
if byte == DOT
if first_dot_pos.nil?
first_dot_pos = ss.pos
else
# we found another dot, so we know that the number ends here
num_end_pos = ss.pos - 1
end
end
end
num_end_pos = markup.length if ss.eos?
if num_end_pos
# number ends with a number "123.123"
markup.byteslice(0, num_end_pos).to_f
else
# number ends with a dot "123."
markup.byteslice(0, first_dot_pos).to_f
end
end
end
end
end
================================================
FILE: lib/liquid/extensions.rb
================================================
# frozen_string_literal: true
require 'time'
require 'date'
class String # :nodoc:
def to_liquid
self
end
end
class Symbol # :nodoc:
def to_liquid
to_s
end
end
class Array # :nodoc:
def to_liquid
self
end
end
class Hash # :nodoc:
def to_liquid
self
end
end
class Numeric # :nodoc:
def to_liquid
self
end
end
class Range # :nodoc:
def to_liquid
self
end
end
class Time # :nodoc:
def to_liquid
self
end
end
class DateTime < Date # :nodoc:
def to_liquid
self
end
end
class Date # :nodoc:
def to_liquid
self
end
end
class TrueClass
def to_liquid # :nodoc:
self
end
end
class FalseClass
def to_liquid # :nodoc:
self
end
end
class NilClass
def to_liquid # :nodoc:
self
end
end
================================================
FILE: lib/liquid/file_system.rb
================================================
# frozen_string_literal: true
module Liquid
# A Liquid file system is a way to let your templates retrieve other templates for use with the include tag.
#
# You can implement subclasses that retrieve templates from the database, from the file system using a different
# path structure, you can provide them as hard-coded inline strings, or any manner that you see fit.
#
# You can add additional instance variables, arguments, or methods as needed.
#
# Example:
#
# Liquid::Template.file_system = Liquid::LocalFileSystem.new(template_path)
# liquid = Liquid::Template.parse(template)
#
# This will parse the template with a LocalFileSystem implementation rooted at 'template_path'.
class BlankFileSystem
# Called by Liquid to retrieve a template file
def read_template_file(_template_path)
raise FileSystemError, "This liquid context does not allow includes."
end
end
# This implements an abstract file system which retrieves template files named in a manner similar to Rails partials,
# ie. with the template name prefixed with an underscore. The extension ".liquid" is also added.
#
# For security reasons, template paths are only allowed to contain letters, numbers, and underscore.
#
# Example:
#
# file_system = Liquid::LocalFileSystem.new("/some/path")
#
# file_system.full_path("mypartial") # => "/some/path/_mypartial.liquid"
# file_system.full_path("dir/mypartial") # => "/some/path/dir/_mypartial.liquid"
#
# Optionally in the second argument you can specify a custom pattern for template filenames.
# The Kernel::sprintf format specification is used.
# Default pattern is "_%s.liquid".
#
# Example:
#
# file_system = Liquid::LocalFileSystem.new("/some/path", "%s.html")
#
# file_system.full_path("index") # => "/some/path/index.html"
#
class LocalFileSystem
attr_accessor :root
def initialize(root, pattern = "_%s.liquid")
@root = root
@pattern = pattern
end
def read_template_file(template_path)
full_path = full_path(template_path)
raise FileSystemError, "No such template '#{template_path}'" unless File.exist?(full_path)
File.read(full_path)
end
def full_path(template_path)
raise FileSystemError, "Illegal template name '#{template_path}'" unless %r{\A[^./][a-zA-Z0-9_/]+\z}.match?(template_path)
full_path = if template_path.include?('/')
File.join(root, File.dirname(template_path), @pattern % File.basename(template_path))
else
File.join(root, @pattern % template_path)
end
raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path).start_with?(File.expand_path(root))
full_path
end
end
end
================================================
FILE: lib/liquid/forloop_drop.rb
================================================
# frozen_string_literal: true
module Liquid
# @liquid_public_docs
# @liquid_type object
# @liquid_name forloop
# @liquid_summary
# Information about a parent [`for` loop](/docs/api/liquid/tags/for).
class ForloopDrop < Drop
def initialize(name, length, parentloop)
@name = name
@length = length
@parentloop = parentloop
@index = 0
end
# @liquid_public_docs
# @liquid_name length
# @liquid_summary
# The total number of iterations in the loop.
# @liquid_return [number]
attr_reader :length
# @liquid_public_docs
# @liquid_name parentloop
# @liquid_summary
# The parent `forloop` object.
# @liquid_description
# If the current `for` loop isn't nested inside another `for` loop, then `nil` is returned.
# @liquid_return [forloop]
attr_reader :parentloop
attr_reader :name
# @liquid_public_docs
# @liquid_summary
# The 1-based index of the current iteration.
# @liquid_return [number]
def index
@index + 1
end
# @liquid_public_docs
# @liquid_summary
# The 0-based index of the current iteration.
# @liquid_return [number]
def index0
@index
end
# @liquid_public_docs
# @liquid_summary
# The 1-based index of the current iteration, in reverse order.
# @liquid_return [number]
def rindex
@length - @index
end
# @liquid_public_docs
# @liquid_summary
# The 0-based index of the current iteration, in reverse order.
# @liquid_return [number]
def rindex0
@length - @index - 1
end
# @liquid_public_docs
# @liquid_summary
# Returns `true` if the current iteration is the first. Returns `false` if not.
# @liquid_return [boolean]
def first
@index == 0
end
# @liquid_public_docs
# @liquid_summary
# Returns `true` if the current iteration is the last. Returns `false` if not.
# @liquid_return [boolean]
def last
@index == @length - 1
end
protected
def increment!
@index += 1
end
end
end
================================================
FILE: lib/liquid/i18n.rb
================================================
# frozen_string_literal: true
require 'yaml'
module Liquid
class I18n
DEFAULT_LOCALE = File.join(File.expand_path(__dir__), "locales", "en.yml")
TranslationError = Class.new(StandardError)
attr_reader :path
def initialize(path = DEFAULT_LOCALE)
@path = path
end
def translate(name, vars = {})
interpolate(deep_fetch_translation(name), vars)
end
alias_method :t, :translate
def locale
@locale ||= YAML.load_file(@path)
end
private
def interpolate(name, vars)
name.gsub(/%\{(\w+)\}/) do
# raise TranslationError, "Undefined key #{$1} for interpolation in translation #{name}" unless vars[$1.to_sym]
vars[Regexp.last_match(1).to_sym].to_s
end
end
def deep_fetch_translation(name)
name.split('.').reduce(locale) do |level, cur|
level[cur] || raise(TranslationError, "Translation for #{name} does not exist in locale #{path}")
end
end
end
end
================================================
FILE: lib/liquid/interrupts.rb
================================================
# frozen_string_literal: true
module Liquid
# An interrupt is any command that breaks processing of a block (ex: a for loop).
class Interrupt
attr_reader :message
def initialize(message = nil)
@message = message || "interrupt"
end
end
# Interrupt that is thrown whenever a {% break %} is called.
class BreakInterrupt < Interrupt; end
# Interrupt that is thrown whenever a {% continue %} is called.
class ContinueInterrupt < Interrupt; end
end
================================================
FILE: lib/liquid/lexer.rb
================================================
# frozen_string_literal: true
module Liquid
class Lexer
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
table.freeze
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
table.freeze
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
sub_table.freeze
end
table[">".ord] = [].tap do |sub_table|
sub_table["=".ord] = COMPARISON_GREATER_THAN_OR_EQUAL
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
# rubocop:disable Metrics/BlockNesting
class << self
def tokenize(ss)
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, ss)
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, ss)
end
end
end
# rubocop:enable Metrics/BlockNesting
output << EOS
rescue ::ArgumentError => e
if e.message == "invalid byte sequence in #{ss.string.encoding}"
raise SyntaxError, "Invalid byte sequence in #{ss.string.encoding}"
else
raise
end
end
def raise_syntax_error(start_pos, ss)
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
end
end
================================================
FILE: lib/liquid/locales/en.yml
================================================
---
errors:
syntax:
tag_unexpected_args: "Syntax Error in '%{tag}' - Valid syntax: %{tag}"
block_tag_unexpected_args: "Syntax Error in '%{tag}' - Valid syntax: {% %{tag} %}{% end%{tag} %}"
assign: "Syntax Error in 'assign' - Valid syntax: assign [var] = [source]"
capture: "Syntax Error in 'capture' - Valid syntax: capture [var]"
case: "Syntax Error in 'case' - Valid syntax: case [condition]"
case_invalid_when: "Syntax Error in tag 'case' - Valid when condition: {% when [condition] [or condition2...] %}"
case_invalid_else: "Syntax Error in tag 'case' - Valid else condition: {% else %} (no parameters) "
cycle: "Syntax Error in 'cycle' - Valid syntax: cycle [name :] var [, var2, var3 ...]"
doc_invalid_nested: "Syntax Error in 'doc' - Nested doc tags are not allowed"
for: "Syntax Error in 'for loop' - Valid syntax: for [item] in [collection]"
for_invalid_in: "For loops require an 'in' clause"
for_invalid_attribute: "Invalid attribute in for loop. Valid attributes are limit and offset"
if: "Syntax Error in tag 'if' - Valid syntax: if [expression]"
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"
table_row_invalid_attribute: "Invalid attribute '%{attribute}' in tablerow loop. Valid attributes are cols, limit, offset, and range"
tag_never_closed: "'%{block_name}' tag was never closed"
tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"
unexpected_else: "%{block_name} tag does not expect 'else' tag"
unexpected_outer_tag: "Unexpected outer '%{tag}' tag"
unknown_tag: "Unknown tag '%{tag}'"
variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}"
argument:
include: "Argument error in tag 'include' - Illegal template name"
disabled:
tag: "usage is not allowed in this context"
================================================
FILE: lib/liquid/parse_context.rb
================================================
# frozen_string_literal: true
module Liquid
class ParseContext
attr_accessor :locale, :line_number, :trim_whitespace, :depth
attr_reader :partial, :warnings, :error_mode, :environment
def initialize(options = Const::EMPTY_HASH)
@environment = options.fetch(:environment, Environment.default)
@template_options = options ? options.dup : {}
@locale = @template_options[:locale] ||= I18n.new
@warnings = []
# constructing new StringScanner in Lexer, Tokenizer, etc is expensive
# This StringScanner will be shared by all of them
@string_scanner = StringScanner.new("")
@expression_cache = if options[:expression_cache].nil?
{}
elsif options[:expression_cache].respond_to?(:[]) && options[:expression_cache].respond_to?(:[]=)
options[:expression_cache]
elsif options[:expression_cache]
{}
end
self.depth = 0
self.partial = false
end
def [](option_key)
@options[option_key]
end
def new_block_body
Liquid::BlockBody.new
end
def new_parser(input)
@string_scanner.string = input
Parser.new(@string_scanner)
end
def new_tokenizer(source, start_line_number: nil, for_liquid_tag: false)
Tokenizer.new(
source: source,
string_scanner: @string_scanner,
line_number: start_line_number,
for_liquid_tag: for_liquid_tag,
)
end
def safe_parse_expression(parser)
Expression.safe_parse(parser, @string_scanner, @expression_cache)
end
def parse_expression(markup, safe: false)
if !safe && @error_mode == :strict2
# parse_expression is a widely used API. To maintain backward
# compatibility while raising awareness about strict2 parser standards,
# the safe flag supports API users make a deliberate decision.
#
# In strict2 mode, markup MUST come from a string returned by the parser
# (e.g., parser.expression). We're not calling the parser here to
# prevent redundant parser overhead.
raise Liquid::InternalError, "unsafe parse_expression cannot be used in strict2 mode"
end
Expression.parse(markup, @string_scanner, @expression_cache)
end
def partial=(value)
@partial = value
@options = value ? partial_options : @template_options
@error_mode = @options[:error_mode] || @environment.error_mode
end
def partial_options
@partial_options ||= begin
dont_pass = @template_options[:include_options_blacklist]
if dont_pass == true
{ locale: locale }
elsif dont_pass.is_a?(Array)
@template_options.reject { |k, _v| dont_pass.include?(k) }
else
@template_options
end
end
end
end
end
================================================
FILE: lib/liquid/parse_tree_visitor.rb
================================================
# frozen_string_literal: true
module Liquid
class ParseTreeVisitor
def self.for(node, callbacks = Hash.new(proc {}))
if defined?(node.class::ParseTreeVisitor)
node.class::ParseTreeVisitor
else
self
end.new(node, callbacks)
end
def initialize(node, callbacks)
@node = node
@callbacks = callbacks
end
def add_callback_for(*classes, &block)
callback = block
callback = ->(node, _) { yield node } if block.arity.abs == 1
callback = ->(_, _) { yield } if block.arity.zero?
classes.each { |klass| @callbacks[klass] = callback }
self
end
def visit(context = nil)
children.map do |node|
item, new_context = @callbacks[node.class].call(node, context)
[
item,
ParseTreeVisitor.for(node, @callbacks).visit(new_context || context),
]
end
end
protected
def children
@node.respond_to?(:nodelist) ? Array(@node.nodelist) : Const::EMPTY_ARRAY
end
end
end
================================================
FILE: lib/liquid/parser.rb
================================================
# frozen_string_literal: true
module Liquid
class Parser
def initialize(input)
ss = input.is_a?(StringScanner) ? input : StringScanner.new(input)
@tokens = Lexer.tokenize(ss)
@p = 0 # pointer to current location
end
def jump(point)
@p = point
end
def consume(type = nil)
token = @tokens[@p]
if type && token[0] != type
raise SyntaxError, "Expected #{type} but found #{@tokens[@p].first}"
end
@p += 1
token[1]
end
# Only consumes the token if it matches the type
# Returns the token's contents if it was consumed
# or false otherwise.
def consume?(type)
token = @tokens[@p]
return false unless token && token[0] == type
@p += 1
token[1]
end
# Like consume? Except for an :id token of a certain name
def id?(str)
token = @tokens[@p]
return false unless token && token[0] == :id
return false unless token[1] == str
@p += 1
token[1]
end
def look(type, ahead = 0)
tok = @tokens[@p + ahead]
return false unless tok
tok[0] == type
end
def expression
token = @tokens[@p]
case token[0]
when :id
str = consume
str << variable_lookups
when :open_square
str = consume.dup
str << expression
str << consume(:close_square)
str << variable_lookups
when :string, :number
consume
when :open_round
consume
first = expression
consume(:dotdot)
last = expression
consume(:close_round)
"(#{first}..#{last})"
else
raise SyntaxError, "#{token} is not a valid expression"
end
end
def argument
str = +""
# might be a keyword argument (identifier: expression)
if look(:id) && look(:colon, 1)
str << consume << consume << ' '
end
str << expression
str
end
def variable_lookups
str = +""
loop do
if look(:open_square)
str << consume
str << expression
str << consume(:close_square)
elsif look(:dot)
str << consume
str << consume(:id)
else
break
end
end
str
end
end
end
================================================
FILE: lib/liquid/parser_switching.rb
================================================
# frozen_string_literal: true
module Liquid
module ParserSwitching
# Do not use this.
#
# It's basically doing the same thing the {#parse_with_selected_parser},
# except this will try the strict parser regardless of the error mode,
# and fall back to the lax parser if the error mode is lax or warn,
# except when in strict2 mode where it uses the strict2 parser.
#
# @deprecated Use {#parse_with_selected_parser} instead.
def strict_parse_with_error_mode_fallback(markup)
return strict2_parse_with_error_context(markup) if strict2_mode?
strict_parse_with_error_context(markup)
rescue SyntaxError => e
case parse_context.error_mode
when :rigid
rigid_warn
raise
when :strict2
raise
when :strict
raise
when :warn
parse_context.warnings << e
end
lax_parse(markup)
end
def parse_with_selected_parser(markup)
case parse_context.error_mode
when :rigid then rigid_warn && strict2_parse_with_error_context(markup)
when :strict2 then strict2_parse_with_error_context(markup)
when :strict then strict_parse_with_error_context(markup)
when :lax then lax_parse(markup)
when :warn
begin
strict2_parse_with_error_context(markup)
rescue SyntaxError => e
parse_context.warnings << e
lax_parse(markup)
end
end
end
def strict2_mode?
parse_context.error_mode == :strict2 || parse_context.error_mode == :rigid
end
private
def rigid_warn
Deprecations.warn(':rigid', ':strict2')
end
def strict2_parse_with_error_context(markup)
strict2_parse(markup)
rescue SyntaxError => e
e.line_number = line_number
e.markup_context = markup_context(markup)
raise e
end
def strict_parse_with_error_context(markup)
strict_parse(markup)
rescue SyntaxError => e
e.line_number = line_number
e.markup_context = markup_context(markup)
raise e
end
def markup_context(markup)
"in \"#{markup.strip}\""
end
end
end
================================================
FILE: lib/liquid/partial_cache.rb
================================================
# frozen_string_literal: true
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]
return cached if cached
file_system = context.registers[:file_system]
source = file_system.read_template_file(template_name)
parse_context.partial = true
template_factory = context.registers[:template_factory]
template = template_factory.for(template_name)
begin
partial = template.parse(source, parse_context)
rescue Liquid::Error => e
e.template_name = template&.name || template_name
raise e
end
partial.name ||= template_name
cached_partials[cache_key] = partial
ensure
parse_context.partial = false
end
end
end
================================================
FILE: lib/liquid/profiler/hooks.rb
================================================
# frozen_string_literal: true
module Liquid
module BlockBodyProfilingHook
def render_node(context, output, node)
if (profiler = context.profiler)
profiler.profile_node(context.template_name, code: node.raw, line_number: node.line_number) do
super
end
else
super
end
end
end
BlockBody.prepend(BlockBodyProfilingHook)
module DocumentProfilingHook
def render_to_output_buffer(context, output)
return super unless context.profiler
context.profiler.profile(context.template_name) { super }
end
end
Document.prepend(DocumentProfilingHook)
module ContextProfilingHook
attr_accessor :profiler
def new_isolated_subcontext
new_context = super
new_context.profiler = profiler
new_context
end
end
Context.prepend(ContextProfilingHook)
end
================================================
FILE: lib/liquid/profiler.rb
================================================
# frozen_string_literal: true
require 'liquid/profiler/hooks'
module Liquid
# Profiler enables support for profiling template rendering to help track down performance issues.
#
# To enable profiling, first require 'liquid/profiler'.
# Then, to profile a parse/render cycle, pass the profile: true option to Liquid::Template.parse.
# After Liquid::Template#render is called, the template object makes available an instance of this
# class via the Liquid::Template#profiler method.
#
# template = Liquid::Template.parse(template_content, profile: true)
# output = template.render
# profile = template.profiler
#
# This object contains all profiling information, containing information on what tags were rendered,
# where in the templates these tags live, and how long each tag took to render.
#
# This is a tree structure that is Enumerable all the way down, and keeps track of tags and rendering times
# inside of {% include %} tags.
#
# profile.each do |node|
# # Access to the node itself
# node.code
#
# # Which template and line number of this node.
# # The top-level template name is `nil` by default, but can be set in the Liquid::Context before rendering.
# node.partial
# node.line_number
#
# # Render time in seconds of this node
# node.render_time
#
# # If the template used {% include %}, this node will also have children.
# node.children.each do |child2|
# # ...
# end
# end
#
# Profiler also exposes the total time of the template's render in Liquid::Profiler#total_render_time.
#
# All render times are in seconds. There is a small performance hit when profiling is enabled.
#
class Profiler
include Enumerable
class Timing
attr_reader :code, :template_name, :line_number, :children
attr_accessor :total_time
alias_method :render_time, :total_time
alias_method :partial, :template_name
def initialize(code: nil, template_name: nil, line_number: nil)
@code = code
@template_name = template_name
@line_number = line_number
@children = []
end
def self_time
@self_time ||= begin
total_children_time = 0.0
@children.each do |child|
total_children_time += child.total_time
end
@total_time - total_children_time
end
end
end
attr_reader :total_time
alias_method :total_render_time, :total_time
def initialize
@root_children = []
@current_children = nil
@total_time = 0.0
end
def profile(template_name, &block)
# nested renders are done from a tag that already has a timing node
return yield if @current_children
root_children = @root_children
render_idx = root_children.length
begin
@current_children = root_children
profile_node(template_name, &block)
ensure
@current_children = nil
if (timing = root_children[render_idx])
@total_time += timing.total_time
end
end
end
def children
children = @root_children
if children.length == 1
children.first.children
else
children
end
end
def each(&block)
children.each(&block)
end
def [](idx)
children[idx]
end
def length
children.length
end
def profile_node(template_name, code: nil, line_number: nil)
timing = Timing.new(code: code, template_name: template_name, line_number: line_number)
parent_children = @current_children
start_time = monotonic_time
begin
@current_children = timing.children
yield
ensure
@current_children = parent_children
timing.total_time = monotonic_time - start_time
parent_children << timing
end
end
private
def monotonic_time
Process.clock_gettime(Process::CLOCK_MONOTONIC)
end
end
end
================================================
FILE: lib/liquid/range_lookup.rb
================================================
# frozen_string_literal: true
module Liquid
class RangeLookup
def self.parse(start_markup, end_markup, string_scanner, cache = nil)
start_obj = Expression.parse(start_markup, string_scanner, cache)
end_obj = Expression.parse(end_markup, string_scanner, cache)
if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate)
new(start_obj, end_obj)
else
begin
start_obj.to_i..end_obj.to_i
rescue NoMethodError
invalid_expr = start_markup unless start_obj.respond_to?(:to_i)
invalid_expr ||= end_markup unless end_obj.respond_to?(:to_i)
if invalid_expr
raise Liquid::SyntaxError, "Invalid expression type '#{invalid_expr}' in range expression"
end
raise
end
end
end
attr_reader :start_obj, :end_obj
def initialize(start_obj, end_obj)
@start_obj = start_obj
@end_obj = end_obj
end
def evaluate(context)
start_int = to_integer(context.evaluate(@start_obj))
end_int = to_integer(context.evaluate(@end_obj))
start_int..end_int
end
private
def to_integer(input)
case input
when Integer
input
when NilClass, String
input.to_i
else
Utils.to_integer(input)
end
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[@node.start_obj, @node.end_obj]
end
end
end
end
================================================
FILE: lib/liquid/registers.rb
================================================
# frozen_string_literal: true
module Liquid
class Registers
attr_reader :static
def initialize(registers = {})
@static = registers.is_a?(Registers) ? registers.static : registers
@changes = {}
end
def []=(key, value)
@changes[key] = value
end
def [](key)
if @changes.key?(key)
@changes[key]
else
@static[key]
end
end
def delete(key)
@changes.delete(key)
end
UNDEFINED = Object.new
def fetch(key, default = UNDEFINED, &block)
if @changes.key?(key)
@changes.fetch(key)
elsif default != UNDEFINED
if block_given?
@static.fetch(key, &block)
else
@static.fetch(key, default)
end
else
@static.fetch(key, &block)
end
end
def key?(key)
@changes.key?(key) || @static.key?(key)
end
end
# Alias for backwards compatibility
StaticRegisters = Registers
end
================================================
FILE: lib/liquid/resource_limits.rb
================================================
# frozen_string_literal: true
module Liquid
class ResourceLimits
attr_accessor :render_length_limit,
:render_score_limit,
:assign_score_limit,
:cumulative_render_score_limit,
:cumulative_assign_score_limit
attr_reader :render_score,
:assign_score,
:cumulative_render_score,
:cumulative_assign_score
def initialize(limits)
@render_length_limit = limits[:render_length_limit]
@render_score_limit = limits[:render_score_limit]
@assign_score_limit = limits[:assign_score_limit]
@cumulative_render_score_limit = limits[:cumulative_render_score_limit]
@cumulative_assign_score_limit = limits[:cumulative_assign_score_limit]
@cumulative_render_score = 0
@cumulative_assign_score = 0
reset
end
def increment_render_score(amount)
@render_score += amount
@cumulative_render_score += amount
raise_limits_reached if @render_score_limit && @render_score > @render_score_limit
raise_limits_reached if @cumulative_render_score_limit && @cumulative_render_score > @cumulative_render_score_limit
end
def increment_assign_score(amount)
@assign_score += amount
@cumulative_assign_score += amount
raise_limits_reached if @assign_score_limit && @assign_score > @assign_score_limit
raise_limits_reached if @cumulative_assign_score_limit && @cumulative_assign_score > @cumulative_assign_score_limit
end
# update either render_length or assign_score based on whether or not the writes are captured
def increment_write_score(output)
if (last_captured = @last_capture_length)
captured = output.bytesize
increment = captured - last_captured
@last_capture_length = captured
increment_assign_score(increment)
elsif @render_length_limit && output.bytesize > @render_length_limit
raise_limits_reached
end
end
def raise_limits_reached
@reached_limit = true
raise MemoryError, "Memory limits exceeded"
end
def reached?
@reached_limit
end
def reset
@reached_limit = false
@last_capture_length = nil
@render_score = @assign_score = 0
raise_limits_reached if @cumulative_render_score_limit && @cumulative_render_score > @cumulative_render_score_limit
raise_limits_reached if @cumulative_assign_score_limit && @cumulative_assign_score > @cumulative_assign_score_limit
end
def with_capture
old_capture_length = @last_capture_length
begin
@last_capture_length = 0
yield
ensure
@last_capture_length = old_capture_length
end
end
end
end
================================================
FILE: lib/liquid/standardfilters.rb
================================================
# frozen_string_literal: true
require 'cgi'
require 'base64'
require 'bigdecimal'
module Liquid
module StandardFilters
MAX_I32 = (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 = {
'&' => '&',
'>' => '>',
'<' => '<',
'"' => '"',
"'" => ''',
}.freeze
HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/
STRIP_HTML_BLOCKS = Regexp.union(
%r{}m,
//m,
%r{}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_type filter
# @liquid_category array
# @liquid_summary
# Returns the size of a string or array.
# @liquid_description
# The size of a string is the number of characters that the string includes. The size of an array is the number of items
# in the array.
# @liquid_syntax variable | size
# @liquid_return [number]
def size(input)
input.respond_to?(:size) ? input.size : 0
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Converts a string to all lowercase characters.
# @liquid_syntax string | downcase
# @liquid_return [string]
def downcase(input)
Utils.to_s(input).downcase
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Converts a string to all uppercase characters.
# @liquid_syntax string | upcase
# @liquid_return [string]
def upcase(input)
Utils.to_s(input).upcase
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Capitalizes the first word in a string and downcases the remaining characters.
# @liquid_syntax string | capitalize
# @liquid_return [string]
def capitalize(input)
Utils.to_s(input).capitalize
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @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.".
# @liquid_syntax string | escape
# @liquid_return [string]
def escape(input)
CGI.escapeHTML(Utils.to_s(input)) unless input.nil?
end
alias_method :h, :escape
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Escapes a string without changing characters that have already been escaped.
# @liquid_syntax string | escape_once
# @liquid_return [string]
def escape_once(input)
Utils.to_s(input).gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE)
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Converts any URL-unsafe characters in a string to the
# [percent-encoded](https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding) equivalent.
# @liquid_description
# > Note:
# > Spaces are converted to a `+` character, instead of a percent-encoded character.
# @liquid_syntax string | url_encode
# @liquid_return [string]
def url_encode(input)
CGI.escape(Utils.to_s(input)) unless input.nil?
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Decodes any [percent-encoded](https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding) characters
# in a string.
# @liquid_syntax string | url_decode
# @liquid_return [string]
def url_decode(input)
return if input.nil?
result = CGI.unescape(Utils.to_s(input))
raise Liquid::ArgumentError, "invalid byte sequence in #{result.encoding}" unless result.valid_encoding?
result
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Encodes a string to [Base64 format](https://developer.mozilla.org/en-US/docs/Glossary/Base64).
# @liquid_syntax string | base64_encode
# @liquid_return [string]
def base64_encode(input)
Base64.strict_encode64(Utils.to_s(input))
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Decodes a string in [Base64 format](https://developer.mozilla.org/en-US/docs/Glossary/Base64).
# @liquid_syntax string | base64_decode
# @liquid_return [string]
def base64_decode(input)
input = Utils.to_s(input)
StandardFilters.try_coerce_encoding(Base64.strict_decode64(input), encoding: input.encoding)
rescue ::ArgumentError
raise Liquid::ArgumentError, "invalid base64 provided to base64_decode"
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Encodes a string to URL-safe [Base64 format](https://developer.mozilla.org/en-US/docs/Glossary/Base64).
# @liquid_syntax string | base64_url_safe_encode
# @liquid_return [string]
def base64_url_safe_encode(input)
Base64.urlsafe_encode64(Utils.to_s(input))
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Decodes a string in URL-safe [Base64 format](https://developer.mozilla.org/en-US/docs/Glossary/Base64).
# @liquid_syntax string | base64_url_safe_decode
# @liquid_return [string]
def base64_url_safe_decode(input)
input = Utils.to_s(input)
StandardFilters.try_coerce_encoding(Base64.urlsafe_decode64(input), encoding: input.encoding)
rescue ::ArgumentError
raise Liquid::ArgumentError, "invalid base64 provided to base64_url_safe_decode"
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Returns a substring or series of array items, starting at a given 0-based index.
# @liquid_description
# By default, the substring has a length of one character, and the array series has one array item. However, you can
# provide a second parameter to specify the number of characters or array items.
# @liquid_syntax string | slice
# @liquid_return [string]
def slice(input, offset, length = nil)
offset = Utils.to_integer(offset)
length = length ? Utils.to_integer(length) : 1
begin
if input.is_a?(Array)
input.slice(offset, length) || []
else
Utils.to_s(input).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
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Truncates a string down to a given number of characters.
# @liquid_description
# If the specified number of characters is less than the length of the string, then an ellipsis (`...`) is appended to
# the truncated string. The ellipsis is included in the character count of the truncated string.
# @liquid_syntax string | truncate: number
# @liquid_return [string]
def truncate(input, length = 50, truncate_string = "...")
return if input.nil?
input_str = Utils.to_s(input)
length = Utils.to_integer(length)
truncate_string_str = Utils.to_s(truncate_string)
l = length - truncate_string_str.length
l = 0 if l < 0
input_str.length > length ? input_str[0...l].concat(truncate_string_str) : input_str
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Truncates a string down to a given number of words.
# @liquid_description
# If the specified number of words is less than the number of words in the string, then an ellipsis (`...`) is appended to
# the truncated string.
#
# > Caution:
# > HTML tags are treated as words, so you should strip any HTML from truncated content. If you don't strip HTML, then
# > closing HTML tags can be removed, which can result in unexpected behavior.
# @liquid_syntax string | truncatewords: number
# @liquid_return [string]
def truncatewords(input, words = 15, truncate_string = "...")
return if input.nil?
input = Utils.to_s(input)
words = Utils.to_integer(words)
words = 1 if words <= 0
wordlist = begin
input.split(" ", words + 1)
rescue RangeError
# integer too big for String#split, but we can semantically assume no truncation is needed
return input if words + 1 > MAX_I32
raise # unexpected error
end
return input if wordlist.length <= words
wordlist.pop
truncate_string = Utils.to_s(truncate_string)
wordlist.join(" ").concat(truncate_string)
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Splits a string into an array of substrings based on a given separator.
# @liquid_syntax string | split: string
# @liquid_return [array[string]]
def split(input, pattern)
pattern = Utils.to_s(pattern)
input = Utils.to_s(input)
input.split(pattern)
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Removes leading and trailing whitespace and collapses consecutive whitespace to a single space.
# @liquid_syntax string | squish
# @liquid_return [string]
def squish(input)
return if input.nil?
Utils.to_s(input).strip.gsub(/\s+/, ' ')
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Strips all whitespace from the left and right of a string.
# @liquid_syntax string | strip
# @liquid_return [string]
def strip(input)
input = Utils.to_s(input)
input.strip
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Strips all whitespace from the left of a string.
# @liquid_syntax string | lstrip
# @liquid_return [string]
def lstrip(input)
input = Utils.to_s(input)
input.lstrip
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Strips all whitespace from the right of a string.
# @liquid_syntax string | rstrip
# @liquid_return [string]
def rstrip(input)
input = Utils.to_s(input)
input.rstrip
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Strips all HTML tags from a string.
# @liquid_syntax string | strip_html
# @liquid_return [string]
def strip_html(input)
input = Utils.to_s(input)
empty = ''
result = input.gsub(STRIP_HTML_BLOCKS, empty)
result.gsub!(STRIP_HTML_TAGS, empty)
result
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Strips all newline characters (line breaks) from a string.
# @liquid_syntax string | strip_newlines
# @liquid_return [string]
def strip_newlines(input)
input = Utils.to_s(input)
input.gsub(/\r?\n/, '')
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Combines all of the items in an array into a single string, separated by a space.
# @liquid_syntax array | join
# @liquid_return [string]
def join(input, glue = ' ')
glue = Utils.to_s(glue)
InputIterator.new(input, context).join(glue)
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Sorts the items in an array in case-sensitive alphabetical, or numerical, order.
# @liquid_syntax array | sort
# @liquid_return [array[untyped]]
def sort(input, property = nil)
ary = InputIterator.new(input, context)
return [] if ary.empty?
if property.nil?
ary.sort do |a, b|
nil_safe_compare(a, b)
end
elsif ary.all? { |el| el.respond_to?(:[]) }
begin
ary.sort { |a, b| nil_safe_compare(a[property], b[property]) }
rescue TypeError
raise_property_error(property)
end
end
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Sorts the items in an array in case-insensitive alphabetical order.
# @liquid_description
# > Caution:
# > You shouldn't use the `sort_natural` filter to sort numerical values. When comparing items an array, each item is converted to a
# > string, so sorting on numerical values can lead to unexpected results.
# @liquid_syntax array | sort_natural
# @liquid_return [array[untyped]]
def sort_natural(input, property = nil)
ary = InputIterator.new(input, context)
return [] if ary.empty?
if property.nil?
ary.sort do |a, b|
nil_safe_casecmp(a, b)
end
elsif ary.all? { |el| el.respond_to?(:[]) }
begin
ary.sort { |a, b| nil_safe_casecmp(a[property], b[property]) }
rescue TypeError
raise_property_error(property)
end
end
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Filters an array to include only items with a specific property value.
# @liquid_description
# This requires you to provide both the property name and the associated value.
# @liquid_syntax array | where: string, string
# @liquid_return [array[untyped]]
def where(input, property, target_value = nil)
filter_array(input, property, target_value) { |ary, &block| ary.select(&block) }
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Filters an array to exclude items with a specific property value.
# @liquid_description
# This requires you to provide both the property name and the associated value.
# @liquid_syntax array | reject: string, string
# @liquid_return [array[untyped]]
def reject(input, property, target_value = nil)
filter_array(input, property, target_value) { |ary, &block| ary.reject(&block) }
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Tests if any item in an array has a specific property value.
# @liquid_description
# This requires you to provide both the property name and the associated value.
# @liquid_syntax array | has: string, string
# @liquid_return [boolean]
def has(input, property, target_value = nil)
filter_array(input, property, target_value, false) { |ary, &block| ary.any?(&block) }
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Returns the first item in an array with a specific property value.
# @liquid_description
# This requires you to provide both the property name and the associated value.
# @liquid_syntax array | find: string, string
# @liquid_return [untyped]
def find(input, property, target_value = nil)
filter_array(input, property, target_value, nil) { |ary, &block| ary.find(&block) }
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Returns the index of the first item in an array with a specific property value.
# @liquid_description
# This requires you to provide both the property name and the associated value.
# @liquid_syntax array | find_index: string, string
# @liquid_return [number]
def find_index(input, property, target_value = nil)
filter_array(input, property, target_value, nil) { |ary, &block| ary.find_index(&block) }
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Removes any duplicate items in an array.
# @liquid_syntax array | uniq
# @liquid_return [array[untyped]]
def uniq(input, property = nil)
ary = InputIterator.new(input, context)
if property.nil?
ary.uniq
elsif ary.empty? # The next two cases assume a non-empty array.
[]
else
ary.uniq do |item|
item[property]
rescue TypeError
raise_property_error(property)
rescue NoMethodError
return nil unless item.respond_to?(:[])
raise
end
end
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Reverses the order of the items in an array.
# @liquid_syntax array | reverse
# @liquid_return [array[untyped]]
def reverse(input)
ary = InputIterator.new(input, context)
ary.reverse
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Creates an array of values from a specific property of the items in an array.
# @liquid_syntax array | map: string
# @liquid_return [array[untyped]]
def map(input, property)
InputIterator.new(input, context).map do |e|
e = e.call if e.is_a?(Proc)
if property == "to_liquid"
e
elsif e.respond_to?(:[])
r = e[property]
r.is_a?(Proc) ? r.call : r
end
end
rescue TypeError
raise_property_error(property)
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Removes any `nil` items from an array.
# @liquid_syntax array | compact
# @liquid_return [array[untyped]]
def compact(input, property = nil)
ary = InputIterator.new(input, context)
if property.nil?
ary.compact
elsif ary.empty? # The next two cases assume a non-empty array.
[]
else
ary.reject do |item|
item[property].nil?
rescue TypeError
raise_property_error(property)
rescue NoMethodError
return nil unless item.respond_to?(:[])
raise
end
end
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Replaces any instance of a substring inside a string with a given string.
# @liquid_syntax string | replace: string, string
# @liquid_return [string]
def replace(input, string, replacement = '')
string = Utils.to_s(string)
replacement = Utils.to_s(replacement)
input = Utils.to_s(input)
input.gsub(string, replacement)
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Replaces the first instance of a substring inside a string with a given string.
# @liquid_syntax string | replace_first: string, string
# @liquid_return [string]
def replace_first(input, string, replacement = '')
string = Utils.to_s(string)
replacement = Utils.to_s(replacement)
input = Utils.to_s(input)
input.sub(string, replacement)
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Replaces the last instance of a substring inside a string with a given string.
# @liquid_syntax string | replace_last: string, string
# @liquid_return [string]
def replace_last(input, string, replacement)
input = Utils.to_s(input)
string = Utils.to_s(string)
replacement = Utils.to_s(replacement)
start_index = input.rindex(string)
return input unless start_index
output = input.dup
output[start_index, string.length] = replacement
output
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Removes any instance of a substring inside a string.
# @liquid_syntax string | remove: string
# @liquid_return [string]
def remove(input, string)
replace(input, string, '')
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Removes the first instance of a substring inside a string.
# @liquid_syntax string | remove_first: string
# @liquid_return [string]
def remove_first(input, string)
replace_first(input, string, '')
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Removes the last instance of a substring inside a string.
# @liquid_syntax string | remove_last: string
# @liquid_return [string]
def remove_last(input, string)
replace_last(input, string, '')
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Adds a given string to the end of a string.
# @liquid_syntax string | append: string
# @liquid_return [string]
def append(input, string)
input = Utils.to_s(input)
string = Utils.to_s(string)
input + string
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Concatenates (combines) two arrays.
# @liquid_description
# > Note:
# > 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).
# @liquid_syntax array | concat: array
# @liquid_return [array[untyped]]
def concat(input, array)
unless array.respond_to?(:to_ary)
raise ArgumentError, "concat filter requires an array argument"
end
InputIterator.new(input, context).concat(array)
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Adds a given string to the beginning of a string.
# @liquid_syntax string | prepend: string
# @liquid_return [string]
def prepend(input, string)
input = Utils.to_s(input)
string = Utils.to_s(string)
string + input
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Converts newlines (`\n`) in a string to HTML line breaks (` `).
# @liquid_syntax string | newline_to_br
# @liquid_return [string]
def newline_to_br(input)
input = Utils.to_s(input)
input.gsub(/\r?\n/, " \n")
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category date
# @liquid_summary
# Formats a date according to a specified format string.
# @liquid_description
# This filter formats a date using various format specifiers. If the format string is empty,
# the original input is returned. If the input cannot be converted to a date, the original input is returned.
#
# The following format specifiers can be used:
#
# %a - The abbreviated weekday name (``Sun'')
# %A - The full weekday name (``Sunday'')
# %b - The abbreviated month name (``Jan'')
# %B - The full month name (``January'')
# %c - The preferred local date and time representation
# %d - Day of the month (01..31)
# %H - Hour of the day, 24-hour clock (00..23)
# %I - Hour of the day, 12-hour clock (01..12)
# %j - Day of the year (001..366)
# %m - Month of the year (01..12)
# %M - Minute of the hour (00..59)
# %p - Meridian indicator (``AM'' or ``PM'')
# %s - Number of seconds since 1970-01-01 00:00:00 UTC.
# %S - Second of the minute (00..60)
# %U - Week number of the current year,
# starting with the first Sunday as the first
# day of the first week (00..53)
# %W - Week number of the current year,
# starting with the first Monday as the first
# day of the first week (00..53)
# %w - Day of the week (Sunday is 0, 0..6)
# %x - Preferred representation for the date alone, no time
# %X - Preferred representation for the time alone, no date
# %y - Year without a century (00..99)
# %Y - Year with century
# %Z - Time zone name
# %% - Literal ``%'' character
# @liquid_syntax date | date: string
# @liquid_return [string]
def date(input, format)
str_format = Utils.to_s(format)
return input if str_format.empty?
return input unless (date = Utils.to_date(input))
date.strftime(str_format)
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Returns the first item in an array.
# @liquid_syntax array | first
# @liquid_return [untyped]
def first(array)
# ActiveSupport returns "" for empty strings, not nil
return array[0] || "" if array.is_a?(String)
array.first if array.respond_to?(:first)
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Returns the last item in an array.
# @liquid_syntax array | last
# @liquid_return [untyped]
def last(array)
# ActiveSupport returns "" for empty strings, not nil
return array[-1] || "" if array.is_a?(String)
array.last if array.respond_to?(:last)
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category math
# @liquid_summary
# Returns the absolute value of a number.
# @liquid_syntax number | abs
# @liquid_return [number]
def abs(input)
result = Utils.to_number(input).abs
result.is_a?(BigDecimal) ? result.to_f : result
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category math
# @liquid_summary
# Adds two numbers.
# @liquid_syntax number | plus: number
# @liquid_return [number]
def plus(input, operand)
apply_operation(input, operand, :+)
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category math
# @liquid_summary
# Subtracts a given number from another number.
# @liquid_syntax number | minus: number
# @liquid_return [number]
def minus(input, operand)
apply_operation(input, operand, :-)
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category math
# @liquid_summary
# Multiplies a number by a given number.
# @liquid_syntax number | times: number
# @liquid_return [number]
def times(input, operand)
apply_operation(input, operand, :*)
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category math
# @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.
# @liquid_syntax number | divided_by: number
# @liquid_return [number]
def divided_by(input, operand)
apply_operation(input, operand, :/)
rescue ::ZeroDivisionError => e
raise Liquid::ZeroDivisionError, e.message
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category math
# @liquid_summary
# Returns the remainder of dividing a number by a given number.
# @liquid_syntax number | modulo: number
# @liquid_return [number]
def modulo(input, operand)
apply_operation(input, operand, :%)
rescue ::ZeroDivisionError => e
raise Liquid::ZeroDivisionError, e.message
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category math
# @liquid_summary
# Rounds a number to the nearest integer.
# @liquid_syntax number | round
# @liquid_return [number]
def round(input, n = 0)
result = Utils.to_number(input).round(Utils.to_number(n))
result = result.to_f if result.is_a?(BigDecimal)
result = result.to_i if n == 0
result
rescue ::FloatDomainError => e
raise Liquid::FloatDomainError, e.message
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category math
# @liquid_summary
# Rounds a number up to the nearest integer.
# @liquid_syntax number | ceil
# @liquid_return [number]
def ceil(input)
Utils.to_number(input).ceil.to_i
rescue ::FloatDomainError => e
raise Liquid::FloatDomainError, e.message
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category math
# @liquid_summary
# Rounds a number down to the nearest integer.
# @liquid_syntax number | floor
# @liquid_return [number]
def floor(input)
Utils.to_number(input).floor.to_i
rescue ::FloatDomainError => e
raise Liquid::FloatDomainError, e.message
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category math
# @liquid_summary
# Limits a number to a minimum value.
# @liquid_syntax number | at_least
# @liquid_return [number]
def at_least(input, n)
min_value = Utils.to_number(n)
result = Utils.to_number(input)
result = min_value if min_value > result
result.is_a?(BigDecimal) ? result.to_f : result
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category math
# @liquid_summary
# Limits a number to a maximum value.
# @liquid_syntax number | at_most
# @liquid_return [number]
def at_most(input, n)
max_value = Utils.to_number(n)
result = Utils.to_number(input)
result = max_value if max_value < result
result.is_a?(BigDecimal) ? result.to_f : result
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category default
# @liquid_summary
# Sets a default value for any variable whose value is one of the following:
#
# - [`empty`](/docs/api/liquid/basics#empty)
# - [`false`](/docs/api/liquid/basics#truthy-and-falsy)
# - [`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.
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
def filter_array(input, property, target_value, default_value = [], &block)
ary = InputIterator.new(input, context)
return default_value if ary.empty?
block.call(ary) do |item|
if target_value.nil?
item[property]
else
item[property] == target_value
end
rescue TypeError
raise_property_error(property)
rescue NoMethodError
return nil unless item.respond_to?(:[])
raise
end
end
def raise_property_error(property)
raise Liquid::ArgumentError, "cannot select the property '#{Utils.to_s(property)}'"
end
def apply_operation(input, operand, operation)
result = Utils.to_number(input).send(operation, Utils.to_number(operand))
result.is_a?(BigDecimal) ? result.to_f : result
end
def nil_safe_compare(a, b)
result = a <=> b
if result
result
elsif a.nil?
1
elsif b.nil?
-1
else
raise Liquid::ArgumentError, "cannot sort values of incompatible types"
end
end
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
end
class InputIterator
include Enumerable
def initialize(input, context)
@context = context
@input = if input.is_a?(Array)
input.flatten
elsif input.is_a?(Hash)
[input]
elsif input.is_a?(Enumerable)
input
else
Array(input)
end
end
def join(glue)
first = true
output = +""
each do |item|
if first
first = false
else
output << glue
end
output << Liquid::Utils.to_s(item)
end
output
end
def concat(args)
to_a.concat(args)
end
def reverse
reverse_each.to_a
end
def uniq(&block)
to_a.uniq do |item|
item = Utils.to_liquid_value(item)
block ? yield(item) : item
end
end
def compact
to_a.compact
end
def empty?
@input.each { return false }
true
end
def each
@input.each do |e|
e = e.respond_to?(:to_liquid) ? e.to_liquid : e
e.context = @context if e.respond_to?(:context=)
yield(e)
end
end
end
end
end
================================================
FILE: lib/liquid/strainer_template.rb
================================================
# frozen_string_literal: true
require 'set'
module Liquid
# StrainerTemplate is the computed class for the filters system.
# New filters are mixed into the strainer class which is then instantiated for each liquid template render run.
#
# The Strainer only allows method calls defined in filters given to it via StrainerFactory.add_global_filter,
# Context#add_filters or Template.register_filter
class StrainerTemplate
def initialize(context)
@context = context
end
class << self
def add_filter(filter)
return if include?(filter)
invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) }
if invokable_non_public_methods.any?
raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}"
end
include(filter)
filter_methods.merge(filter.public_instance_methods.map(&:to_s))
end
def invokable?(method)
filter_methods.include?(method.to_s)
end
def inherited(subclass)
super
subclass.instance_variable_set(:@filter_methods, @filter_methods.dup)
end
def filter_method_names
filter_methods.map(&:to_s).to_a
end
private
def filter_methods
@filter_methods ||= Set.new
end
end
def invoke(method, *args)
if self.class.invokable?(method)
send(method, *args)
elsif @context.strict_filters
raise Liquid::UndefinedFilter, "undefined filter #{method}"
else
args.first
end
rescue ::ArgumentError => e
raise Liquid::ArgumentError, e.message, e.backtrace
end
end
end
================================================
FILE: lib/liquid/tablerowloop_drop.rb
================================================
# frozen_string_literal: true
module Liquid
# @liquid_public_docs
# @liquid_type object
# @liquid_name tablerowloop
# @liquid_summary
# Information about a parent [`tablerow` loop](/docs/api/liquid/tags/tablerow).
class TablerowloopDrop < Drop
def initialize(length, cols)
@length = length
@row = 1
@col = 1
@cols = cols
@index = 0
end
# @liquid_public_docs
# @liquid_summary
# The total number of iterations in the loop.
# @liquid_return [number]
attr_reader :length
# @liquid_public_docs
# @liquid_summary
# The 1-based index of the current column.
# @liquid_return [number]
attr_reader :col
# @liquid_public_docs
# @liquid_summary
# The 1-based index of current row.
# @liquid_return [number]
attr_reader :row
# @liquid_public_docs
# @liquid_summary
# The 1-based index of the current iteration.
# @liquid_return [number]
def index
@index + 1
end
# @liquid_public_docs
# @liquid_summary
# The 0-based index of the current iteration.
# @liquid_return [number]
def index0
@index
end
# @liquid_public_docs
# @liquid_summary
# The 0-based index of the current column.
# @liquid_return [number]
def col0
@col - 1
end
# @liquid_public_docs
# @liquid_summary
# The 1-based index of the current iteration, in reverse order.
# @liquid_return [number]
def rindex
@length - @index
end
# @liquid_public_docs
# @liquid_summary
# The 0-based index of the current iteration, in reverse order.
# @liquid_return [number]
def rindex0
@length - @index - 1
end
# @liquid_public_docs
# @liquid_summary
# Returns `true` if the current iteration is the first. Returns `false` if not.
# @liquid_return [boolean]
def first
@index == 0
end
# @liquid_public_docs
# @liquid_summary
# Returns `true` if the current iteration is the last. Returns `false` if not.
# @liquid_return [boolean]
def last
@index == @length - 1
end
# @liquid_public_docs
# @liquid_summary
# Returns `true` if the current column is the first in the row. Returns `false` if not.
# @liquid_return [boolean]
def col_first
@col == 1
end
# @liquid_public_docs
# @liquid_summary
# Returns `true` if the current column is the last in the row. Returns `false` if not.
# @liquid_return [boolean]
def col_last
@col == @cols
end
protected
def increment!
@index += 1
if @col == @cols
@col = 1
@row += 1
else
@col += 1
end
end
end
end
================================================
FILE: lib/liquid/tag/disableable.rb
================================================
# frozen_string_literal: true
module Liquid
class Tag
module Disableable
def render_to_output_buffer(context, output)
if context.tag_disabled?(tag_name)
output << disabled_error(context)
return
end
super
end
def disabled_error(context)
# raise then rescue the exception so that the Context#exception_renderer can re-raise it
raise DisabledError, "#{tag_name} #{parse_context[:locale].t('errors.disabled.tag')}"
rescue DisabledError => exc
context.handle_error(exc, line_number)
end
end
end
end
================================================
FILE: lib/liquid/tag/disabler.rb
================================================
# frozen_string_literal: true
module Liquid
class Tag
module Disabler
def render_to_output_buffer(context, output)
context.with_disabled_tags(self.class.disabled_tags) do
super
end
end
end
end
end
================================================
FILE: lib/liquid/tag.rb
================================================
# 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
alias_method :options, :parse_context
include ParserSwitching
class << self
def parse(tag_name, markup, tokenizer, parse_context)
tag = new(tag_name, markup, parse_context)
tag.parse(tokenizer)
tag
end
def disable_tags(*tag_names)
tag_names += disabled_tags
define_singleton_method(:disabled_tags) { tag_names }
prepend(Disabler)
end
private :new
protected
def disabled_tags
[]
end
end
def initialize(tag_name, markup, parse_context)
@tag_name = tag_name
@markup = markup
@parse_context = parse_context
@line_number = parse_context.line_number
end
def parse(_tokens)
end
def raw
"#{@tag_name} #{@markup}"
end
def name
self.class.name.downcase
end
def render(_context)
''
end
# For backwards compatibility with custom tags. In a future release, the semantics
# 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
end
def blank?
false
end
private
def safe_parse_expression(parser)
parse_context.safe_parse_expression(parser)
end
def parse_expression(markup, safe: false)
parse_context.parse_expression(markup, safe: safe)
end
end
end
================================================
FILE: lib/liquid/tags/assign.rb
================================================
# frozen_string_literal: true
module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category variable
# @liquid_name assign
# @liquid_summary
# Creates a new variable.
# @liquid_description
# You can create variables of any [basic type](/docs/api/liquid/basics#types), [object](/docs/api/liquid/objects), or object property.
#
# > Caution:
# > Predefined Liquid objects can be overridden by variables with the same name.
# > To make sure that you can access all Liquid objects, make sure that your variable name doesn't match a predefined object's name.
# @liquid_syntax
# {% assign variable_name = value %}
# @liquid_syntax_keyword variable_name The name of the variable being created.
# @liquid_syntax_keyword value The value you want to assign to the variable.
class Assign < Tag
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
# @api private
def self.raise_syntax_error(parse_context)
raise Liquid::SyntaxError, parse_context.locale.t('errors.syntax.assign')
end
attr_reader :to, :from
def initialize(tag_name, markup, parse_context)
super
if markup =~ Syntax
@to = Regexp.last_match(1)
@from = Variable.new(Regexp.last_match(2), parse_context)
else
self.class.raise_syntax_error(parse_context)
end
end
def render_to_output_buffer(context, output)
val = @from.render(context)
context.scopes.last[@to] = val
context.resource_limits.increment_assign_score(assign_score_of(val))
output
end
def blank?
true
end
private
def assign_score_of(val)
if val.instance_of?(String)
val.bytesize
elsif val.instance_of?(Array)
sum = 1
# Uses #each to avoid extra allocations.
val.each { |child| sum += assign_score_of(child) }
sum
elsif val.instance_of?(Hash)
sum = 1
val.each do |key, entry_value|
sum += assign_score_of(key)
sum += assign_score_of(entry_value)
end
sum
else
1
end
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[@node.from]
end
end
end
end
================================================
FILE: lib/liquid/tags/break.rb
================================================
# frozen_string_literal: true
module Liquid
# Break tag to be used to break out of a for loop.
#
# == Basic Usage:
# {% for item in collection %}
# {% if item.condition %}
# {% break %}
# {% endif %}
# {% endfor %}
#
# @liquid_public_docs
# @liquid_type tag
# @liquid_category iteration
# @liquid_name break
# @liquid_summary
# Stops a [`for` loop](/docs/api/liquid/tags/for) from iterating.
# @liquid_syntax
# {% break %}
class Break < Tag
INTERRUPT = BreakInterrupt.new.freeze
def render_to_output_buffer(context, output)
context.push_interrupt(INTERRUPT)
output
end
end
end
================================================
FILE: lib/liquid/tags/capture.rb
================================================
# frozen_string_literal: true
module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category variable
# @liquid_name capture
# @liquid_summary
# Creates a new variable with a string value.
# @liquid_description
# You can create complex strings with Liquid logic and variables.
#
# > Caution:
# > Predefined Liquid objects can be overridden by variables with the same name.
# > To make sure that you can access all Liquid objects, make sure that your variable name doesn't match a predefined object's name.
# @liquid_syntax
# {% capture variable %}
# value
# {% endcapture %}
# @liquid_syntax_keyword variable The name of the variable being created.
# @liquid_syntax_keyword value The value you want to assign to the variable.
class Capture < Block
Syntax = /(#{VariableSignature}+)/o
def initialize(tag_name, markup, options)
super
if markup =~ Syntax
@to = Regexp.last_match(1)
else
raise SyntaxError, options[:locale].t("errors.syntax.capture")
end
end
def render_to_output_buffer(context, output)
context.resource_limits.with_capture do
capture_output = render(context)
context.scopes.last[@to] = capture_output
end
output
end
def blank?
true
end
end
end
================================================
FILE: lib/liquid/tags/case.rb
================================================
# frozen_string_literal: true
module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category conditional
# @liquid_name case
# @liquid_summary
# Renders a specific expression depending on the value of a specific variable.
# @liquid_syntax
# {% case variable %}
# {% when first_value %}
# first_expression
# {% when second_value %}
# second_expression
# {% else %}
# third_expression
# {% endcase %}
# @liquid_syntax_keyword variable The name of the variable you want to base your case statement on.
# @liquid_syntax_keyword first_value A specific value to check for.
# @liquid_syntax_keyword second_value A specific value to check for.
# @liquid_syntax_keyword first_expression An expression to be rendered when the variable's value matches `first_value`.
# @liquid_syntax_keyword second_expression An expression to be rendered when the variable's value matches `second_value`.
# @liquid_syntax_keyword third_expression An expression to be rendered when the variable's value has no match.
class Case < Block
Syntax = /(#{QuotedFragment})/o
WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om
attr_reader :blocks, :left
def initialize(tag_name, markup, options)
super
@blocks = []
parse_with_selected_parser(markup)
end
def parse(tokens)
body = case_body = new_body
body = @blocks.last.attachment while parse_body(body, tokens)
@blocks.reverse_each do |condition|
body = condition.attachment
unless body.frozen?
body.remove_blank_strings if blank?
body.freeze
end
end
case_body.freeze
end
def nodelist
@blocks.map(&:attachment)
end
def unknown_tag(tag, markup, tokens)
case tag
when 'when'
record_when_condition(markup)
when 'else'
record_else_condition(markup)
else
super
end
end
def render_to_output_buffer(context, output)
execute_else_block = true
@blocks.each do |block|
if block.else?
block.attachment.render_to_output_buffer(context, output) if execute_else_block
next
end
result = Liquid::Utils.to_liquid_value(
block.evaluate(context),
)
if result
execute_else_block = false
block.attachment.render_to_output_buffer(context, output)
end
end
output
end
private
def strict2_parse(markup)
parser = @parse_context.new_parser(markup)
@left = safe_parse_expression(parser)
parser.consume(:end_of_string)
end
def strict_parse(markup)
lax_parse(markup)
end
def lax_parse(markup)
if markup =~ Syntax
@left = parse_expression(Regexp.last_match(1))
else
raise SyntaxError, options[:locale].t("errors.syntax.case")
end
end
def record_when_condition(markup)
body = new_body
if strict2_mode?
parse_strict2_when(markup, body)
else
parse_lax_when(markup, body)
end
end
def parse_strict2_when(markup, body)
parser = @parse_context.new_parser(markup)
loop do
expr = Condition.parse_expression(parse_context, parser.expression, safe: true)
block = Condition.new(@left, '==', expr)
block.attach(body)
@blocks << block
break unless parser.id?('or') || parser.consume?(:comma)
end
parser.consume(:end_of_string)
end
def parse_lax_when(markup, body)
while markup
unless markup =~ WhenSyntax
raise SyntaxError, options[:locale].t("errors.syntax.case_invalid_when")
end
markup = Regexp.last_match(2)
block = Condition.new(@left, '==', Condition.parse_expression(parse_context, Regexp.last_match(1)))
block.attach(body)
@blocks << block
end
end
def record_else_condition(markup)
unless markup.strip.empty?
raise SyntaxError, options[:locale].t("errors.syntax.case_invalid_else")
end
block = ElseCondition.new
block.attach(new_body)
@blocks << block
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[@node.left] + @node.blocks
end
end
end
end
================================================
FILE: lib/liquid/tags/comment.rb
================================================
# frozen_string_literal: true
module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category syntax
# @liquid_name comment
# @liquid_summary
# Prevents an expression from being rendered or output.
# @liquid_description
# Any text inside `comment` tags won't be output, and any Liquid code will be parsed, but not executed.
# @liquid_syntax
# {% comment %}
# content
# {% endcomment %}
# @liquid_syntax_keyword content The content of the comment.
class Comment < Block
def render_to_output_buffer(_context, output)
output
end
def unknown_tag(_tag, _markup, _tokens)
end
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
end
================================================
FILE: lib/liquid/tags/continue.rb
================================================
# frozen_string_literal: true
module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category iteration
# @liquid_name continue
# @liquid_summary
# Causes a [`for` loop](/docs/api/liquid/tags/for) to skip to the next iteration.
# @liquid_syntax
# {% continue %}
class Continue < Tag
INTERRUPT = ContinueInterrupt.new.freeze
def render_to_output_buffer(context, output)
context.push_interrupt(INTERRUPT)
output
end
end
end
================================================
FILE: lib/liquid/tags/cycle.rb
================================================
# frozen_string_literal: true
module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category iteration
# @liquid_name cycle
# @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).
# @liquid_description
# The `cycle` tag must be used inside a `for` loop.
#
# > Tip:
# > Use the `cycle` tag to output text in a predictable pattern. For example, to apply odd/even classes to rows in a table.
# @liquid_syntax
# {% cycle string, string, ... %}
class Cycle < Tag
SimpleSyntax = /\A#{QuotedFragment}+/o
NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om
UNNAMED_CYCLE_PATTERN = /\w+:0x\h{8}/
attr_reader :variables
def initialize(tag_name, markup, options)
super
parse_with_selected_parser(markup)
end
def named?
@is_named
end
def render_to_output_buffer(context, output)
context.registers[:cycle] ||= {}
key = context.evaluate(@name)
iteration = context.registers[:cycle][key].to_i
val = context.evaluate(@variables[iteration])
if val.is_a?(Array)
val = val.join
elsif !val.is_a?(String)
val = val.to_s
end
output << val
iteration += 1
iteration = 0 if iteration >= @variables.size
context.registers[:cycle][key] = iteration
output
end
private
# cycle [name:] expression(, expression)*
def strict2_parse(markup)
p = @parse_context.new_parser(markup)
@variables = []
raise SyntaxError, options[:locale].t("errors.syntax.cycle") if p.look(:end_of_string)
first_expression = safe_parse_expression(p)
if p.look(:colon)
# cycle name: expr1, expr2, ...
@name = first_expression
@is_named = true
p.consume(:colon)
# After the colon, parse the first variable (required for named cycles)
@variables << maybe_dup_lookup(safe_parse_expression(p))
else
# cycle expr1, expr2, ...
@variables << maybe_dup_lookup(first_expression)
end
# Parse remaining comma-separated expressions
while p.consume?(:comma)
break if p.look(:end_of_string)
@variables << maybe_dup_lookup(safe_parse_expression(p))
end
p.consume(:end_of_string)
unless @is_named
@name = @variables.to_s
@is_named = !@name.match?(UNNAMED_CYCLE_PATTERN)
end
end
def strict_parse(markup)
lax_parse(markup)
end
def lax_parse(markup)
case markup
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?(UNNAMED_CYCLE_PATTERN)
else
raise SyntaxError, options[:locale].t("errors.syntax.cycle")
end
end
def variables_from_string(markup)
markup.split(',').collect do |var|
var =~ /\s*(#{QuotedFragment})\s*/o
next unless Regexp.last_match(1)
var = parse_expression(Regexp.last_match(1))
maybe_dup_lookup(var)
end.compact
end
# For backwards compatibility, whenever a lookup is used in an unnamed cycle,
# we make it so that the @variables.to_s produces different strings for cycles
# called with the same arguments (since @variables.to_s is used as the cycle counter key)
# This makes it so {% cycle a, b %} and {% cycle a, b %} have independent counters even if a and b share value.
# This is not true for literal values, {% cycle "a", "b" %} and {% cycle "a", "b" %} share the same counter.
# I was really scratching my head about this one, but migrating away from this would be more headache
# than it's worth. So we're keeping this quirk for now.
def maybe_dup_lookup(var)
var.is_a?(VariableLookup) ? var.dup : var
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
Array(@node.variables)
end
end
end
end
================================================
FILE: lib/liquid/tags/decrement.rb
================================================
# frozen_string_literal: true
module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category variable
# @liquid_name decrement
# @liquid_summary
# Creates a new variable, with a default value of -1, that's decreased by 1 with each subsequent call.
#
# > Caution:
# > Predefined Liquid objects can be overridden by variables with the same name.
# > To make sure that you can access all Liquid objects, make sure that your variable name doesn't match a predefined object's name.
# @liquid_description
# Variables that are declared with `decrement` are unique to the [layout](/themes/architecture/layouts), [template](/themes/architecture/templates),
# 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.
#
# Similarly, variables that are created with `decrement` are independent from those created with [`assign`](/docs/api/liquid/tags/assign)
# and [`capture`](/docs/api/liquid/tags/capture). However, `decrement` and [`increment`](/docs/api/liquid/tags/increment) share
# variables.
# @liquid_syntax
# {% decrement variable_name %}
# @liquid_syntax_keyword variable_name The name of the variable being decremented.
class Decrement < Tag
attr_reader :variable_name
def initialize(tag_name, markup, options)
super
@variable_name = markup.strip
end
def render_to_output_buffer(context, output)
counter_environment = context.environments.first
value = counter_environment[@variable_name] || 0
value -= 1
counter_environment[@variable_name] = value
output << value.to_s
output
end
end
end
================================================
FILE: lib/liquid/tags/doc.rb
================================================
# frozen_string_literal: true
module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category syntax
# @liquid_name doc
# @liquid_summary
# Documents template elements with annotations.
# @liquid_description
# The `doc` tag allows developers to include documentation within Liquid
# templates. Any content inside `doc` tags is not rendered or outputted.
# Liquid code inside will be parsed but not executed. This facilitates
# tooling support for features like code completion, linting, and inline
# documentation.
#
# For detailed documentation syntax and examples, see the
# [`LiquidDoc` reference](/docs/storefronts/themes/tools/liquid-doc).
#
# @liquid_syntax
# {% doc %}
# Renders a message.
#
# @param {string} foo - A string value.
# @param {string} [bar] - An optional string value.
#
# @example
# {% render 'message', foo: 'Hello', bar: 'World' %}
# {% enddoc %}
class Doc < Block
NO_UNEXPECTED_ARGS = /\A\s*\z/
def initialize(tag_name, markup, parse_context)
super
ensure_valid_markup(tag_name, markup, parse_context)
end
def parse(tokens)
@body = +""
while (token = tokens.shift)
tag_name = token =~ BlockBody::FullTokenPossiblyInvalid && Regexp.last_match(2)
raise_nested_doc_error if tag_name == @tag_name
if tag_name == block_delimiter
parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
@body << Regexp.last_match(1) if Regexp.last_match(1) != ""
return
end
@body << token unless token.empty?
end
raise_tag_never_closed(block_name)
end
def render_to_output_buffer(_context, output)
output
end
def blank?
@body.empty?
end
def nodelist
[@body]
end
private
def ensure_valid_markup(tag_name, markup, parse_context)
unless NO_UNEXPECTED_ARGS.match?(markup)
raise SyntaxError, parse_context.locale.t("errors.syntax.block_tag_unexpected_args", tag: tag_name)
end
end
def raise_nested_doc_error
raise SyntaxError, parse_context.locale.t("errors.syntax.doc_invalid_nested")
end
end
end
================================================
FILE: lib/liquid/tags/echo.rb
================================================
# frozen_string_literal: true
module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category syntax
# @liquid_name echo
# @liquid_summary
# Outputs an expression.
# @liquid_description
# 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).
#
# > Tip:
# > You can use [filters](/docs/api/liquid/filters) on expressions inside `echo` tags.
# @liquid_syntax
# {% liquid
# echo expression
# %}
# @liquid_syntax_keyword expression The expression to be output.
class Echo < Tag
attr_reader :variable
def initialize(tag_name, markup, parse_context)
super
@variable = Variable.new(markup, parse_context)
end
def render(context)
@variable.render_to_output_buffer(context, +'')
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[@node.variable]
end
end
end
end
================================================
FILE: lib/liquid/tags/for.rb
================================================
# frozen_string_literal: true
module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category iteration
# @liquid_name for
# @liquid_summary
# Renders an expression for every item in an array.
# @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
# [`paginate` tag](/docs/api/liquid/tags/paginate) to split the items over multiple pages.
#
# > Tip:
# > Every `for` loop has an associated [`forloop` object](/docs/api/liquid/objects/forloop) with information about the loop.
# @liquid_syntax
# {% for variable in array %}
# expression
# {% endfor %}
# @liquid_syntax_keyword variable The current item in the array.
# @liquid_syntax_keyword array The array to iterate over.
# @liquid_syntax_keyword expression The expression to render for each iteration.
# @liquid_optional_param limit: [number] The number of iterations to perform.
# @liquid_optional_param offset: [number] The 1-based index to start iterating at.
# @liquid_optional_param range [untyped] A custom numeric range to iterate over.
# @liquid_optional_param reversed [untyped] Iterate in reverse order.
class For < Block
Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
attr_reader :collection_name, :variable_name, :limit, :from
def initialize(tag_name, markup, options)
super
@from = @limit = nil
parse_with_selected_parser(markup)
@for_block = new_body
@else_block = nil
end
def parse(tokens)
if parse_body(@for_block, tokens)
parse_body(@else_block, tokens)
end
if blank?
@else_block&.remove_blank_strings
@for_block.remove_blank_strings
end
@else_block&.freeze
@for_block.freeze
end
def nodelist
@else_block ? [@for_block, @else_block] : [@for_block]
end
def unknown_tag(tag, markup, tokens)
return super unless tag == 'else'
@else_block = new_body
end
def render_to_output_buffer(context, output)
segment = collection_segment(context)
if segment.empty?
render_else(context, output)
else
render_segment(context, output, segment)
end
output
end
protected
def lax_parse(markup)
if markup =~ Syntax
@variable_name = Regexp.last_match(1)
collection_name = Regexp.last_match(2)
@reversed = !!Regexp.last_match(3)
@name = "#{@variable_name}-#{collection_name}"
@collection_name = parse_expression(collection_name)
markup.scan(TagAttributes) do |key, value|
set_attribute(key, value)
end
else
raise SyntaxError, options[:locale].t("errors.syntax.for")
end
end
def strict_parse(markup)
p = @parse_context.new_parser(markup)
@variable_name = p.consume(:id)
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in") unless p.id?('in')
collection_name = p.expression
@collection_name = parse_expression(collection_name, safe: true)
@name = "#{@variable_name}-#{collection_name}"
@reversed = p.id?('reversed')
while p.look(:comma) || p.look(:id)
p.consume?(:comma)
unless (attribute = p.id?('limit') || p.id?('offset'))
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_attribute")
end
p.consume(:colon)
set_attribute(attribute, p.expression, safe: true)
end
p.consume(:end_of_string)
end
private
def strict2_parse(markup)
strict_parse(markup)
end
def collection_segment(context)
offsets = context.registers[:for] ||= {}
from = if @from == :continue
offsets[@name].to_i
else
from_value = context.evaluate(@from)
if from_value.nil?
0
else
Utils.to_integer(from_value)
end
end
collection = context.evaluate(@collection_name)
collection = collection.to_a if collection.is_a?(Range)
limit_value = context.evaluate(@limit)
to = if limit_value.nil?
nil
else
Utils.to_integer(limit_value) + from
end
segment = Utils.slice_collection(collection, from, to)
segment.reverse! if @reversed
offsets[@name] = from + segment.length
segment
end
def render_segment(context, output, segment)
for_stack = context.registers[:for_stack] ||= []
length = segment.length
context.stack do
loop_vars = Liquid::ForloopDrop.new(@name, length, for_stack[-1])
for_stack.push(loop_vars)
begin
context['forloop'] = loop_vars
segment.each do |item|
context[@variable_name] = item
@for_block.render_to_output_buffer(context, output)
loop_vars.send(:increment!)
# Handle any interrupts if they exist.
next unless context.interrupt?
interrupt = context.pop_interrupt
break if interrupt.is_a?(BreakInterrupt)
next if interrupt.is_a?(ContinueInterrupt)
end
ensure
for_stack.pop
end
end
output
end
def set_attribute(key, expr, safe: false)
case key
when 'offset'
@from = if expr == 'continue'
:continue
else
parse_expression(expr, safe: safe)
end
when 'limit'
@limit = parse_expression(expr, safe: safe)
end
end
def render_else(context, output)
if @else_block
@else_block.render_to_output_buffer(context, output)
else
output
end
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
(super + [@node.limit, @node.from, @node.collection_name]).compact
end
end
end
end
================================================
FILE: lib/liquid/tags/if.rb
================================================
# frozen_string_literal: true
module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category conditional
# @liquid_name if
# @liquid_summary
# Renders an expression if a specific condition is `true`.
# @liquid_syntax
# {% if condition %}
# expression
# {% endif %}
# @liquid_syntax_keyword condition The condition to evaluate.
# @liquid_syntax_keyword expression The expression to render if the condition is met.
class If < Block
Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o
ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o
BOOLEAN_OPERATORS = %w(and or).freeze
attr_reader :blocks
def initialize(tag_name, markup, options)
super
@blocks = []
push_block('if', markup)
end
def nodelist
@blocks.map(&:attachment)
end
def parse(tokens)
while parse_body(@blocks.last.attachment, tokens)
end
@blocks.reverse_each do |block|
block.attachment.remove_blank_strings if blank?
block.attachment.freeze
end
end
ELSE_TAG_NAMES = ['elsif', 'else'].freeze
private_constant :ELSE_TAG_NAMES
def unknown_tag(tag, markup, tokens)
if ELSE_TAG_NAMES.include?(tag)
push_block(tag, markup)
else
super
end
end
def render_to_output_buffer(context, output)
@blocks.each do |block|
result = Liquid::Utils.to_liquid_value(
block.evaluate(context),
)
if result
return block.attachment.render_to_output_buffer(context, output)
end
end
output
end
private
def strict2_parse(markup)
strict_parse(markup)
end
def push_block(tag, markup)
block = if tag == 'else'
ElseCondition.new
else
parse_with_selected_parser(markup)
end
@blocks.push(block)
block.attach(new_body)
end
def parse_expression(markup, safe: false)
Condition.parse_expression(parse_context, markup, safe: safe)
end
def lax_parse(markup)
expressions = markup.scan(ExpressionsAndOperators)
raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop =~ Syntax
condition = Condition.new(parse_expression(Regexp.last_match(1)), Regexp.last_match(2), parse_expression(Regexp.last_match(3)))
until expressions.empty?
operator = expressions.pop.to_s.strip
raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop.to_s =~ Syntax
new_condition = Condition.new(parse_expression(Regexp.last_match(1)), Regexp.last_match(2), parse_expression(Regexp.last_match(3)))
raise SyntaxError, options[:locale].t("errors.syntax.if") unless BOOLEAN_OPERATORS.include?(operator)
new_condition.send(operator, condition)
condition = new_condition
end
condition
end
def strict_parse(markup)
p = @parse_context.new_parser(markup)
condition = parse_binary_comparisons(p)
p.consume(:end_of_string)
condition
end
def parse_binary_comparisons(p)
condition = parse_comparison(p)
first_condition = condition
while (op = p.id?('and') || p.id?('or'))
child_condition = parse_comparison(p)
condition.send(op, child_condition)
condition = child_condition
end
first_condition
end
def parse_comparison(p)
a = parse_expression(p.expression, safe: true)
if (op = p.consume?(:comparison))
b = parse_expression(p.expression, safe: true)
Condition.new(a, op, b)
else
Condition.new(a)
end
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
@node.blocks
end
end
end
end
================================================
FILE: lib/liquid/tags/ifchanged.rb
================================================
# frozen_string_literal: true
module Liquid
class Ifchanged < Block
def render_to_output_buffer(context, output)
block_output = +''
super(context, block_output)
if block_output != context.registers[:ifchanged]
context.registers[:ifchanged] = block_output
output << block_output
end
output
end
end
end
================================================
FILE: lib/liquid/tags/include.rb
================================================
# frozen_string_literal: true
module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category theme
# @liquid_name include
# @liquid_summary
# Renders a [snippet](/themes/architecture/snippets).
# @liquid_description
# Inside the snippet, you can access and alter variables that are [created](/docs/api/liquid/tags/variable-tags) outside of the
# snippet.
# @liquid_syntax
# {% include 'filename' %}
# @liquid_syntax_keyword filename The name of the snippet to render, without the `.liquid` extension.
# @liquid_deprecated
# 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).
class Include < Tag
prepend Tag::Disableable
SYNTAX = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
Syntax = SYNTAX
attr_reader :template_name_expr, :variable_name_expr, :attributes
def initialize(tag_name, markup, options)
super
parse_with_selected_parser(markup)
end
def parse(_tokens)
end
def render_to_output_buffer(context, output)
template_name = context.evaluate(@template_name_expr)
raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name.is_a?(String)
partial = PartialCache.load(
template_name,
context: context,
parse_context: parse_context,
)
context_variable_name = @alias_name || template_name.split('/').last
variable = if @variable_name_expr
context.evaluate(@variable_name_expr)
else
context.find_variable(template_name, raise_on_not_found: false)
end
old_template_name = context.template_name
old_partial = context.partial
begin
context.template_name = partial.name
context.partial = true
context.stack do
@attributes.each do |key, value|
context[key] = context.evaluate(value)
end
if variable.is_a?(Array)
variable.each do |var|
context[context_variable_name] = var
partial.render_to_output_buffer(context, output)
end
else
context[context_variable_name] = variable
partial.render_to_output_buffer(context, output)
end
end
ensure
context.template_name = old_template_name
context.partial = old_partial
end
output
end
alias_method :parse_context, :options
private :parse_context
def strict2_parse(markup)
p = @parse_context.new_parser(markup)
@template_name_expr = safe_parse_expression(p)
@variable_name_expr = safe_parse_expression(p) if p.id?("for") || p.id?("with")
@alias_name = p.consume(:id) if p.id?("as")
p.consume?(:comma)
@attributes = {}
while p.look(:id)
key = p.consume
p.consume(:colon)
@attributes[key] = safe_parse_expression(p)
p.consume?(:comma)
end
p.consume(:end_of_string)
end
def strict_parse(markup)
lax_parse(markup)
end
def lax_parse(markup)
if markup =~ SYNTAX
template_name = Regexp.last_match(1)
variable_name = Regexp.last_match(3)
@alias_name = Regexp.last_match(5)
@variable_name_expr = variable_name ? parse_expression(variable_name) : nil
@template_name_expr = parse_expression(template_name)
@attributes = {}
markup.scan(TagAttributes) do |key, value|
@attributes[key] = parse_expression(value)
end
else
raise SyntaxError, options[:locale].t("errors.syntax.include")
end
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[
@node.template_name_expr,
@node.variable_name_expr,
] + @node.attributes.values
end
end
end
end
================================================
FILE: lib/liquid/tags/increment.rb
================================================
# frozen_string_literal: true
module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category variable
# @liquid_name increment
# @liquid_summary
# Creates a new variable, with a default value of 0, that's increased by 1 with each subsequent call.
#
# > Caution:
# > Predefined Liquid objects can be overridden by variables with the same name.
# > To make sure that you can access all Liquid objects, make sure that your variable name doesn't match a predefined object's name.
# @liquid_description
# Variables that are declared with `increment` are unique to the [layout](/themes/architecture/layouts), [template](/themes/architecture/templates),
# 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.
#
# Similarly, variables that are created with `increment` are independent from those created with [`assign`](/docs/api/liquid/tags/assign)
# and [`capture`](/docs/api/liquid/tags/capture). However, `increment` and [`decrement`](/docs/api/liquid/tags/decrement) share
# variables.
# @liquid_syntax
# {% increment variable_name %}
# @liquid_syntax_keyword variable_name The name of the variable being incremented.
class Increment < Tag
attr_reader :variable_name
def initialize(tag_name, markup, options)
super
@variable_name = markup.strip
end
def render_to_output_buffer(context, output)
counter_environment = context.environments.first
value = counter_environment[@variable_name] || 0
counter_environment[@variable_name] = value + 1
output << value.to_s
output
end
end
end
================================================
FILE: lib/liquid/tags/inline_comment.rb
================================================
# frozen_string_literal: true
module Liquid
class InlineComment < Tag
def initialize(tag_name, markup, options)
super
# Semantically, a comment should only ignore everything after it on the line.
# Currently, this implementation doesn't support mixing a comment with another tag
# but we need to reserve future support for this and prevent the introduction
# of inline comments from being backward incompatible change.
#
# As such, we're forcing users to put a # symbol on every line otherwise this
# tag will throw an error.
if markup.match?(/\n\s*[^#\s]/)
raise SyntaxError, options[:locale].t("errors.syntax.inline_comment_invalid")
end
end
def render_to_output_buffer(_context, output)
output
end
def blank?
true
end
end
end
================================================
FILE: lib/liquid/tags/raw.rb
================================================
# frozen_string_literal: true
module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category syntax
# @liquid_name raw
# @liquid_summary
# Outputs any Liquid code as text instead of rendering it.
# @liquid_syntax
# {% raw %}
# expression
# {% endraw %}
# @liquid_syntax_keyword expression The expression to be output without being rendered.
class Raw < Block
Syntax = /\A\s*\z/
def initialize(tag_name, markup, parse_context)
super
ensure_valid_markup(tag_name, markup, parse_context)
end
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)
@body << Regexp.last_match(1) if Regexp.last_match(1) != ""
return
end
@body << token unless token.empty?
end
raise_tag_never_closed(block_name)
end
def render_to_output_buffer(_context, output)
output << @body
output
end
def nodelist
[@body]
end
def blank?
@body.empty?
end
protected
def ensure_valid_markup(tag_name, markup, parse_context)
unless Syntax.match?(markup)
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_unexpected_args", tag: tag_name)
end
end
end
end
================================================
FILE: lib/liquid/tags/render.rb
================================================
# frozen_string_literal: true
module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category theme
# @liquid_name render
# @liquid_summary
# Renders a [snippet](/themes/architecture/snippets) or [app block](/themes/architecture/sections/section-schema#render-app-blocks).
# @liquid_description
# Inside snippets and app blocks, you can't directly access variables that are [created](/docs/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)
# 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
# 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 [`section` object](/docs/api/liquid/objects/section).
#
# Outside a snippet or app block, you can't access variables created inside the snippet or app block.
#
# > Note:
# > When you render a snippet using the `render` tag, you can't use the [`include` tag](/docs/api/liquid/tags/include)
# > inside the snippet.
# @liquid_syntax
# {% render 'filename' %}
# @liquid_syntax_keyword filename The name of the snippet to render, without the `.liquid` extension.
class Render < Tag
FOR = 'for'
SYNTAX = /(#{QuotedString}+)(\s+(with|#{FOR})\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
disable_tags "include"
attr_reader :template_name_expr, :variable_name_expr, :attributes, :alias_name
def initialize(tag_name, markup, options)
super
parse_with_selected_parser(markup)
end
def for_loop?
@is_for_loop
end
def render_to_output_buffer(context, output)
render_tag(context, output)
end
def render_tag(context, output)
# The expression should be a String literal, which parses to a String object
template_name = @template_name_expr
raise ::ArgumentError unless template_name.is_a?(String)
partial = PartialCache.load(
template_name,
context: context,
parse_context: parse_context,
)
context_variable_name = @alias_name || template_name.split('/').last
render_partial_func = ->(var, forloop) {
inner_context = context.new_isolated_subcontext
inner_context.template_name = partial.name
inner_context.partial = true
inner_context['forloop'] = forloop if forloop
@attributes.each do |key, value|
inner_context[key] = context.evaluate(value)
end
inner_context[context_variable_name] = var unless var.nil?
partial.render_to_output_buffer(inner_context, output)
forloop&.send(:increment!)
}
variable = @variable_name_expr ? context.evaluate(@variable_name_expr) : nil
if @is_for_loop && variable.respond_to?(:each) && variable.respond_to?(:count)
forloop = Liquid::ForloopDrop.new(template_name, variable.count, nil)
variable.each { |var| render_partial_func.call(var, forloop) }
else
render_partial_func.call(variable, nil)
end
output
end
# render (string) (with|for expression)? (as id)? (key: value)*
def strict2_parse(markup)
p = @parse_context.new_parser(markup)
@template_name_expr = parse_expression(strict2_template_name(p), safe: true)
with_or_for = p.id?("for") || p.id?("with")
@variable_name_expr = safe_parse_expression(p) if with_or_for
@alias_name = p.consume(:id) if p.id?("as")
@is_for_loop = (with_or_for == FOR)
p.consume?(:comma)
@attributes = {}
while p.look(:id)
key = p.consume
p.consume(:colon)
@attributes[key] = safe_parse_expression(p)
p.consume?(:comma)
end
p.consume(:end_of_string)
end
def strict2_template_name(p)
p.consume(:string)
end
def strict_parse(markup)
lax_parse(markup)
end
def lax_parse(markup)
raise SyntaxError, options[:locale].t("errors.syntax.render") unless markup =~ SYNTAX
template_name = Regexp.last_match(1)
with_or_for = Regexp.last_match(3)
variable_name = Regexp.last_match(4)
@alias_name = Regexp.last_match(6)
@variable_name_expr = variable_name ? parse_expression(variable_name) : nil
@template_name_expr = parse_expression(template_name)
@is_for_loop = (with_or_for == FOR)
@attributes = {}
markup.scan(TagAttributes) do |key, value|
@attributes[key] = parse_expression(value)
end
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[
@node.template_name_expr,
@node.variable_name_expr,
] + @node.attributes.values
end
end
end
end
================================================
FILE: lib/liquid/tags/table_row.rb
================================================
# frozen_string_literal: true
module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category iteration
# @liquid_name tablerow
# @liquid_summary
# Generates HTML table rows for every item in an array.
# @liquid_description
# The `tablerow` tag must be wrapped in HTML `
` and `
` tags.
#
# > Tip:
# > Every `tablerow` loop has an associated [`tablerowloop` object](/docs/api/liquid/objects/tablerowloop) with information about the loop.
# @liquid_syntax
# {% tablerow variable in array %}
# expression
# {% endtablerow %}
# @liquid_syntax_keyword variable The current item in the array.
# @liquid_syntax_keyword array The array to iterate over.
# @liquid_syntax_keyword expression The expression to render.
# @liquid_optional_param cols: [number] The number of columns that the table should have.
# @liquid_optional_param limit: [number] The number of iterations to perform.
# @liquid_optional_param offset: [number] The 1-based index to start iterating at.
# @liquid_optional_param range [untyped] A custom numeric range to iterate over.
class TableRow < Block
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
ALLOWED_ATTRIBUTES = ['cols', 'limit', 'offset', 'range'].freeze
attr_reader :variable_name, :collection_name, :attributes
def initialize(tag_name, markup, options)
super
parse_with_selected_parser(markup)
end
def strict2_parse(markup)
p = @parse_context.new_parser(markup)
@variable_name = p.consume(:id)
unless p.id?("in")
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in")
end
@collection_name = safe_parse_expression(p)
p.consume?(:comma)
@attributes = {}
while p.look(:id)
key = p.consume
unless ALLOWED_ATTRIBUTES.include?(key)
raise SyntaxError, options[:locale].t("errors.syntax.table_row_invalid_attribute", attribute: key)
end
p.consume(:colon)
@attributes[key] = safe_parse_expression(p)
p.consume?(:comma)
end
p.consume(:end_of_string)
end
def strict_parse(markup)
lax_parse(markup)
end
def lax_parse(markup)
if markup =~ Syntax
@variable_name = Regexp.last_match(1)
@collection_name = parse_expression(Regexp.last_match(2))
@attributes = {}
markup.scan(TagAttributes) do |key, value|
@attributes[key] = parse_expression(value)
end
else
raise SyntaxError, options[:locale].t("errors.syntax.table_row")
end
end
def render_to_output_buffer(context, output)
(collection = context.evaluate(@collection_name)) || (return '')
from = @attributes.key?('offset') ? to_integer(context.evaluate(@attributes['offset'])) : 0
to = @attributes.key?('limit') ? from + to_integer(context.evaluate(@attributes['limit'])) : nil
collection = Utils.slice_collection(collection, from, to)
length = collection.length
cols = @attributes.key?('cols') ? to_integer(context.evaluate(@attributes['cols'])) : length
output << "
\n"
context.stack do
tablerowloop = Liquid::TablerowloopDrop.new(length, cols)
context['tablerowloop'] = tablerowloop
collection.each do |item|
context[@variable_name] = item
output << "
"
super
output << '
'
# 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 << "
\n
"
end
tablerowloop.send(:increment!)
end
end
output << "
\n"
output
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
super + @node.attributes.values + [@node.collection_name]
end
end
private
def to_integer(value)
value.to_i
rescue NoMethodError
raise Liquid::ArgumentError, "invalid integer"
end
end
end
================================================
FILE: lib/liquid/tags/unless.rb
================================================
# frozen_string_literal: true
require_relative 'if'
module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category conditional
# @liquid_name unless
# @liquid_summary
# Renders an expression unless a specific condition is `true`.
# @liquid_description
# > Tip:
# > Similar to the [`if` tag](/docs/api/liquid/tags/if), you can use `elsif` to add more conditions to an `unless` tag.
# @liquid_syntax
# {% unless condition %}
# expression
# {% endunless %}
# @liquid_syntax_keyword condition The condition to evaluate.
# @liquid_syntax_keyword expression The expression to render unless the condition is met.
class Unless < If
def render_to_output_buffer(context, output)
# First condition is interpreted backwards ( if not )
first_block = @blocks.first
result = Liquid::Utils.to_liquid_value(
first_block.evaluate(context),
)
unless result
return first_block.attachment.render_to_output_buffer(context, output)
end
# After the first condition unless works just like if
@blocks[1..-1].each do |block|
result = Liquid::Utils.to_liquid_value(
block.evaluate(context),
)
if result
return block.attachment.render_to_output_buffer(context, output)
end
end
output
end
end
end
================================================
FILE: lib/liquid/tags.rb
================================================
# 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"
require_relative "tags/doc"
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,
'doc' => Doc,
}.freeze
end
end
================================================
FILE: lib/liquid/template.rb
================================================
# frozen_string_literal: true
module Liquid
# Templates are central to liquid.
# Interpreting templates is a two step process. First you compile the
# source code you got. During compile time some extensive error checking is performed.
# your code should expect to get some SyntaxErrors.
#
# After you have a compiled template you can then render it.
# You can use a compiled template over and over again and keep it cached.
#
# Example:
#
# template = Liquid::Template.parse(source)
# template.render('user_name' => 'bob')
#
class Template
attr_accessor :root, :name
attr_reader :resource_limits, :warnings
attr_reader :profiler
class << self
# Sets how strict the parser should be.
# :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 enforces correct syntax for most tags
# :strict2 enforces correct syntax for all tags
def error_mode=(mode)
Deprecations.warn("Template.error_mode=", "Environment#error_mode=")
Environment.default.error_mode = mode
end
def error_mode
Environment.default.error_mode
end
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
def register_tag(name, klass)
Deprecations.warn("Template.register_tag", "Environment#register_tag")
Environment.default.register_tag(name, 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)
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
# creates a new Template object from liquid source code
# To enable profiling, pass in profile: true as an option.
# See Liquid::Profiler for more information
def parse(source, options = {})
environment = options[:environment] || Environment.default
new(environment: environment).parse(source, options)
end
end
def initialize(environment: Environment.default)
@environment = environment
@rethrow_errors = false
@resource_limits = ResourceLimits.new(environment.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
end
def registers
@registers ||= {}
end
def assigns
@assigns ||= {}
end
def instance_assigns
@instance_assigns ||= {}
end
def errors
@errors ||= []
end
# Render takes a hash with local variables.
#
# if you use the same filters over and over again consider registering them globally
# with Template.register_filter
#
# if profiling was enabled in Template#parse then the resulting profiling information
# will be available via Template#profiler
#
# Following options can be passed:
#
# * filters : array with local filters
# * registers : hash with register variables. Those can be accessed from
# filters and tags and might be useful to integrate liquid more with its host application
#
def render(*args)
return '' if @root.nil?
context = case args.first
when Liquid::Context
c = args.shift
if @rethrow_errors
c.exception_renderer = Liquid::RAISE_EXCEPTION_LAMBDA
end
c
when Liquid::Drop
drop = args.shift
drop.context = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits, {}, @environment)
when Hash
Context.new([args.shift, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits, {}, @environment)
when nil
Context.new(assigns, instance_assigns, registers, @rethrow_errors, @resource_limits, {}, @environment)
else
raise ArgumentError, "Expected Hash or Liquid::Context as parameter"
end
output = nil
case args.last
when Hash
options = args.pop
output = options[:output] if options[:output]
static_registers = context.registers.static
options[:registers]&.each do |key, register|
static_registers[key] = register
end
apply_options_to_context(context, options)
when Module, Array
context.add_filters(args.pop)
end
# Retrying a render resets resource usage
context.resource_limits.reset
if @profiling && context.profiler.nil?
@profiler = context.profiler = Liquid::Profiler.new
end
context.template_name ||= name
begin
# render the nodelist.
@root.render_to_output_buffer(context, output || +'')
rescue Liquid::MemoryError => e
context.handle_error(e)
ensure
@errors = context.errors
end
end
def render!(*args)
@rethrow_errors = true
render(*args)
end
def render_to_output_buffer(context, output)
render(context, output: output)
end
private
def configure_options(options)
if (profiling = options[:profile])
raise "Profiler not loaded, require 'liquid/profiler' first" unless defined?(Liquid::Profiler)
end
@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
end
def apply_options_to_context(context, options)
context.add_filters(options[:filters]) if options[:filters]
context.global_filter = options[:global_filter] if options[:global_filter]
context.exception_renderer = options[:exception_renderer] if options[:exception_renderer]
context.strict_variables = options[:strict_variables] if options[:strict_variables]
context.strict_filters = options[:strict_filters] if options[:strict_filters]
end
end
end
================================================
FILE: lib/liquid/template_factory.rb
================================================
# frozen_string_literal: true
module Liquid
class TemplateFactory
def for(_template_name)
Liquid::Template.new
end
end
end
================================================
FILE: lib/liquid/tokenizer.rb
================================================
# frozen_string_literal: true
require "strscan"
module Liquid
class Tokenizer
attr_reader :line_number, :for_liquid_tag
TAG_END = /%\}/
TAG_OR_VARIABLE_START = /\{[\{\%]/
NEWLINE = /\n/
OPEN_CURLEY = "{".ord
CLOSE_CURLEY = "}".ord
PERCENTAGE = "%".ord
def initialize(
source:,
string_scanner:,
line_numbers: false,
line_number: nil,
for_liquid_tag: false
)
@line_number = line_number || (line_numbers ? 1 : nil)
@for_liquid_tag = for_liquid_tag
@source = source.to_s.to_str
@offset = 0
@tokens = []
if @source
@ss = string_scanner
@ss.string = @source
tokenize
end
end
def shift
token = @tokens[@offset]
return unless token
@offset += 1
if @line_number
@line_number += @for_liquid_tag ? 1 : token.count("\n")
end
token
end
private
def tokenize
if @for_liquid_tag
@tokens = @source.split("\n")
else
@tokens << shift_normal until @ss.eos?
end
@source = nil
@ss = nil
end
def shift_normal
token = next_token
return unless token
token
end
def next_token
# possible states: :text, :tag, :variable
byte_a = @ss.peek_byte
if byte_a == OPEN_CURLEY
@ss.scan_byte
byte_b = @ss.peek_byte
if byte_b == PERCENTAGE
@ss.scan_byte
return next_tag_token
elsif byte_b == OPEN_CURLEY
@ss.scan_byte
return next_variable_token
end
@ss.pos -= 1
end
next_text_token
end
def next_text_token
start = @ss.pos
unless @ss.skip_until(TAG_OR_VARIABLE_START)
token = @ss.rest
@ss.terminate
return token
end
pos = @ss.pos -= 2
@source.byteslice(start, pos - start)
rescue ::ArgumentError => e
if e.message == "invalid byte sequence in #{@ss.string.encoding}"
raise SyntaxError, "Invalid byte sequence in #{@ss.string.encoding}"
else
raise
end
end
def next_variable_token
start = @ss.pos - 2
byte_a = byte_b = @ss.scan_byte
while byte_b
byte_a = @ss.scan_byte while byte_a && byte_a != CLOSE_CURLEY && byte_a != OPEN_CURLEY
break unless byte_a
if @ss.eos?
return byte_a == CLOSE_CURLEY ? @source.byteslice(start, @ss.pos - start) : "{{"
end
byte_b = @ss.scan_byte
if byte_a == CLOSE_CURLEY
if byte_b == CLOSE_CURLEY
return @source.byteslice(start, @ss.pos - start)
elsif byte_b != CLOSE_CURLEY
@ss.pos -= 1
return @source.byteslice(start, @ss.pos - start)
end
elsif byte_a == OPEN_CURLEY && byte_b == PERCENTAGE
return next_tag_token_with_start(start)
end
byte_a = byte_b
end
"{{"
end
def next_tag_token
start = @ss.pos - 2
if (len = @ss.skip_until(TAG_END))
@source.byteslice(start, len + 2)
else
"{%"
end
end
def next_tag_token_with_start(start)
@ss.skip_until(TAG_END)
@source.byteslice(start, @ss.pos - start)
end
end
end
================================================
FILE: lib/liquid/usage.rb
================================================
# frozen_string_literal: true
module Liquid
module Usage
def self.increment(name)
end
end
end
================================================
FILE: lib/liquid/utils.rb
================================================
# frozen_string_literal: true
module Liquid
module Utils
DECIMAL_REGEX = /\A-?\d+\.\d+\z/
UNIX_TIMESTAMP_REGEX = /\A\d+\z/
def self.slice_collection(collection, from, to)
if (from != 0 || !to.nil?) && collection.respond_to?(:load_slice)
collection.load_slice(from, to)
else
slice_collection_using_each(collection, from, to)
end
end
def self.slice_collection_using_each(collection, from, to)
segments = []
index = 0
# Maintains Ruby 1.8.7 String#each behaviour on 1.9
if collection.is_a?(String)
return collection.empty? ? [] : [collection]
end
return [] unless collection.respond_to?(:each)
collection.each do |item|
if to && to <= index
break
end
if from <= index
segments << item
end
index += 1
end
segments
end
def self.to_integer(num)
return num if num.is_a?(Integer)
num = num.to_s
begin
Integer(num)
rescue ::ArgumentError
raise Liquid::ArgumentError, "invalid integer"
end
end
def self.to_number(obj)
case obj
when Float
BigDecimal(obj.to_s)
when Numeric
obj
when String
DECIMAL_REGEX.match?(obj.strip) ? BigDecimal(obj) : obj.to_i
else
if obj.respond_to?(:to_number)
obj.to_number
else
0
end
end
end
def self.to_date(obj)
return obj if obj.respond_to?(:strftime)
if obj.is_a?(String)
return if obj.empty?
obj = obj.downcase
end
case obj
when 'now', 'today'
Time.now
when UNIX_TIMESTAMP_REGEX, Integer
Time.at(obj.to_i)
when String
Time.parse(obj)
end
rescue ::ArgumentError
nil
end
def self.to_liquid_value(obj)
# Enable "obj" to represent itself as a primitive value like integer, string, or boolean
return obj.to_liquid_value if obj.respond_to?(:to_liquid_value)
# Otherwise return the object itself
obj
end
def self.to_s(obj, seen = {})
case obj
when BigDecimal
obj.to_s("F")
when Hash
# If the custom hash implementation overrides `#to_s`, use their
# custom implementation. Otherwise we use Liquid's default
# implementation.
if obj.class.instance_method(:to_s) == HASH_TO_S_METHOD
hash_inspect(obj, seen)
else
obj.to_s
end
when Array
array_inspect(obj, seen)
else
obj.to_s
end
end
def self.inspect(obj, seen = {})
case obj
when Hash
# If the custom hash implementation overrides `#inspect`, use their
# custom implementation. Otherwise we use Liquid's default
# implementation.
if obj.class.instance_method(:inspect) == HASH_INSPECT_METHOD
hash_inspect(obj, seen)
else
obj.inspect
end
when Array
array_inspect(obj, seen)
else
obj.inspect
end
end
def self.array_inspect(arr, seen = {})
if seen[arr.object_id]
return "[...]"
end
seen[arr.object_id] = true
str = +"["
cursor = 0
len = arr.length
while cursor < len
if cursor > 0
str << ", "
end
item_str = inspect(arr[cursor], seen)
str << item_str
cursor += 1
end
str << "]"
str
ensure
seen.delete(arr.object_id)
end
def self.hash_inspect(hash, seen = {})
if seen[hash.object_id]
return "{...}"
end
seen[hash.object_id] = true
str = +"{"
first = true
hash.each do |key, value|
if first
first = false
else
str << ", "
end
key_str = inspect(key, seen)
str << key_str
str << "=>"
value_str = inspect(value, seen)
str << value_str
end
str << "}"
str
ensure
seen.delete(hash.object_id)
end
HASH_TO_S_METHOD = Hash.instance_method(:to_s)
private_constant :HASH_TO_S_METHOD
HASH_INSPECT_METHOD = Hash.instance_method(:inspect)
private_constant :HASH_INSPECT_METHOD
end
end
================================================
FILE: lib/liquid/variable.rb
================================================
# frozen_string_literal: true
module Liquid
# Holds variables. Variables are only loaded "just in time"
# and are not evaluated as part of the render stage
#
# {{ monkey }}
# {{ user.name }}
#
# Variables can be combined with filters:
#
# {{ user | link }}
#
class Variable
FilterMarkupRegex = /#{FilterSeparator}\s*(.*)/om
FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o
FilterArgsRegex = /(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o
JustTagAttributes = /\A#{TagAttributes}\z/o
MarkupWithQuotedFragment = /(#{QuotedFragment})(.*)/om
attr_accessor :filters, :name, :line_number
attr_reader :parse_context
alias_method :options, :parse_context
include ParserSwitching
def initialize(markup, parse_context)
@markup = markup
@name = nil
@parse_context = parse_context
@line_number = parse_context.line_number
strict_parse_with_error_mode_fallback(markup)
end
def raw
@markup
end
def markup_context(markup)
"in \"{{#{markup}}}\""
end
def lax_parse(markup)
@filters = []
return unless markup =~ MarkupWithQuotedFragment
name_markup = Regexp.last_match(1)
filter_markup = Regexp.last_match(2)
@name = parse_context.parse_expression(name_markup)
if filter_markup =~ FilterMarkupRegex
filters = Regexp.last_match(1).scan(FilterParser)
filters.each do |f|
next unless f =~ /\w+/
filtername = Regexp.last_match(0)
filterargs = f.scan(FilterArgsRegex).flatten
@filters << lax_parse_filter_expressions(filtername, filterargs)
end
end
end
def strict_parse(markup)
@filters = []
p = @parse_context.new_parser(markup)
return if p.look(:end_of_string)
@name = parse_context.safe_parse_expression(p)
while p.consume?(:pipe)
filtername = p.consume(:id)
filterargs = p.consume?(:colon) ? parse_filterargs(p) : Const::EMPTY_ARRAY
@filters << lax_parse_filter_expressions(filtername, filterargs)
end
p.consume(:end_of_string)
end
def strict2_parse(markup)
@filters = []
p = @parse_context.new_parser(markup)
return if p.look(:end_of_string)
@name = parse_context.safe_parse_expression(p)
@filters << strict2_parse_filter_expressions(p) while p.consume?(:pipe)
p.consume(:end_of_string)
end
def parse_filterargs(p)
# first argument
filterargs = [p.argument]
# followed by comma separated others
filterargs << p.argument while p.consume?(:comma)
filterargs
end
def render(context)
obj = context.evaluate(@name)
@filters.each do |filter_name, filter_args, filter_kwargs|
filter_args = evaluate_filter_expressions(context, filter_args, filter_kwargs)
obj = context.invoke(filter_name, obj, *filter_args)
end
context.apply_global_filter(obj)
end
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
else
output << Liquid::Utils.to_s(obj)
end
end
def disabled?(_context)
false
end
def disabled_tags
[]
end
private
def lax_parse_filter_expressions(filter_name, unparsed_args)
filter_args = []
keyword_args = nil
unparsed_args.each do |a|
if (matches = a.match(JustTagAttributes))
keyword_args ||= {}
keyword_args[matches[1]] = parse_context.parse_expression(matches[2])
else
filter_args << parse_context.parse_expression(a)
end
end
result = [filter_name, filter_args]
result << keyword_args if keyword_args
result
end
# Surprisingly, positional and keyword arguments can be mixed.
#
# filter = filtername [":" filterargs?]
# filterargs = argument ("," argument)*
# argument = (positional_argument | keyword_argument)
# positional_argument = expression
# keyword_argument = id ":" expression
def strict2_parse_filter_expressions(p)
filtername = p.consume(:id)
filter_args = []
keyword_args = {}
if p.consume?(:colon)
# Parse first argument (no leading comma)
argument(p, filter_args, keyword_args) unless end_of_arguments?(p)
# Parse remaining arguments (with leading commas) and optional trailing comma
argument(p, filter_args, keyword_args) while p.consume?(:comma) && !end_of_arguments?(p)
end
result = [filtername, filter_args]
result << keyword_args unless keyword_args.empty?
result
end
def argument(p, positional_arguments, keyword_arguments)
if p.look(:id) && p.look(:colon, 1)
key = p.consume(:id)
p.consume(:colon)
value = parse_context.safe_parse_expression(p)
keyword_arguments[key] = value
else
positional_arguments << parse_context.safe_parse_expression(p)
end
end
def end_of_arguments?(p)
p.look(:pipe) || p.look(:end_of_string)
end
def evaluate_filter_expressions(context, filter_args, filter_kwargs)
parsed_args = filter_args.map { |expr| context.evaluate(expr) }
if filter_kwargs
parsed_kwargs = {}
filter_kwargs.each do |key, expr|
parsed_kwargs[key] = context.evaluate(expr)
end
parsed_args << parsed_kwargs
end
parsed_args
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[@node.name] + @node.filters.flatten
end
end
end
end
================================================
FILE: lib/liquid/variable_lookup.rb
================================================
# frozen_string_literal: true
module Liquid
class VariableLookup
COMMAND_METHODS = ['size', 'first', 'last'].freeze
attr_reader :name, :lookups
def self.parse(markup, string_scanner = StringScanner.new(""), cache = nil)
new(markup, string_scanner, cache)
end
def initialize(markup, string_scanner = StringScanner.new(""), cache = nil)
lookups = markup.scan(VariableParser)
name = lookups.shift
if name&.start_with?('[') && name&.end_with?(']')
name = Expression.parse(
name[1..-2],
string_scanner,
cache,
)
end
@name = name
@lookups = lookups
@command_flags = 0
@lookups.each_index do |i|
lookup = lookups[i]
if lookup&.start_with?('[') && lookup&.end_with?(']')
lookups[i] = Expression.parse(
lookup[1..-2],
string_scanner,
cache,
)
elsif COMMAND_METHODS.include?(lookup)
@command_flags |= 1 << i
end
end
end
def lookup_command?(lookup_index)
@command_flags & (1 << lookup_index) != 0
end
def evaluate(context)
name = context.evaluate(@name)
object = context.find_variable(name)
@lookups.each_index do |i|
key = context.evaluate(@lookups[i])
# Cast "key" to its liquid value to enable it to act as a primitive value
key = Liquid::Utils.to_liquid_value(key)
# If object is a hash- or array-like object we look for the
# presence of the key and if its available we return it
if object.respond_to?(:[]) &&
((object.respond_to?(:key?) && object.key?(key)) ||
(object.respond_to?(:fetch) && key.is_a?(Integer)))
# if its a proc we will replace the entry with the proc
res = context.lookup_and_evaluate(object, key)
object = res.to_liquid
# Some special cases. If the part wasn't in square brackets and
# no key with the same name was found we interpret following calls
# as commands and call them on the current object
elsif lookup_command?(i) && object.respond_to?(key)
object = object.send(key).to_liquid
# Handle string first/last like ActiveSupport does (returns first/last character)
# ActiveSupport returns "" for empty strings, not nil
elsif lookup_command?(i) && object.is_a?(String) && (key == "first" || key == "last")
object = key == "first" ? (object[0] || "") : (object[-1] || "")
# No key was present with the desired value and it wasn't one of the directly supported
# keywords either. The only thing we got left is to return nil or
# raise an exception if `strict_variables` option is set to true
else
return nil unless context.strict_variables
raise Liquid::UndefinedVariable, "undefined variable #{key}"
end
# If we are dealing with a drop here we have to
object.context = context if object.respond_to?(:context=)
end
object
end
def ==(other)
self.class == other.class && state == other.state
end
protected
def state
[@name, @lookups, @command_flags]
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
@node.lookups
end
end
end
end
================================================
FILE: lib/liquid/version.rb
================================================
# encoding: utf-8
# frozen_string_literal: true
module Liquid
VERSION = "5.12.0"
end
================================================
FILE: lib/liquid.rb
================================================
# frozen_string_literal: true
# Copyright (c) 2005 Tobias Luetke
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
require "strscan"
module Liquid
FilterSeparator = /\|/
ArgumentSeparator = ','
FilterArgumentSeparator = ':'
VariableAttributeSeparator = '.'
WhitespaceControl = '-'
TagStart = /\{\%/
TagEnd = /\%\}/
TagName = /#|\w+/
VariableSignature = /\(?[\w\-\.\[\]]\)?/
VariableSegment = /[\w\-]/
VariableStart = /\{\{/
VariableEnd = /\}\}/
VariableIncompleteEnd = /\}\}?/
QuotedString = /"[^"]*"|'[^']*'/
QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o
TagAttributes = /(\w[\w-]*)\s*\:\s*(#{QuotedFragment})/o
AnyStartingTag = /#{TagStart}|#{VariableStart}/o
PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om
VariableParser = /\[(?>[^\[\]]+|\g<0>)*\]|#{VariableSegment}+\??/o
RAISE_EXCEPTION_LAMBDA = ->(_e) { raise }
HAS_STRING_SCANNER_SCAN_BYTE = StringScanner.instance_methods.include?(:scan_byte)
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'
require 'liquid/drop'
require 'liquid/tablerowloop_drop'
require 'liquid/forloop_drop'
require 'liquid/extensions'
require 'liquid/errors'
require 'liquid/interrupts'
require 'liquid/strainer_template'
require 'liquid/context'
require 'liquid/tag'
require 'liquid/block_body'
require 'liquid/document'
require 'liquid/variable'
require 'liquid/variable_lookup'
require 'liquid/range_lookup'
require 'liquid/resource_limits'
require 'liquid/expression'
require 'liquid/template'
require 'liquid/condition'
require 'liquid/utils'
require 'liquid/tokenizer'
require 'liquid/parse_context'
require 'liquid/partial_cache'
require 'liquid/usage'
require 'liquid/registers'
require 'liquid/template_factory'
================================================
FILE: liquid.gemspec
================================================
# encoding: utf-8
# frozen_string_literal: true
lib = File.expand_path('../lib/', __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require "liquid/version"
Gem::Specification.new do |s|
s.name = "liquid"
s.version = Liquid::VERSION
s.platform = Gem::Platform::RUBY
s.summary = "A secure, non-evaling end user template engine with aesthetic markup."
s.authors = ["Tobias Lütke"]
s.email = ["tobi@leetsoft.com"]
s.homepage = "https://shopify.github.io/liquid/"
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_rubygems_version = ">= 1.3.7"
s.metadata['allowed_push_host'] = 'https://rubygems.org'
s.files = Dir.glob("{lib}/**/*") + %w(LICENSE README.md)
s.extra_rdoc_files = ["History.md", "README.md"]
s.require_path = "lib"
s.add_dependency("strscan", ">= 3.1.1")
s.add_dependency("bigdecimal")
s.add_development_dependency('rake', '~> 13.0')
s.add_development_dependency('minitest')
end
================================================
FILE: performance/benchmark.rb
================================================
# frozen_string_literal: true
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
profiler = ThemeRunner.new
Benchmark.ips do |x|
x.time = 20
x.warmup = 10
puts
puts "Running benchmark for #{x.time} seconds (with #{x.warmup} seconds warmup)."
puts
phase = ENV["PHASE"] || "all"
x.report("tokenize:") { profiler.tokenize } if phase == "all" || phase == "tokenize"
x.report("parse:") { profiler.compile } if phase == "all" || phase == "parse"
x.report("render:") { profiler.render } if phase == "all" || phase == "render"
x.report("parse & render:") { profiler.run } if phase == "all" || phase == "run"
end
================================================
FILE: performance/memory_profile.rb
================================================
# frozen_string_literal: true
require 'benchmark/ips'
require 'memory_profiler'
require 'terminal-table'
require_relative 'theme_runner'
class Profiler
LOG_LABEL = "Profiling: ".rjust(14).freeze
REPORTS_DIR = File.expand_path('.memprof', __dir__).freeze
def self.run
puts
yield new
end
def initialize
@allocated = []
@retained = []
@headings = []
end
def profile(phase, &block)
print(LOG_LABEL)
print("#{phase}.. ".ljust(10))
report = MemoryProfiler.report(&block)
puts 'Done.'
@headings << phase.capitalize
@allocated << "#{report.scale_bytes(report.total_allocated_memsize)} (#{report.total_allocated} objects)"
@retained << "#{report.scale_bytes(report.total_retained_memsize)} (#{report.total_retained} objects)"
return if ENV['CI']
require 'fileutils'
report_file = File.join(REPORTS_DIR, "#{sanitize(phase)}.txt")
FileUtils.mkdir_p(REPORTS_DIR)
report.pretty_print(to_file: report_file, scale_bytes: true)
end
def tabulate
table = Terminal::Table.new(headings: @headings.unshift('Phase')) do |t|
t << @allocated.unshift('Total allocated')
t << @retained.unshift('Total retained')
end
puts
puts table
puts "\nDetailed report(s) saved to #{REPORTS_DIR}/" unless ENV['CI']
end
def sanitize(string)
string.downcase.gsub(/[\W]/, '-').squeeze('-')
end
end
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
runner = ThemeRunner.new
Profiler.run do |x|
x.profile('parse') { runner.compile }
x.profile('render') { runner.render }
x.tabulate
end
================================================
FILE: performance/profile.rb
================================================
# frozen_string_literal: true
require 'stackprof'
require_relative 'theme_runner'
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new
profiler.run
[:cpu, :object].each do |profile_type|
puts "Profiling in #{profile_type} mode..."
results = StackProf.run(mode: profile_type) do
200.times do
profiler.run
end
end
if profile_type == :cpu && (graph_filename = ENV['GRAPH_FILENAME'])
File.open(graph_filename, 'w') do |f|
StackProf::Report.new(results).print_graphviz(nil, f)
end
end
StackProf::Report.new(results).print_text(false, 20)
File.write(ENV['FILENAME'] + "." + profile_type.to_s, Marshal.dump(results)) if ENV['FILENAME']
end
================================================
FILE: performance/shopify/comment_form.rb
================================================
# frozen_string_literal: true
class CommentForm < Liquid::Block
Syntax = /(#{Liquid::VariableSignature}+)/
def initialize(tag_name, markup, options)
super
if markup =~ Syntax
@variable_name = Regexp.last_match(1)
@attributes = {}
else
raise SyntaxError, "Syntax Error in 'comment_form' - Valid syntax: comment_form [article]"
end
end
def render_to_output_buffer(context, output)
article = context[@variable_name]
context.stack do
context['form'] = {
'posted_successfully?' => context.registers[:posted_successfully],
'errors' => context['comment.errors'],
'author' => context['comment.author'],
'email' => context['comment.email'],
'body' => context['comment.body'],
}
output << wrap_in_form(article, render_all(@nodelist, context, output))
output
end
end
def wrap_in_form(article, input)
%()
end
end
================================================
FILE: performance/shopify/database.rb
================================================
# frozen_string_literal: true
require 'yaml'
module Database
DATABASE_FILE_PATH = "#{__dir__}/vision.database.yml"
# Load the standard vision toolkit database and re-arrage it to be simply exportable
# to liquid as assigns. All this is based on Shopify
def self.tables
@tables ||= begin
db =
if YAML.respond_to?(:unsafe_load_file) # Only Psych 4+ can use unsafe_load_file
# unsafe_load_file is needed for YAML references
YAML.unsafe_load_file(DATABASE_FILE_PATH)
else
YAML.load_file(DATABASE_FILE_PATH)
end
# From vision source
db['products'].each do |product|
collections = db['collections'].find_all do |collection|
collection['products'].any? { |p| p['id'].to_i == product['id'].to_i }
end
product['collections'] = collections
end
# key the tables by handles, as this is how liquid expects it.
db = db.each_with_object({}) do |(key, values), assigns|
assigns[key] = values.each_with_object({}) do |v, h|
h[v['handle']] = v
end
end
# Some standard direct accessors so that the specialized templates
# render correctly
db['collection'] = db['collections'].values.first
db['product'] = db['products'].values.first
db['blog'] = db['blogs'].values.first
db['article'] = db['blog']['articles'].first
db['cart'] = {
'total_price' => db['line_items'].values.inject(0) { |sum, item| sum + item['line_price'] * item['quantity'] },
'item_count' => db['line_items'].values.inject(0) { |sum, item| sum + item['quantity'] },
'items' => db['line_items'].values,
}
db
end
end
end
================================================
FILE: performance/shopify/json_filter.rb
================================================
# frozen_string_literal: true
require 'json'
module JsonFilter
def json(object)
JSON.dump(object.reject { |k, _v| k == "collections" })
end
end
================================================
FILE: performance/shopify/liquid.rb
================================================
# frozen_string_literal: true
$LOAD_PATH.unshift(__dir__ + '/../../lib')
require_relative '../../lib/liquid'
require_relative 'comment_form'
require_relative 'paginate'
require_relative 'json_filter'
require_relative 'money_filter'
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)
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)
================================================
FILE: performance/shopify/money_filter.rb
================================================
# frozen_string_literal: true
module MoneyFilter
def money_with_currency(money)
return '' if money.nil?
format("$ %.2f USD", money / 100.0)
end
def money(money)
return '' if money.nil?
format("$ %.2f", money / 100.0)
end
private
def currency
ShopDrop.new.currency
end
end
================================================
FILE: performance/shopify/paginate.rb
================================================
# frozen_string_literal: true
class Paginate < Liquid::Block
Syntax = /(#{Liquid::QuotedFragment})\s*(by\s*(\d+))?/
def initialize(tag_name, markup, options)
super
if markup =~ Syntax
@collection_name = Regexp.last_match(1)
@page_size = if Regexp.last_match(2)
Regexp.last_match(3).to_i
else
20
end
@attributes = { 'window_size' => 3 }
markup.scan(Liquid::TagAttributes) do |key, value|
@attributes[key] = value
end
else
raise SyntaxError, "Syntax Error in tag 'paginate' - Valid syntax: paginate [collection] by number"
end
end
def render_to_output_buffer(context, output)
@context = context
context.stack do
current_page = context['current_page'].to_i
pagination = {
'page_size' => @page_size,
'current_page' => 5,
'current_offset' => @page_size * 5,
}
context['paginate'] = pagination
collection_size = context[@collection_name].size
raise ArgumentError, "Cannot paginate array '#{@collection_name}'. Not found." if collection_size.nil?
page_count = (collection_size.to_f / @page_size.to_f).to_f.ceil + 1
pagination['items'] = collection_size
pagination['pages'] = page_count - 1
pagination['previous'] = link('« Previous', current_page - 1) if 1 < current_page
pagination['next'] = link('Next »', current_page + 1) if page_count > current_page + 1
pagination['parts'] = []
hellip_break = false
if page_count > 2
1.upto(page_count - 1) do |page|
if current_page == page
pagination['parts'] << no_link(page)
elsif page == 1
pagination['parts'] << link(page, page)
elsif page == page_count - 1
pagination['parts'] << link(page, page)
elsif page <= current_page - @attributes['window_size'] || page >= current_page + @attributes['window_size']
next if hellip_break
pagination['parts'] << no_link('…')
hellip_break = true
next
else
pagination['parts'] << link(page, page)
end
hellip_break = false
end
end
super
end
end
private
def no_link(title)
{ 'title' => title, 'is_link' => false }
end
def link(title, page)
{ 'title' => title, 'url' => current_url + "?page=#{page}", 'is_link' => true }
end
def current_url
"/collections/frontpage"
end
end
================================================
FILE: performance/shopify/shop_filter.rb
================================================
# frozen_string_literal: true
module ShopFilter
def asset_url(input)
"/files/1/[shop_id]/[shop_id]/assets/#{input}"
end
def global_asset_url(input)
"/global/#{input}"
end
def shopify_asset_url(input)
"/shopify/#{input}"
end
def script_tag(url)
%()
end
def stylesheet_tag(url, media = "all")
%()
end
def link_to(link, url, title = "")
%(#{link})
end
def img_tag(url, alt = "")
%()
end
def link_to_vendor(vendor)
if vendor
link_to(vendor, url_for_vendor(vendor), vendor)
else
'Unknown Vendor'
end
end
def link_to_type(type)
if type
link_to(type, url_for_type(type), type)
else
'Unknown Vendor'
end
end
def url_for_vendor(vendor_title)
"/collections/#{to_handle(vendor_title)}"
end
def url_for_type(type_title)
"/collections/#{to_handle(type_title)}"
end
def product_img_url(url, style = 'small')
unless url =~ %r{\Aproducts/([\w\-\_]+)\.(\w{2,4})}
raise ArgumentError, 'filter "size" can only be called on product images'
end
case style
when 'original'
'/files/shops/random_number/' + url
when 'grande', 'large', 'medium', 'compact', 'small', 'thumb', 'icon'
"/files/shops/random_number/products/#{Regexp.last_match(1)}_#{style}.#{Regexp.last_match(2)}"
else
raise ArgumentError, 'valid parameters for filter "size" are: original, grande, large, medium, compact, small, thumb and icon '
end
end
def default_pagination(paginate)
html = []
html << %(#{link_to(paginate['previous']['title'], paginate['previous']['url'])}) if paginate['previous']
paginate['parts'].each do |part|
html << if part['is_link']
%(#{link_to(part['title'], part['url'])})
elsif part['title'].to_i == paginate['current_page'].to_i
%(#{part['title']})
else
%(#{part['title']})
end
end
html << %(#{link_to(paginate['next']['title'], paginate['next']['url'])}) if paginate['next']
html.join(' ')
end
# Accepts a number, and two words - one for singular, one for plural
# Returns the singular word if input equals 1, otherwise plural
def pluralize(input, singular, plural)
input == 1 ? singular : plural
end
private
def to_handle(str)
result = str.dup
result.downcase!
result.delete!("'\"()[]")
result.gsub!(/\W+/, '-')
result.gsub!(/-+\z/, '') if result[-1] == '-'
result.gsub!(/\A-+/, '') if result[0] == '-'
result
end
end
================================================
FILE: performance/shopify/tag_filter.rb
================================================
# frozen_string_literal: true
module TagFilter
def link_to_tag(label, tag)
"#{label}"
end
def highlight_active_tag(tag, css_class = 'active')
if @context['current_tags'].include?(tag)
"#{tag}"
else
tag
end
end
def link_to_add_tag(label, tag)
tags = (@context['current_tags'] + [tag]).uniq
"#{label}"
end
def link_to_remove_tag(label, tag)
tags = (@context['current_tags'] - [tag]).uniq
"#{label}"
end
end
================================================
FILE: performance/shopify/vision.database.yml
================================================
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# Variants
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
product_variants:
- &product-1-var-1
id: 1
title: 151cm / Normal
price: 19900
weight: 1000
compare_at_price: 49900
available: true
inventory_quantity: 5
option1: 151cm
option2: Normal
option3:
- &product-1-var-2
id: 2
title: 155cm / Normal
price: 31900
weight: 1000
compare_at_price: 50900
available: true
inventory_quantity: 2
option1: 155cm
option2: Normal
option3:
- &product-2-var-1
id: 3
title: 162cm
price: 29900
weight: 1000
compare_at_price: 52900
available: true
inventory_quantity: 3
option1: 162cm
option2:
option3:
- &product-3-var-1
id: 4
title: 159cm
price: 19900
weight: 1000
compare_at_price:
available: true
inventory_quantity: 4
option1: 159cm
option2:
option3:
- &product-4-var-1
id: 5
title: 159cm
price: 19900
weight: 1000
compare_at_price: 32900
available: true
inventory_quantity: 6
option1: 159cm
option2:
option3:
- &product-1-var-3
id: 6
title: 158cm / Wide
price: 23900
weight: 1000
compare_at_price: 99900
available: false
inventory_quantity: 0
option1: 158cm
option2: Wide
option3:
- &product-3-var-2
id: 7
title: 162cm
price: 19900
weight: 1000
compare_at_price:
available: false
inventory_quantity: 0
option1: 162cm
option2:
option3:
- &product-3-var-3
id: 8
title: 165cm
price: 22900
weight: 1000
compare_at_price:
available: true
inventory_quantity: 4
option1: 165cm
option2:
option3:
- &product-5-var-1
id: 9
title: black / 42
price: 11900
weight: 500
compare_at_price: 22900
available: true
inventory_quantity: 1
option1: black
option2: 42
option3:
- &product-5-var-2
id: 10
title: beige / 42
price: 11900
weight: 500
compare_at_price: 22900
available: true
inventory_quantity: 3
option1: beige
option2: 42
option3:
- &product-5-var-3
id: 11
title: white / 42
price: 13900
weight: 500
compare_at_price: 24900
available: true
inventory_quantity: 1
option1: white
option2: 42
option3:
- &product-5-var-4
id: 12
title: black / 44
price: 11900
weight: 500
compare_at_price: 22900
available: true
inventory_quantity: 2
option1: black
option2: 44
option3:
- &product-5-var-5
id: 13
title: beige / 44
price: 11900
weight: 500
compare_at_price: 22900
available: false
inventory_quantity: 0
option1: beige
option2: 44
option3:
- &product-5-var-6
id: 14
title: white / 44
price: 13900
weight: 500
compare_at_price: 24900
available: false
inventory_quantity: 0
option1: white
option2: 44
option3:
- &product-6-var-1
id: 15
title: red
price: 2179500
weight: 200000
compare_at_price:
available: true
inventory_quantity: 0
option1: red
option2:
option3:
- &product-7-var-1
id: 16
title: black / small
price: 1900
weight: 200
compare_at_price:
available: true
inventory_quantity: 20
option1: black
option2: small
option3:
- &product-7-var-2
id: 17
title: black / medium
price: 1900
weight: 200
compare_at_price:
available: false
inventory_quantity: 0
option1: black
option2: medium
option3:
- &product-7-var-3
id: 18
title: black / large
price: 1900
weight: 200
compare_at_price:
available: true
inventory_quantity: 10
option1: black
option2: large
option3:
- &product-7-var-4
id: 19
title: black / extra large
price: 1900
weight: 200
compare_at_price:
available: false
inventory_quantity: 0
option1: black
option2: extra large
option3:
- &product-8-var-1
id: 20
title: brown / small
price: 5900
weight: 400
compare_at_price: 6900
available: true
inventory_quantity: 5
option1: brown
option2: small
option3:
- &product-8-var-2
id: 21
title: brown / medium
price: 5900
weight: 400
compare_at_price: 6900
available: false
inventory_quantity: 0
option1: brown
option2: medium
option3:
- &product-8-var-3
id: 22
title: brown / large
price: 5900
weight: 400
compare_at_price: 6900
available: true
inventory_quantity: 10
option1: brown
option2: large
option3:
- &product-8-var-4
id: 23
title: black / small
price: 5900
weight: 400
compare_at_price: 6900
available: true
inventory_quantity: 10
option1: black
option2: small
option3:
- &product-8-var-5
id: 24
title: black / medium
price: 5900
weight: 400
compare_at_price: 6900
available: true
inventory_quantity: 10
option1: black
option2: medium
option3:
- &product-8-var-6
id: 25
title: black / large
price: 5900
weight: 400
compare_at_price: 6900
available: false
inventory_quantity: 0
option1: black
option2: large
option3:
- &product-9-var-1
id: 26
title: Body Only
price: 499995
weight: 2000
compare_at_price:
available: true
inventory_quantity: 3
option1: Body Only
option2:
option3:
- &product-9-var-2
id: 27
title: Kit with 18-55mm VR lens
price: 523995
weight: 2000
compare_at_price:
available: true
inventory_quantity: 2
option1: Kit with 18-55mm VR lens
option2:
option3:
- &product-9-var-3
id: 28
title: Kit with 18-200 VR lens
price: 552500
weight: 2000
compare_at_price:
available: true
inventory_quantity: 3
option1: Kit with 18-200 VR lens
option2:
option3:
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# Products
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
products:
- &product-1
id: 1
title: Arbor Draft
handle: arbor-draft
type: Snowboards
vendor: Arbor
price: 23900
price_max: 31900
price_min: 23900
price_varies: true
available: true
tags:
- season2005
- pro
- intermediate
- wooden
- freestyle
options:
- Length
- Style
compare_at_price: 49900
compare_at_price_max: 50900
compare_at_price_min: 49900
compare_at_price_varies: true
url: /products/arbor-draft
featured_image: products/arbor_draft.jpg
images:
- products/arbor_draft.jpg
description:
The Arbor Draft snowboard wouldn't exist if Polynesians hadn't figured out how to surf hundreds of years ago. But the Draft does exist, and it's here to bring your urban and park riding to a new level. The board's freaky Tiki design pays homage to culture that inspired snowboarding. It's designed to spin with ease, land smoothly, lock hook-free onto rails, and take the abuse of a pavement pounding or twelve. The Draft will pop off kickers with authority and carve solidly across the pipe. The Draft features targeted Koa wood die cuts inlayed into the deck that enhance the flex pattern. Now bow down to riding's ancestors.
variants:
- *product-1-var-1
- *product-1-var-2
- *product-1-var-3
- &product-2
id: 2
title: Arbor Element
handle: arbor-element
type: Snowboards
vendor: Arbor
price: 29900
price_max: 29900
price_min: 29900
price_varies: false
available: true
tags:
- season2005
- pro
- wooden
- freestyle
options:
- Length
compare_at_price: 52900
compare_at_price_max: 52900
compare_at_price_min: 52900
compare_at_price_varies: false
url: /products/arbor-element
featured_image: products/element58.jpg
images:
- products/element58.jpg
description:
The Element is a technically advanced all-mountain board for riders who readily transition from one terrain, snow condition, or riding style to another. Its balanced design provides the versatility needed for the true ride-it-all experience. The Element is exceedingly lively, freely initiates, and holds a tight edge at speed. Its structural real-wood topsheet is made with book-matched Koa.
variants:
- *product-2-var-1
- &product-3
id: 3
title: Comic ~ Pastel
handle: comic-pastel
type: Snowboards
vendor: Technine
price: 19900
price_max: 22900
price_min: 19900
tags:
- season2006
- beginner
- intermediate
- freestyle
- purple
options:
- Length
price_varies: true
available: true
compare_at_price:
compare_at_price_max: 0
compare_at_price_min: 0
compare_at_price_varies: false
url: /products/comic-pastel
featured_image: products/technine1.jpg
images:
- products/technine1.jpg
- products/technine2.jpg
- products/technine_detail.jpg
description:
2005 Technine Comic Series Description The Comic series was developed to be the ultimate progressive freestyle board in the Technine line. Dependable edge control and a perfect flex pattern for jumping in the park or out of bounds. Landins and progression will come easy with this board and it will help your riding progress to the next level. Street rails, park jibs, backcountry booters and park jumps, this board will do it all.
variants:
- *product-3-var-1
- *product-3-var-2
- *product-3-var-3
- &product-4
id: 4
title: Comic ~ Orange
handle: comic-orange
type: Snowboards
vendor: Technine
price: 19900
price_max: 19900
price_min: 19900
price_varies: false
available: true
tags:
- season2006
- beginner
- intermediate
- freestyle
- orange
options:
- Length
compare_at_price: 32900
compare_at_price_max: 32900
compare_at_price_min: 32900
compare_at_price_varies: false
url: /products/comic-orange
featured_image: products/technine3.jpg
images:
- products/technine3.jpg
- products/technine4.jpg
description:
2005 Technine Comic Series Description The Comic series was developed to be the ultimate progressive freestyle board in the Technine line. Dependable edge control and a perfect flex pattern for jumping in the park or out of bounds. Landins and progression will come easy with this board and it will help your riding progress to the next level. Street rails, park jibs, backcountry booters and park jumps, this board will do it all.
variants:
- *product-4-var-1
- &product-5
id: 5
title: Burton Boots
handle: burton-boots
type: Boots
vendor: Burton
price: 11900
price_max: 11900
price_min: 11900
price_varies: false
available: true
tags:
- season2006
- beginner
- intermediate
- boots
options:
- Color
- Shoe Size
compare_at_price: 22900
compare_at_price_max: 22900
compare_at_price_min: 22900
compare_at_price_varies: false
url: /products/burton-boots
featured_image: products/burton.jpg
images:
- products/burton.jpg
description:
The Burton boots are particularly well on snowboards. The very best thing about them is that the according picture is cubic. This makes testing in a Vision testing environment very easy.
variants:
- *product-5-var-1
- *product-5-var-2
- *product-5-var-3
- *product-5-var-4
- *product-5-var-5
- *product-5-var-6
- &product-6
id: 6
title: Superbike 1198 S
handle: superbike
type: Superbike
vendor: Ducati
price: 2179500
price_max: 2179500
price_min: 2179500
price_varies: false
available: true
tags:
- ducati
- superbike
- bike
- street
- racing
- performance
options:
- Color
compare_at_price:
compare_at_price_max: 0
compare_at_price_min: 0
compare_at_price_varies: false
url: /products/superbike
featured_image: products/ducati.jpg
images:
- products/ducati.jpg
description:
‘S’ PERFORMANCE
Producing 170hp (125kW) and with a dry weight of just 169kg (372.6lb), the new 1198 S now incorporates more World Superbike technology than ever before by taking the 1198 motor and adding top-of-the-range suspension, lightweight chassis components and a true racing-style traction control system designed for road use.
The high performance, fully adjustable 43mm Öhlins forks, which sport low friction titanium nitride-treated fork sliders, respond effortlessly to every imperfection in the tarmac. Beyond their advanced engineering solutions, one of the most important characteristics of Öhlins forks is their ability to communicate the condition and quality of the tyre-to-road contact patch, a feature that puts every rider in superior control. The suspension set-up at the rear is complemented with a fully adjustable Öhlins rear shock equipped with a ride enhancing top-out spring and mounted to a single-sided swingarm for outstanding drive and traction. The front-to-rear Öhlins package is completed with a control-enhancing adjustable steering damper.
High Quality Shopify Shirt. Wear your e-commerce solution with pride and attract attention anywhere you go.
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Extra comfortable zip up sweater. Durable quality, ideal for any outdoor activities.
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Flagship pro D-SLR with a 12.1-MP FX-format CMOS sensor, blazing 9 fps shooting at full FX resolution and low-noise performance up to 6400 ISO.
Nikon's original 12.1-megapixel FX-format (23.9 x 36mm) CMOS sensor: Couple Nikon's exclusive digital image processing system with the 12.1-megapixel FX-format and you'll get breathtakingly rich images while also reducing noise to unprecedented levels with even higher ISOs.
Continuous shooting at up to 9 frames per second: At full FX resolution and up to 11fps in the DX crop mode, the D3 offers uncompromised shooting speeds for fast-action and sports photography.
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
created_at: 2005-04-04 12:00
blogs:
- id: 1
handle: news
title: News
url: /blogs/news
articles:
- id: 3
title: 'Welcome to the new Foo Shop'
author: Daniel
content:
Welcome to your Shopify store! The jaded Pixel crew is really glad you decided to take Shopify for a spin.
To help you get you started with Shopify, here are a couple of tips regarding what you see on this page.
The text you see here is an article. To edit this article, create new articles or create new pages you can go to the Blogs & Pages tab of the administration menu.
The Shopify t-shirt above is a product and selling products is what Shopify is all about. To edit this product, or create new products you can go to the Products Tab in of the administration menu.
While you're looking around be sure to check out the Collections and Navigations tabs and soon you will be well on your way to populating your site.
And of course don't forget to browse the theme gallery to pick a new look for your shop!
Shopify is in beta If you would like to make comments or suggestions please visit us in the Shopify Forums or drop us an email.
created_at: 2005-04-04 16:00
- id: 4
title: 'Breaking News: Restock on all sales products'
author: Tobi
content: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
created_at: 2005-04-04 12:00
articles_count: 2
- id: 2
handle: bigcheese-blog
title: Bigcheese blog
url: /blogs/bigcheese-blog
articles:
- id: 1
title: 'One thing you probably did not know yet...'
author: Justin
content: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
created_at: 2005-04-04 16:00
comments:
-
id: 1
author: John Smith
email: john@smith.com
content: Wow...great article man.
status: published
created_at: 2009-01-01 12:00
updated_at: 2009-02-01 12:00
url: ""
-
id: 2
author: John Jones
email: john@jones.com
content: I really enjoyed this article. And I love your shop! It's awesome. Shopify rocks!
status: published
created_at: 2009-03-01 12:00
updated_at: 2009-02-01 12:00
url: "http://somesite.com/"
- id: 2
title: Fascinating
author: Tobi
content: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
created_at: 2005-04-06 12:00
comments:
articles_count: 2
comments_enabled?: true
comment_post_url: ""
comments_count: 2
moderated?: true
- id: 3
handle: paginated-blog
title: Paginated blog
url: /blogs/paginated-blog
articles:
- id: 6
title: 'One thing you probably did not know yet...'
author: Justin
content: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
created_at: 2005-04-04 16:00
- id: 7
title: Fascinating
author: Tobi
content: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
created_at: 2005-04-06 12:00
articles_count: 200
================================================
FILE: performance/shopify/weight_filter.rb
================================================
# frozen_string_literal: true
module WeightFilter
def weight(grams)
format("%.2f", grams / 1000)
end
def weight_with_unit(grams)
"#{weight(grams)} kg"
end
end
================================================
FILE: performance/tests/dropify/article.liquid
================================================
In Admin > Blogs & Pages, create a page with the handle frontpage and it will show up here.
{{ "Learn more about handles" | link_to: "http://wiki.shopify.com/Handle" }}
{{ article.content }}
{% else %}
In Admin > Blogs & Pages, create a page with the handle frontpage and it will show up here.
{{ "Learn more about handles" | link_to: "http://wiki.shopify.com/Handle" }}
{% endif %}
Seems like you are looking for something that just isn't here. Try heading back to our main page. Or you can checkout some of our featured products below.
Featured Products
{% for product in collections.frontpage.products %}
{% else %}
{% endif %}
================================================
FILE: performance/tests/vogue/collection.liquid
================================================
{% paginate collection.products by 12 %}{% if collection.products.size == 0 %}
No products found in this collection.{% else %}
{{ collection.title }}
{{ collection.description }}
{% tablerow product in collection.products cols: 3 %}
{{ article.content }}
{% else %}
In Admin > Blogs & Pages, create a page with the handle frontpage and it will show up here.
{{ "Learn more about handles" | link_to: "http://wiki.shopify.com/Handle" }}
{% endif %}
{% tablerow product in collections.frontpage.products cols: 3 limit: 12 %}
{% if collection.tags.size == 0 %}
No tags found.{% else %}
{% for tag in collection.tags %}{% if current_tags contains tag %} {{ tag | highlight_active_tag | link_to_remove_tag: tag }}{% else %} {{ tag | highlight_active_tag | link_to_add_tag: tag }}{% endif %}{% unless forloop.last %}, {% endunless %}{% endfor %}{% endif %}
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 %}',
error_mode: :warn,
render_errors: true,
)
assert_template_result(
"Liquid error (line 1): invalid integer",
'{% tablerow n in (1...10) offset:true %} {{n}} {% endtablerow %}',
error_mode: :warn,
render_errors: true,
)
assert_template_result(
"Liquid error (line 1): invalid integer",
'{% tablerow n in (1...10) cols:true %} {{n}} {% endtablerow %}',
render_errors: true,
error_mode: :warn,
)
end
def test_table_row_handles_interrupts
assert_template_result(
"
\n
1
\n",
'{% tablerow n in (1..3) cols:2 %} {{n}} {% break %} {{n}} {% endtablerow %}',
)
assert_template_result(
"
\n
1
2
\n
3
\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
loop j=1
loop j=2
loop i=1
loop j=1
loop j=2
loop i=2
after loop
STR
assert_template_result(
expected,
template,
)
end
def test_tablerow_with_cols_attribute_in_strict2_mode
template = <<~LIQUID.chomp
{% tablerow i in (1..6) cols: 3 %}{{ i }}{% endtablerow %}
LIQUID
expected = <<~OUTPUT
1
2
3
4
5
6
OUTPUT
with_error_modes(:strict2) do
assert_template_result(expected, template)
end
end
def test_tablerow_with_limit_attribute_in_strict2_mode
template = <<~LIQUID.chomp
{% tablerow i in (1..10) limit: 3 %}{{ i }}{% endtablerow %}
LIQUID
expected = <<~OUTPUT
1
2
3
OUTPUT
with_error_modes(:strict2) do
assert_template_result(expected, template)
end
end
def test_tablerow_with_offset_attribute_in_strict2_mode
template = <<~LIQUID.chomp
{% tablerow i in (1..5) offset: 2 %}{{ i }}{% endtablerow %}
LIQUID
expected = <<~OUTPUT
3
4
5
OUTPUT
with_error_modes(:strict2) do
assert_template_result(expected, template)
end
end
def test_tablerow_with_range_attribute_in_strict2_mode
template = <<~LIQUID.chomp
{% tablerow i in (1..3) range: (1..10) %}{{ i }}{% endtablerow %}
LIQUID
expected = <<~OUTPUT
1
2
3
OUTPUT
with_error_modes(:strict2) do
assert_template_result(expected, template)
end
end
def test_tablerow_with_multiple_attributes_in_strict2_mode
template = <<~LIQUID.chomp
{% tablerow i in (1..10) cols: 2, limit: 4, offset: 1 %}{{ i }}{% endtablerow %}
LIQUID
expected = <<~OUTPUT
2
3
4
5
OUTPUT
with_error_modes(:strict2) do
assert_template_result(expected, template)
end
end
def test_tablerow_with_variable_collection_in_strict2_mode
template = <<~LIQUID.chomp
{% tablerow n in numbers cols: 2 %}{{ n }}{% endtablerow %}
LIQUID
expected = <<~OUTPUT
1
2
3
4
OUTPUT
with_error_modes(:strict2) do
assert_template_result(expected, template, { 'numbers' => [1, 2, 3, 4] })
end
end
def test_tablerow_with_dotted_access_in_strict2_mode
template = <<~LIQUID.chomp
{% tablerow n in obj.numbers cols: 2 %}{{ n }}{% endtablerow %}
LIQUID
expected = <<~OUTPUT
1
2
3
4
OUTPUT
with_error_modes(:strict2) do
assert_template_result(expected, template, { 'obj' => { 'numbers' => [1, 2, 3, 4] } })
end
end
def test_tablerow_with_bracketed_access_in_strict2_mode
template = <<~LIQUID.chomp
{% tablerow n in obj["numbers"] cols: 2 %}{{ n }}{% endtablerow %}
LIQUID
expected = <<~OUTPUT
10
20
OUTPUT
with_error_modes(:strict2) do
assert_template_result(expected, template, { 'obj' => { 'numbers' => [10, 20] } })
end
end
def test_tablerow_without_attributes_in_strict2_mode
template = <<~LIQUID.chomp
{% tablerow i in (1..3) %}{{ i }}{% endtablerow %}
LIQUID
expected = <<~OUTPUT
1
2
3
OUTPUT
with_error_modes(:strict2) do
assert_template_result(expected, template)
end
end
def test_tablerow_without_in_keyword_in_strict2_mode
template = '{% tablerow i (1..10) %}{{ i }}{% endtablerow %}'
with_error_modes(:strict2) do
error = assert_raises(SyntaxError) { Template.parse(template) }
assert_equal("Liquid syntax error: For loops require an 'in' clause in \"i (1..10)\"", error.message)
end
end
def test_tablerow_with_multiple_invalid_attributes_reports_first_in_strict2_mode
template = '{% tablerow i in (1..10) invalid1: 5, invalid2: 10 %}{{ i }}{% endtablerow %}'
with_error_modes(:strict2) do
error = assert_raises(SyntaxError) { Template.parse(template) }
assert_equal("Liquid syntax error: Invalid attribute 'invalid1' in tablerow loop. Valid attributes are cols, limit, offset, and range in \"i in (1..10) invalid1: 5, invalid2: 10\"", error.message)
end
end
def test_tablerow_with_empty_collection_in_strict2_mode
template = <<~LIQUID.chomp
{% tablerow i in empty_array cols: 2 %}{{ i }}{% endtablerow %}
LIQUID
expected = <<~OUTPUT
OUTPUT
with_error_modes(:strict2) do
assert_template_result(expected, template, { 'empty_array' => [] })
end
end
def test_tablerow_with_invalid_attribute_strict_vs_strict2
template = '{% tablerow i in (1..5) invalid_attr: 10 %}{{ i }}{% endtablerow %}'
expected = <<~OUTPUT
1
2
3
4
5
OUTPUT
with_error_modes(:lax, :strict) do
assert_template_result(expected, template)
end
with_error_modes(:strict2) do
error = assert_raises(SyntaxError) { Template.parse(template) }
assert_match(/Invalid attribute 'invalid_attr'/, error.message)
end
end
def test_tablerow_with_invalid_expression_strict_vs_strict2
template = '{% tablerow i in (1..5) limit: foo=>bar %}{{ i }}{% endtablerow %}'
with_error_modes(:lax, :strict) do
expected = <<~OUTPUT
OUTPUT
assert_template_result(expected, template)
end
with_error_modes(:strict2) do
error = assert_raises(SyntaxError) { Template.parse(template) }
assert_match(/Unexpected character =/, error.message)
end
end
end
================================================
FILE: test/integration/tags/unless_else_tag_test.rb
================================================
# frozen_string_literal: true
require 'test_helper'
class UnlessElseTagTest < Minitest::Test
include Liquid
def test_unless
assert_template_result(' ', ' {% unless true %} this text should not go into the output {% endunless %} ')
assert_template_result(
' this text should go into the output ',
' {% 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 %}?')
end
def test_unless_else
assert_template_result(' YES ', '{% unless true %} NO {% else %} YES {% endunless %}')
assert_template_result(' YES ', '{% unless false %} YES {% else %} NO {% endunless %}')
assert_template_result(' YES ', '{% unless "foo" %} NO {% else %} YES {% endunless %}')
end
def test_unless_in_loop
assert_template_result('23', '{% for i in choices %}{% unless i %}{{ forloop.index }}{% endunless %}{% endfor %}', { 'choices' => [1, nil, false] })
end
def test_unless_else_in_loop
assert_template_result(' TRUE 2 3 ', '{% for i in choices %}{% unless i %} {{ forloop.index }} {% else %} TRUE {% endunless %}{% endfor %}', { 'choices' => [1, nil, false] })
end
end # UnlessElseTest
================================================
FILE: test/integration/template_test.rb
================================================
# frozen_string_literal: true
require 'test_helper'
require 'timeout'
class TemplateContextDrop < Liquid::Drop
def liquid_method_missing(method)
method
end
def foo
'fizzbuzz'
end
def baz
@context.registers['lulz']
end
end
class SomethingWithLength < Liquid::Drop
def length
nil
end
end
class ErroneousDrop < Liquid::Drop
def bad_method
raise 'ruby error in drop'
end
end
class DropWithUndefinedMethod < Liquid::Drop
def foo
'foo'
end
end
class TemplateTest < Minitest::Test
include Liquid
def test_instance_assigns_persist_on_same_template_object_between_parses
t = Template.new
assert_equal('from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render!)
assert_equal('from instance assigns', t.parse("{{ foo }}").render!)
end
def test_warnings_is_not_exponential_time
str = "false"
100.times do
str = "{% if true %}true{% else %}#{str}{% endif %}"
end
t = Template.parse(str)
assert_equal([], Timeout.timeout(1) { t.warnings })
end
def test_instance_assigns_persist_on_same_template_parsing_between_renders
t = Template.new.parse("{{ foo }}{% assign foo = 'foo' %}{{ foo }}")
assert_equal('foo', t.render!)
assert_equal('foofoo', t.render!)
end
def test_custom_assigns_do_not_persist_on_same_template
t = Template.new
assert_equal('from custom assigns', t.parse("{{ foo }}").render!('foo' => 'from custom assigns'))
assert_equal('', t.parse("{{ foo }}").render!)
end
def test_custom_assigns_squash_instance_assigns
t = Template.new
assert_equal('from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render!)
assert_equal('from custom assigns', t.parse("{{ foo }}").render!('foo' => 'from custom assigns'))
end
def test_persistent_assigns_squash_instance_assigns
t = Template.new
assert_equal('from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render!)
t.assigns['foo'] = 'from persistent assigns'
assert_equal('from persistent assigns', t.parse("{{ foo }}").render!)
end
def test_lambda_is_called_once_from_persistent_assigns_over_multiple_parses_and_renders
t = Template.new
t.assigns['number'] = -> {
@global ||= 0
@global += 1
}
assert_equal('1', t.parse("{{number}}").render!)
assert_equal('1', t.parse("{{number}}").render!)
assert_equal('1', t.render!)
@global = nil
end
def test_lambda_is_called_once_from_custom_assigns_over_multiple_parses_and_renders
t = Template.new
assigns = {
'number' => -> {
@global ||= 0
@global += 1
},
}
assert_equal('1', t.parse("{{number}}").render!(assigns))
assert_equal('1', t.parse("{{number}}").render!(assigns))
assert_equal('1', t.render!(assigns))
@global = nil
end
def test_resource_limits_works_with_custom_length_method
t = Template.parse("{% assign foo = bar %}")
t.resource_limits.render_length_limit = 42
assert_equal("", t.render!("bar" => SomethingWithLength.new))
end
def test_resource_limits_render_length
t = Template.parse("0123456789")
t.resource_limits.render_length_limit = 9
assert_equal("Liquid error: Memory limits exceeded", t.render)
assert(t.resource_limits.reached?)
t.resource_limits.render_length_limit = 10
assert_equal("0123456789", t.render!)
end
def test_resource_limits_render_score
t = Template.parse("{% for a in (1..10) %} {% for a in (1..10) %} foo {% endfor %} {% endfor %}")
t.resource_limits.render_score_limit = 50
assert_equal("Liquid error: Memory limits exceeded", t.render)
assert(t.resource_limits.reached?)
t = Template.parse("{% for a in (1..100) %} foo {% endfor %}")
t.resource_limits.render_score_limit = 50
assert_equal("Liquid error: Memory limits exceeded", t.render)
assert(t.resource_limits.reached?)
t.resource_limits.render_score_limit = 200
assert_equal(" foo " * 100, t.render!)
refute_nil(t.resource_limits.render_score)
end
def test_resource_limits_aborts_rendering_after_first_error
t = Template.parse("{% for a in (1..100) %} foo1 {% endfor %} bar {% for a in (1..100) %} foo2 {% endfor %}")
t.resource_limits.render_score_limit = 50
assert_equal("Liquid error: Memory limits exceeded", t.render)
assert(t.resource_limits.reached?)
end
def test_resource_limits_hash_in_template_gets_updated_even_if_no_limits_are_set
t = Template.parse("{% for a in (1..100) %}x{% assign foo = 1 %} {% endfor %}")
t.render!
assert(t.resource_limits.assign_score > 0)
assert(t.resource_limits.render_score > 0)
end
def test_render_length_persists_between_blocks
t = Template.parse("{% if true %}aaaa{% endif %}")
t.resource_limits.render_length_limit = 3
assert_equal("Liquid error: Memory limits exceeded", t.render)
t.resource_limits.render_length_limit = 4
assert_equal("aaaa", t.render)
t = Template.parse("{% if true %}aaaa{% endif %}{% if true %}bbb{% endif %}")
t.resource_limits.render_length_limit = 6
assert_equal("Liquid error: Memory limits exceeded", t.render)
t.resource_limits.render_length_limit = 7
assert_equal("aaaabbb", t.render)
t = Template.parse("{% if true %}a{% endif %}{% if true %}b{% endif %}{% if true %}a{% endif %}{% if true %}b{% endif %}{% if true %}a{% endif %}{% if true %}b{% endif %}")
t.resource_limits.render_length_limit = 5
assert_equal("Liquid error: Memory limits exceeded", t.render)
t.resource_limits.render_length_limit = 6
assert_equal("ababab", t.render)
end
def test_render_length_uses_number_of_bytes_not_characters
t = Template.parse("{% if true %}すごい{% endif %}")
t.resource_limits.render_length_limit = 8
assert_equal("Liquid error: Memory limits exceeded", t.render)
t.resource_limits.render_length_limit = 9
assert_equal("すごい", t.render)
end
def test_cumulative_render_score_limit_across_render_tags
file_system = StubFileSystem.new(
'loop' => '{% for a in (1..10) %} foo {% endfor %}',
)
environment = Liquid::Environment.build(file_system: file_system)
# Without cumulative limit, all 5 partials render successfully
t = Template.parse(
'{% render "loop" %}{% render "loop" %}{% render "loop" %}{% render "loop" %}{% render "loop" %}',
environment: environment,
)
unlimited_output = t.render!
total_cumulative = t.resource_limits.cumulative_render_score
# With cumulative limit set below the total, rendering stops early
t2 = Template.parse(
'{% render "loop" %}{% render "loop" %}{% render "loop" %}{% render "loop" %}{% render "loop" %}',
environment: environment,
)
t2.resource_limits.cumulative_render_score_limit = total_cumulative / 2
limited_output = t2.render
assert(t2.resource_limits.reached?)
assert_operator(limited_output.length, :<, unlimited_output.length)
end
def test_cumulative_render_score_limit_raises_on_render_bang
file_system = StubFileSystem.new(
'loop' => '{% for a in (1..10) %} foo {% endfor %}',
)
environment = Liquid::Environment.build(file_system: file_system)
t = Template.parse(
'{% render "loop" %}{% render "loop" %}{% render "loop" %}{% render "loop" %}{% render "loop" %}',
environment: environment,
)
t.resource_limits.cumulative_render_score_limit = 20
assert_raises(Liquid::MemoryError) do
t.render!
end
end
def test_cumulative_assign_score_limit_across_include_tags
file_system = StubFileSystem.new(
'assign_partial' => '{% assign x = "a long string value here" %}',
)
environment = Liquid::Environment.build(file_system: file_system)
# Without cumulative limit, all 5 partials render
t = Template.parse(
'{% include "assign_partial" %}{% include "assign_partial" %}{% include "assign_partial" %}{% include "assign_partial" %}{% include "assign_partial" %}',
environment: environment,
)
t.render!
total_cumulative = t.resource_limits.cumulative_assign_score
# With cumulative limit set below the total, rendering stops early
t2 = Template.parse(
'{% include "assign_partial" %}{% include "assign_partial" %}{% include "assign_partial" %}{% include "assign_partial" %}{% include "assign_partial" %}',
environment: environment,
)
t2.resource_limits.cumulative_assign_score_limit = total_cumulative / 2
t2.render
assert(t2.resource_limits.reached?)
end
def test_cumulative_render_score_tracks_across_partials_without_limit
file_system = StubFileSystem.new(
'loop' => '{% for a in (1..10) %} foo {% endfor %}',
)
environment = Liquid::Environment.build(file_system: file_system)
t = Template.parse(
'{% render "loop" %}{% render "loop" %}{% render "loop" %}',
environment: environment,
)
t.render!
assert(
t.resource_limits.cumulative_render_score > t.resource_limits.render_score,
"cumulative should exceed per-template score after multiple partials",
)
end
def test_default_resource_limits_unaffected_by_render_with_context
context = Context.new
t = Template.parse("{% for a in (1..100) %}x{% assign foo = 1 %} {% endfor %}")
t.render!(context)
assert(context.resource_limits.assign_score > 0)
assert(context.resource_limits.render_score > 0)
end
def test_can_use_drop_as_context
t = Template.new
t.registers['lulz'] = 'haha'
drop = TemplateContextDrop.new
assert_equal('fizzbuzz', t.parse('{{foo}}').render!(drop))
assert_equal('bar', t.parse('{{bar}}').render!(drop))
assert_equal('haha', t.parse("{{baz}}").render!(drop))
end
def test_render_bang_force_rethrow_errors_on_passed_context
context = Context.new('drop' => ErroneousDrop.new)
t = Template.new.parse('{{ drop.bad_method }}')
e = assert_raises(RuntimeError) do
t.render!(context)
end
assert_equal('ruby error in drop', e.message)
end
def test_exception_renderer_that_returns_string
exception = nil
handler = ->(e) {
exception = e
''
}
output = Template.parse("{{ 1 | divided_by: 0 }}").render({}, exception_renderer: handler)
assert(exception.is_a?(Liquid::ZeroDivisionError))
assert_equal('', output)
end
def test_exception_renderer_that_raises
exception = nil
assert_raises(Liquid::ZeroDivisionError) do
Template.parse("{{ 1 | divided_by: 0 }}").render({}, exception_renderer: ->(e) {
exception = e
raise
})
end
assert(exception.is_a?(Liquid::ZeroDivisionError))
end
def test_global_filter_option_on_render
global_filter_proc = ->(output) { "#{output} filtered" }
rendered_template = Template.parse("{{name}}").render({ "name" => "bob" }, global_filter: global_filter_proc)
assert_equal('bob filtered', rendered_template)
end
def test_global_filter_option_when_native_filters_exist
global_filter_proc = ->(output) { "#{output} filtered" }
rendered_template = Template.parse("{{name | upcase}}").render({ "name" => "bob" }, global_filter: global_filter_proc)
assert_equal('BOB filtered', rendered_template)
end
def test_undefined_variables
t = Template.parse("{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}")
result = t.render({ 'x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } } }, strict_variables: true)
assert_equal('33 32 ', result)
assert_equal(3, t.errors.count)
assert_instance_of(Liquid::UndefinedVariable, t.errors[0])
assert_equal('Liquid error: undefined variable y', t.errors[0].message)
assert_instance_of(Liquid::UndefinedVariable, t.errors[1])
assert_equal('Liquid error: undefined variable b', t.errors[1].message)
assert_instance_of(Liquid::UndefinedVariable, t.errors[2])
assert_equal('Liquid error: undefined variable d', t.errors[2].message)
end
def test_nil_value_does_not_raise
t = Template.parse("some{{x}}thing", error_mode: :strict)
result = t.render!({ 'x' => nil }, strict_variables: true)
assert_equal(0, t.errors.count)
assert_equal('something', result)
end
def test_undefined_variables_raise
t = Template.parse("{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}")
assert_raises(UndefinedVariable) do
t.render!({ 'x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } } }, strict_variables: true)
end
end
def test_undefined_drop_methods
d = DropWithUndefinedMethod.new
t = Template.new.parse('{{ foo }} {{ woot }}')
result = t.render(d, strict_variables: true)
assert_equal('foo ', result)
assert_equal(1, t.errors.count)
assert_instance_of(Liquid::UndefinedDropMethod, t.errors[0])
end
def test_undefined_drop_methods_raise
d = DropWithUndefinedMethod.new
t = Template.new.parse('{{ foo }} {{ woot }}')
assert_raises(UndefinedDropMethod) do
t.render!(d, strict_variables: true)
end
end
def test_undefined_filters
t = Template.parse("{{a}} {{x | upcase | somefilter1 | somefilter2 | somefilter3}}")
filters = Module.new do
def somefilter3(v)
"-#{v}-"
end
end
result = t.render({ 'a' => 123, 'x' => 'foo' }, filters: [filters], strict_filters: true)
assert_equal('123 ', result)
assert_equal(1, t.errors.count)
assert_instance_of(Liquid::UndefinedFilter, t.errors[0])
assert_equal('Liquid error: undefined filter somefilter1', t.errors[0].message)
end
def test_undefined_filters_raise
t = Template.parse("{{x | somefilter1 | upcase | somefilter2}}")
assert_raises(UndefinedFilter) do
t.render!({ 'x' => 'foo' }, strict_filters: true)
end
end
def test_using_range_literal_works_as_expected
source = "{% assign foo = (x..y) %}{{ foo }}"
assert_template_result("1..5", source, { "x" => 1, "y" => 5 })
source = "{% assign nums = (x..y) %}{% for num in nums %}{{ num }}{% endfor %}"
assert_template_result("12345", source, { "x" => 1, "y" => 5 })
end
def test_source_string_subclass
string_subclass = Class.new(String) do
# E.g. ActiveSupport::SafeBuffer does this, so don't just rely on to_s to return a String
def to_s
self
end
end
source = string_subclass.new("{% assign x = 2 -%} x= {{- x }}")
assert_instance_of(string_subclass, source)
output = Template.parse(source).render!
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
================================================
FILE: test/integration/trim_mode_test.rb
================================================
# frozen_string_literal: true
require 'test_helper'
class TrimModeTest < Minitest::Test
include Liquid
# Make sure the trim isn't applied to standard output
def test_standard_output
text = <<-END_TEMPLATE
{{ 'John' }}
END_TEMPLATE
expected = <<-END_EXPECTED
John
END_EXPECTED
assert_template_result(expected, text)
end
def test_variable_output_with_multiple_blank_lines
text = <<-END_TEMPLATE
{{- 'John' -}}
END_TEMPLATE
expected = <<-END_EXPECTED
John
END_EXPECTED
assert_template_result(expected, text)
end
def test_tag_output_with_multiple_blank_lines
text = <<-END_TEMPLATE
{%- if true -%}
yes
{%- endif -%}
END_TEMPLATE
expected = <<-END_EXPECTED
yes
END_EXPECTED
assert_template_result(expected, text)
end
# Make sure the trim isn't applied to standard tags
def test_standard_tags
whitespace = ' '
text = <<-END_TEMPLATE
{% if true %}
yes
{% endif %}
END_TEMPLATE
expected = <<~END_EXPECTED
#{whitespace}
yes
#{whitespace}
END_EXPECTED
assert_template_result(expected, text)
text = <<-END_TEMPLATE
{% if false %}
no
{% endif %}
END_TEMPLATE
expected = <<~END_EXPECTED
#{whitespace}
END_EXPECTED
assert_template_result(expected, text)
end
# Make sure the trim isn't too agressive
def test_no_trim_output
text = '
{{- \'John\' -}}
'
expected = '
John
'
assert_template_result(expected, text)
end
# Make sure the trim isn't too agressive
def test_no_trim_tags
text = '
{%- if true -%}yes{%- endif -%}
'
expected = '
yes
'
assert_template_result(expected, text)
text = '
{%- if false -%}no{%- endif -%}
'
expected = ''
assert_template_result(expected, text)
end
def test_single_line_outer_tag
text = '
{%- if true %} yes {% endif -%}
'
expected = '
yes
'
assert_template_result(expected, text)
text = '
{%- if false %} no {% endif -%}
'
expected = ''
assert_template_result(expected, text)
end
def test_single_line_inner_tag
text = '
{% if true -%} yes {%- endif %}
'
expected = '
yes
'
assert_template_result(expected, text)
text = '
{% if false -%} no {%- endif %}
'
expected = '
'
assert_template_result(expected, text)
end
def test_single_line_post_tag
text = '
{% if true -%} yes {% endif -%}
'
expected = '
yes
'
assert_template_result(expected, text)
text = '
{% if false -%} no {% endif -%}
'
expected = '
'
assert_template_result(expected, text)
end
def test_single_line_pre_tag
text = '
{%- if true %} yes {%- endif %}
'
expected = '
yes
'
assert_template_result(expected, text)
text = '
{%- if false %} no {%- endif %}
'
expected = '
'
assert_template_result(expected, text)
end
def test_pre_trim_output
text = <<-END_TEMPLATE
{{- 'John' }}
END_TEMPLATE
expected = <<-END_EXPECTED
John
END_EXPECTED
assert_template_result(expected, text)
end
def test_pre_trim_tags
text = <<-END_TEMPLATE
{%- if true %}
yes
{%- endif %}
END_TEMPLATE
expected = <<-END_EXPECTED
yes
END_EXPECTED
assert_template_result(expected, text)
text = <<-END_TEMPLATE
{%- if false %}
no
{%- endif %}
END_TEMPLATE
expected = <<-END_EXPECTED
END_EXPECTED
assert_template_result(expected, text)
end
def test_post_trim_output
text = <<-END_TEMPLATE
{{ 'John' -}}
END_TEMPLATE
expected = <<-END_EXPECTED
John
END_EXPECTED
assert_template_result(expected, text)
end
def test_post_trim_tags
text = <<-END_TEMPLATE
{% if true -%}
yes
{% endif -%}
END_TEMPLATE
expected = <<-END_EXPECTED
yes
END_EXPECTED
assert_template_result(expected, text)
text = <<-END_TEMPLATE
{% if false -%}
no
{% endif -%}
END_TEMPLATE
expected = <<-END_EXPECTED
END_EXPECTED
assert_template_result(expected, text)
end
def test_pre_and_post_trim_tags
text = <<-END_TEMPLATE
{%- if true %}
yes
{% endif -%}
END_TEMPLATE
expected = <<-END_EXPECTED
yes
END_EXPECTED
assert_template_result(expected, text)
text = <<-END_TEMPLATE
{%- if false %}
no
{% endif -%}
END_TEMPLATE
expected = <<-END_EXPECTED
END_EXPECTED
assert_template_result(expected, text)
end
def test_post_and_pre_trim_tags
text = <<-END_TEMPLATE
{% if true -%}
yes
{%- endif %}
END_TEMPLATE
expected = <<-END_EXPECTED
yes
END_EXPECTED
assert_template_result(expected, text)
whitespace = ' '
text = <<-END_TEMPLATE
{% if false -%}
no
{%- endif %}
END_TEMPLATE
expected = <<~END_EXPECTED
#{whitespace}
END_EXPECTED
assert_template_result(expected, text)
end
def test_trim_output
text = <<-END_TEMPLATE
{{- 'John' -}}
END_TEMPLATE
expected = <<-END_EXPECTED
John
END_EXPECTED
assert_template_result(expected, text)
end
def test_trim_tags
text = <<-END_TEMPLATE
{%- if true -%}
yes
{%- endif -%}
END_TEMPLATE
expected = <<-END_EXPECTED
yes
END_EXPECTED
assert_template_result(expected, text)
text = <<-END_TEMPLATE
{%- if false -%}
no
{%- endif -%}
END_TEMPLATE
expected = <<-END_EXPECTED
END_EXPECTED
assert_template_result(expected, text)
end
def test_whitespace_trim_output
text = <<-END_TEMPLATE
{{- 'John' -}},
{{- '30' -}}
END_TEMPLATE
expected = <<-END_EXPECTED
John,30
END_EXPECTED
assert_template_result(expected, text)
end
def test_whitespace_trim_tags
text = <<-END_TEMPLATE
{%- if true -%}
yes
{%- endif -%}
END_TEMPLATE
expected = <<-END_EXPECTED
yes
END_EXPECTED
assert_template_result(expected, text)
text = <<-END_TEMPLATE
{%- if false -%}
no
{%- endif -%}
END_TEMPLATE
expected = <<-END_EXPECTED
END_EXPECTED
assert_template_result(expected, text)
end
def test_complex_trim_output
text = <<-END_TEMPLATE
Comments
{% for comment in article.comments %}-
{{ comment.author }} said on {{ comment.created_at | date: "%B %d, %Y" }}:
{{ comment.content }}
{% endfor %}
Leave a comment
{% if form.posted_successfully? %} {% if blog.moderated? %}It will have to be approved by the blog owner first before showing up.
{% if blog.moderated? %}comments have to be approved before showing up
{% endif %} {% endform %}