Compare commits

...

16 Commits

Author SHA1 Message Date
Michael Go
50d1a2ffc9 Merge pull request #1458 from Shopify/use-to-liquid-value-with-conditions
use Utils.to_liquid_value on conditionals
2021-06-15 12:18:14 -03:00
Michael Go
f686c5dec7 use Utils.to_liquid_value on conditionals 2021-06-14 18:19:37 -03:00
Michael Go
aa8ce87b96 Merge pull request #1454 from Shopify/default-filter-with-to-liquid-value
utilize input's to_liquid_value on default filter
2021-06-14 15:43:56 -03:00
Peter Zhu
698f5e0d96 Merge pull request #1456 from Shopify/pz-psych-4-unsafe-load
Fix benchmark for breaking change in Psych 4
2021-06-10 13:14:13 -04:00
Peter Zhu
996bfe0c82 Fix benchmark for breaking change in Psych 4
Psych 4 introduces a breaking change (ruby/psych#487) where
`Psych#load`/`Psych#load_file` now default to safe loading,
meaning that YAML references are not allowed anymore. This
commit changes the benchmark to use `Psych#unsafe_load_file`
when it's available.
2021-06-10 13:01:16 -04:00
Michael Go
be81c9ae5a Merge pull request #1457 from Shopify/fix-unless-to-use-to-liquid-value-helper
fix unless to use to_liquid_value helper with multiple conditions
2021-06-10 14:00:19 -03:00
Michael Go
edd4d70aee fix unless to use to_liquid_value helper with multiple conditions 2021-06-10 13:28:17 -03:00
Michael Go
ac66dbbafe utilize input's to_liquid_value on default filter 2021-06-08 15:20:53 -03:00
Daniel Insley
017c1b5e83 Base64 Decode & Encode Filters (#1450) 2021-06-03 13:23:11 -04:00
Michael Go
250555c9a8 Merge pull request #1441 from Shopify/to-raw-value
Implement to_liquid_value to Liquid::Drop
2021-05-27 10:21:05 -03:00
Michael Go
e361a4d53c introduce to_liquid_value on variable look and conditional statements 2021-05-26 17:27:18 -03:00
Dylan Thacker-Smith
b9e0d28729 rubocop: Clarify that a config is a rubocop-shopify backport 2021-04-20 09:01:06 -07:00
Dylan Thacker-Smith
020f6b93c5 rubocop: Fix unsupported ruby version in TargetRubyVersion error 2021-04-20 09:00:20 -07:00
Dylan Thacker-Smith
cfe1637bdd Translate RangeError to Liquid::Error for truncatewords with large int (#1431) 2021-04-20 11:48:22 -04:00
Dylan Thacker-Smith
eab13a07d9 Add changelog entry for a recent fix 2021-03-29 13:43:01 -07:00
Dylan Thacker-Smith
ca96ca0fef Fix support for using a String subclass for the liquid source (#1421) 2021-03-29 16:22:05 -04:00
16 changed files with 216 additions and 13 deletions

View File

@@ -10,7 +10,7 @@ Performance:
Enabled: true
AllCops:
TargetRubyVersion: 2.4
TargetRubyVersion: 2.5
NewCops: disable
Exclude:
- 'vendor/bundle/**/*'
@@ -18,3 +18,7 @@ AllCops:
Naming/MethodName:
Exclude:
- 'example/server/liquid_servlet.rb'
# Backport https://github.com/Shopify/ruby-style-guide/pull/258
Layout/BeginEndAlignment:
Enabled: true

View File

@@ -1,5 +1,13 @@
# Liquid Change Log
## 5.0.2 (unreleased)
### Features
* Add `base64_encode`, `base64_decode`, `base64_url_safe_encode`, and `base64_url_safe_decode` filters (#1450) [Daniel Insley]
### Fixes
* Fix support for using a String subclass for the liquid source (#1421) [Dylan Thacker-Smith]
## 5.0.1 / 2021-03-24
### Fixes

View File

@@ -134,8 +134,8 @@ module Liquid
# return this as the result.
return context.evaluate(left) if op.nil?
left = context.evaluate(left)
right = context.evaluate(right)
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}")

View File

@@ -1,10 +1,12 @@
# frozen_string_literal: true
require 'cgi'
require 'base64'
require 'bigdecimal'
module Liquid
module StandardFilters
MAX_INT = (1 << 31) - 1
HTML_ESCAPE = {
'&' => '&amp;',
'>' => '&gt;',
@@ -62,6 +64,26 @@ module Liquid
result
end
def base64_encode(input)
Base64.strict_encode64(input.to_s)
end
def base64_decode(input)
Base64.strict_decode64(input.to_s)
rescue ::ArgumentError
raise Liquid::ArgumentError, "invalid base64 provided to base64_decode"
end
def base64_url_safe_encode(input)
Base64.urlsafe_encode64(input.to_s)
end
def base64_url_safe_decode(input)
Base64.urlsafe_decode64(input.to_s)
rescue ::ArgumentError
raise Liquid::ArgumentError, "invalid base64 provided to base64_url_safe_decode"
end
def slice(input, offset, length = nil)
offset = Utils.to_integer(offset)
length = length ? Utils.to_integer(length) : 1
@@ -93,7 +115,13 @@ module Liquid
words = Utils.to_integer(words)
words = 1 if words <= 0
wordlist = input.split(" ", words + 1)
wordlist = begin
input.split(" ", words + 1)
rescue RangeError
raise if words + 1 < MAX_INT
# e.g. integer #{words} too big to convert to `int'
raise Liquid::ArgumentError, "integer #{words} too big for truncatewords"
end
return input if wordlist.length <= words
wordlist.pop
@@ -440,7 +468,7 @@ module Liquid
#
def default(input, default_value = '', options = {})
options = {} unless options.is_a?(Hash)
false_check = options['allow_false'] ? input.nil? : !input
false_check = options['allow_false'] ? input.nil? : !Liquid::Utils.to_liquid_value(input)
false_check || (input.respond_to?(:empty?) && input.empty?) ? default_value : input
end

View File

@@ -52,7 +52,14 @@ module Liquid
@blocks.each do |block|
if block.else?
block.attachment.render_to_output_buffer(context, output) if execute_else_block
elsif block.evaluate(context)
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

View File

@@ -50,7 +50,11 @@ module Liquid
def render_to_output_buffer(context, output)
@blocks.each do |block|
if block.evaluate(context)
result = Liquid::Utils.to_liquid_value(
block.evaluate(context)
)
if result
return block.attachment.render_to_output_buffer(context, output)
end
end

View File

@@ -11,13 +11,21 @@ module Liquid
def render_to_output_buffer(context, output)
# First condition is interpreted backwards ( if not )
first_block = @blocks.first
unless first_block.evaluate(context)
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|
if block.evaluate(context)
result = Liquid::Utils.to_liquid_value(
block.evaluate(context)
)
if result
return block.attachment.render_to_output_buffer(context, output)
end
end

View File

@@ -5,7 +5,7 @@ module Liquid
attr_reader :line_number, :for_liquid_tag
def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false)
@source = source
@source = source.to_s.to_str
@line_number = line_number || (line_numbers ? 1 : nil)
@for_liquid_tag = for_liquid_tag
@tokens = tokenize
@@ -24,7 +24,7 @@ module Liquid
private
def tokenize
return [] if @source.to_s.empty?
return [] if @source.empty?
return @source.split("\n") if @for_liquid_tag

View File

@@ -81,5 +81,13 @@ module Liquid
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
end
end

View File

@@ -40,6 +40,9 @@ module Liquid
@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?(:[]) &&

View File

@@ -2,5 +2,5 @@
# frozen_string_literal: true
module Liquid
VERSION = "5.0.1"
VERSION = "5.0.2.alpha"
end

View File

@@ -3,11 +3,19 @@
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 = YAML.load_file("#{__dir__}/vision.database.yml")
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|

View File

@@ -145,6 +145,40 @@ class StandardFiltersTest < Minitest::Test
assert_equal('&lt;strong&gt;Hulk&lt;/strong&gt;', @filters.escape_once('&lt;strong&gt;Hulk</strong>'))
end
def test_base64_encode
assert_equal('b25lIHR3byB0aHJlZQ==', @filters.base64_encode('one two three'))
assert_equal('', @filters.base64_encode(nil))
end
def test_base64_decode
assert_equal('one two three', @filters.base64_decode('b25lIHR3byB0aHJlZQ=='))
exception = assert_raises(Liquid::ArgumentError) do
@filters.base64_decode("invalidbase64")
end
assert_equal('Liquid error: invalid base64 provided to base64_decode', exception.message)
end
def test_base64_url_safe_encode
assert_equal(
'YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXogQUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVogMTIzNDU2Nzg5MCAhQCMkJV4mKigpLT1fKy8_Ljo7W117fVx8',
@filters.base64_url_safe_encode('abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890 !@#$%^&*()-=_+/?.:;[]{}\|')
)
assert_equal('', @filters.base64_url_safe_encode(nil))
end
def test_base64_url_safe_decode
assert_equal(
'abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890 !@#$%^&*()-=_+/?.:;[]{}\|',
@filters.base64_url_safe_decode('YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXogQUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVogMTIzNDU2Nzg5MCAhQCMkJV4mKigpLT1fKy8_Ljo7W117fVx8')
)
exception = assert_raises(Liquid::ArgumentError) do
@filters.base64_url_safe_decode("invalidbase64")
end
assert_equal('Liquid error: invalid base64 provided to base64_url_safe_decode', exception.message)
end
def test_url_encode
assert_equal('foo%2B1%40example.com', @filters.url_encode('foo+1@example.com'))
assert_equal('1', @filters.url_encode(1))
@@ -178,6 +212,10 @@ class StandardFiltersTest < Minitest::Test
assert_equal('one two three...', @filters.truncatewords("one two\tthree\nfour", 3))
assert_equal('one two...', @filters.truncatewords("one two three four", 2))
assert_equal('one...', @filters.truncatewords("one two three four", 0))
exception = assert_raises(Liquid::ArgumentError) do
@filters.truncatewords("one two three four", 1 << 31)
end
assert_equal("Liquid error: integer #{1 << 31} too big for truncatewords", exception.message)
end
def test_strip_html
@@ -690,6 +728,8 @@ class StandardFiltersTest < Minitest::Test
assert_equal("bar", @filters.default([], "bar"))
assert_equal("bar", @filters.default({}, "bar"))
assert_template_result('bar', "{{ false | default: 'bar' }}")
assert_template_result('bar', "{{ drop | default: 'bar' }}", 'drop' => BooleanDrop.new(false))
assert_template_result('Yay', "{{ drop | default: 'bar' }}", 'drop' => BooleanDrop.new(true))
end
def test_default_handle_false
@@ -700,6 +740,8 @@ class StandardFiltersTest < Minitest::Test
assert_equal("bar", @filters.default([], "bar", "allow_false" => true))
assert_equal("bar", @filters.default({}, "bar", "allow_false" => true))
assert_template_result('false', "{{ false | default: 'bar', allow_false: true }}")
assert_template_result('Nay', "{{ drop | default: 'bar', allow_false: true }}", 'drop' => BooleanDrop.new(false))
assert_template_result('Yay', "{{ drop | default: 'bar', allow_false: true }}", 'drop' => BooleanDrop.new(true))
end
def test_cannot_access_private_methods

View File

@@ -323,4 +323,18 @@ class TemplateTest < Minitest::Test
result = t.render('x' => 1, 'y' => 5)
assert_equal('12345', result)
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
end

View File

@@ -15,6 +15,33 @@ class VariableTest < Minitest::Test
assert_template_result('foobar', '{{ foo }}', 'foo' => ThingWithToLiquid.new)
end
def test_variable_lookup_calls_to_liquid_value
assert_template_result('1', '{{ foo }}', 'foo' => IntegerDrop.new('1'))
assert_template_result('2', '{{ list[foo] }}', 'foo' => IntegerDrop.new('1'), 'list' => [1, 2, 3])
assert_template_result('one', '{{ list[foo] }}', 'foo' => IntegerDrop.new('1'), 'list' => { 1 => 'one' })
assert_template_result('Yay', '{{ foo }}', 'foo' => BooleanDrop.new(true))
assert_template_result('YAY', '{{ foo | upcase }}', 'foo' => BooleanDrop.new(true))
end
def test_if_tag_calls_to_liquid_value
assert_template_result('one', '{% if foo == 1 %}one{% endif %}', 'foo' => IntegerDrop.new('1'))
assert_template_result('one', '{% if 0 < foo %}one{% endif %}', 'foo' => IntegerDrop.new('1'))
assert_template_result('one', '{% if foo > 0 %}one{% endif %}', 'foo' => IntegerDrop.new('1'))
assert_template_result('true', '{% if foo == true %}true{% endif %}', 'foo' => BooleanDrop.new(true))
assert_template_result('true', '{% if foo %}true{% endif %}', 'foo' => BooleanDrop.new(true))
assert_template_result('', '{% if foo %}true{% endif %}', 'foo' => BooleanDrop.new(false))
assert_template_result('', '{% if foo == true %}True{% endif %}', 'foo' => BooleanDrop.new(false))
end
def test_unless_tag_calls_to_liquid_value
assert_template_result('', '{% unless foo %}true{% endunless %}', 'foo' => BooleanDrop.new(true))
end
def test_case_tag_calls_to_liquid_value
assert_template_result('One', '{% case foo %}{% when 1 %}One{% endcase %}', 'foo' => IntegerDrop.new('1'))
end
def test_simple_with_whitespaces
template = Template.parse(%( {{ test }} ))
assert_equal(' worked ', template.render!('test' => 'worked'))
@@ -104,4 +131,8 @@ class VariableTest < Minitest::Test
def test_dynamic_find_var
assert_template_result('bar', '{{ [key] }}', 'key' => 'foo', 'foo' => 'bar')
end
def test_raw_value_variable
assert_template_result('bar', '{{ [key] }}', 'key' => 'foo', 'foo' => 'bar')
end
end

View File

@@ -119,6 +119,44 @@ class ThingWithToLiquid
end
end
class IntegerDrop < Liquid::Drop
def initialize(value)
super()
@value = value.to_i
end
def ==(other)
@value == other
end
def to_s
@value.to_s
end
def to_liquid_value
@value
end
end
class BooleanDrop < Liquid::Drop
def initialize(value)
super()
@value = value
end
def ==(other)
@value == other
end
def to_liquid_value
@value
end
def to_s
@value ? "Yay" : "Nay"
end
end
class ErrorDrop < Liquid::Drop
def standard_error
raise Liquid::StandardError, 'standard error'