Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/liquid.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ module Liquid
require 'liquid/parser'
require 'liquid/i18n'
require 'liquid/drop'
require 'liquid/self_drop'
require 'liquid/tablerowloop_drop'
require 'liquid/forloop_drop'
require 'liquid/extensions'
Expand Down
13 changes: 13 additions & 0 deletions lib/liquid/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,15 @@ def key?(key)
find_variable(key, raise_on_not_found: false) != nil
end

# Checks whether a variable is defined in any scope, including nil-valued keys.
# Unlike #key?, this uses Hash#key? so that variables explicitly set to nil
# are still considered defined.
def variable_defined?(key)
@scopes.any? { |s| s.key?(key) } ||
@environments.any? { |e| e.key?(key) } ||
@static_environments.any? { |e| e.key?(key) }
end

def evaluate(object)
object.respond_to?(:evaluate) ? object.evaluate(self) : object
end
Expand All @@ -197,6 +206,10 @@ def find_variable(key, raise_on_not_found: true)
# path and find_index() is optimized in MRI to reduce object allocation
index = @scopes.find_index { |s| s.key?(key) }

# `self` resolves to a SelfDrop (enabling `self['var']` lookups),
# but only when it hasn't been explicitly assigned as a local variable.
return SelfDrop.new(self) if key == Expression::SELF && !index

variable = if index
lookup_and_evaluate(@scopes[index], key, raise_on_not_found: raise_on_not_found)
else
Expand Down
2 changes: 2 additions & 0 deletions lib/liquid/expression.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

module Liquid
class Expression
SELF = 'self'

LITERALS = {
nil => nil,
'nil' => nil,
Expand Down
2 changes: 1 addition & 1 deletion lib/liquid/parse_context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def new_block_body

def new_parser(input)
@string_scanner.string = input
Parser.new(@string_scanner)
Parser.new(@string_scanner, reject_bare_brackets: @error_mode == :strict2 || @error_mode == :rigid)
end

def new_tokenizer(source, start_line_number: nil, for_liquid_tag: false)
Expand Down
6 changes: 5 additions & 1 deletion lib/liquid/parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@

module Liquid
class Parser
def initialize(input)
def initialize(input, reject_bare_brackets: false)
ss = input.is_a?(StringScanner) ? input : StringScanner.new(input)
@tokens = Lexer.tokenize(ss)
@p = 0 # pointer to current location
@reject_bare_brackets = reject_bare_brackets
end

def jump(point)
Expand Down Expand Up @@ -53,6 +54,9 @@ def expression
str = consume
str << variable_lookups
when :open_square
if @reject_bare_brackets
raise SyntaxError, "Bare bracket access is not allowed in strict2 mode. Use #{Expression::SELF}['...'] instead"
end
str = consume.dup
str << expression
str << consume(:close_square)
Expand Down
38 changes: 38 additions & 0 deletions lib/liquid/self_drop.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# frozen_string_literal: true

module Liquid
# @liquid_public_docs
# @liquid_type object
# @liquid_name self
# @liquid_summary
# Provides access to variables through the current scope chain.
# @liquid_description
# The `self` object resolves variables through the normal lookup hierarchy
# (local > file > global) without exposing filters, interrupts, errors,
# or other context internals. It's used when bare bracket notation
# (`['variable']`) needs to be replaced with an explicit variable lookup.
#
# If `self` is explicitly assigned as a local variable (e.g. `{% assign self = 'value' %}`),
# then the local value takes precedence over the `self` object.
# @liquid_access global
class SelfDrop < Drop
def initialize(context)
super()
@context = context
end

def [](key)
@context.find_variable(key)
rescue UndefinedVariable
nil
end

def key?(key)
@context.variable_defined?(key)
end

def to_liquid
self
end
end
end
4 changes: 4 additions & 0 deletions lib/liquid/variable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ def raw
@markup
end

def ==(other)
self.class == other.class && name == other.name && filters == other.filters
end

def markup_context(markup)
"in \"{{#{markup}}}\""
end
Expand Down
4 changes: 2 additions & 2 deletions test/integration/context_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -296,8 +296,8 @@ def test_access_hashes_with_hash_notation
end

def test_access_variable_with_hash_notation
assert_template_result('baz', '{{ ["foo"] }}', { "foo" => "baz" })
assert_template_result('baz', '{{ [bar] }}', { 'foo' => 'baz', 'bar' => 'foo' })
assert_template_result('baz', '{{ foo }}', { "foo" => "baz" })
assert_template_result('baz', '{{ self[bar] }}', { 'foo' => 'baz', 'bar' => 'foo' })
end

def test_access_hashes_with_hash_access_variables
Expand Down
10 changes: 5 additions & 5 deletions test/integration/variable_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def test_simple_with_whitespaces

def test_expression_with_whitespace_in_square_brackets
assert_template_result('result', "{{ a[ 'b' ] }}", { 'a' => { 'b' => 'result' } })
assert_template_result('result', "{{ a[ [ 'b' ] ] }}", { 'b' => 'c', 'a' => { 'c' => 'result' } })
assert_template_result('result', "{{ a[ self[ 'b' ] ] }}", { 'b' => 'c', 'a' => { 'c' => 'result' } })
end

def test_ignore_unknown
Expand Down Expand Up @@ -135,17 +135,17 @@ def test_nested_array
end

def test_dynamic_find_var
assert_template_result('bar', '{{ [key] }}', { 'key' => 'foo', 'foo' => 'bar' })
assert_template_result('bar', '{{ self[key] }}', { 'key' => 'foo', 'foo' => 'bar' })
end

def test_raw_value_variable
assert_template_result('bar', '{{ [key] }}', { 'key' => 'foo', 'foo' => 'bar' })
assert_template_result('bar', '{{ self[key] }}', { 'key' => 'foo', 'foo' => 'bar' })
end

def test_dynamic_find_var_with_drop
assert_template_result(
'bar',
'{{ [list[settings.zero]] }}',
'{{ self[list[settings.zero]] }}',
{
'list' => ['foo'],
'settings' => SettingsDrop.new("zero" => 0),
Expand All @@ -155,7 +155,7 @@ def test_dynamic_find_var_with_drop

assert_template_result(
'foo',
'{{ [list[settings.zero]["foo"]] }}',
'{{ self[list[settings.zero]["foo"]] }}',
{
'list' => [{ 'foo' => 'bar' }],
'settings' => SettingsDrop.new("zero" => 0),
Expand Down
Loading