Compare commits

..
48 changed files with 1313 additions and 1032 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ jobs:
matrix:
entry:
- { ruby: 2.5, allowed-failure: false } # minimum supported
- { ruby: 3.1, allowed-failure: false } # latest
- { ruby: 3.0, allowed-failure: false } # latest
- { ruby: ruby-head, allowed-failure: true }
name: test (${{ matrix.entry.ruby }})
steps:
File diff suppressed because it is too large Load Diff
+2 -8
View File
@@ -1,7 +1,5 @@
inherit_gem:
rubocop-shopify: rubocop.yml
inherit_from:
- 'https://shopify.github.io/ruby-style-guide/rubocop.yml'
- .rubocop_todo.yml
require: rubocop-performance
@@ -10,7 +8,7 @@ Performance:
Enabled: true
AllCops:
TargetRubyVersion: 2.5
TargetRubyVersion: 2.4
NewCops: disable
Exclude:
- 'vendor/bundle/**/*'
@@ -18,7 +16,3 @@ AllCops:
Naming/MethodName:
Exclude:
- 'example/server/liquid_servlet.rb'
# Backport https://github.com/Shopify/ruby-style-guide/pull/258
Layout/BeginEndAlignment:
Enabled: true
+1 -4
View File
@@ -5,7 +5,6 @@
* Bugfixes
* Performance improvements
* Features that are likely to be useful to the majority of Liquid users
* Documentation updates that are concise and likely to be useful to the majority of Liquid users
## Things we won't merge
@@ -15,14 +14,12 @@
* Features that can easily be implemented on top of Liquid (for example as a custom filter or custom filesystem)
* Code that does not include tests
* Code that breaks existing tests
* Documentation changes that are verbose, incorrect or not important to most people (we want to keep it simple and easy to understand)
## Workflow
* [Sign the CLA](https://cla.shopify.com/) if you haven't already
* Fork the Liquid repository
* Create a new branch in your fork
* For updating [Liquid documentation](https://shopify.github.io/liquid/), create it from `gh-pages` branch. (You can skip tests.)
* If it makes sense, add tests for your code and/or run a performance benchmark
* Make sure all tests pass (`bundle exec rake`)
* Create a pull request
+1 -2
View File
@@ -19,10 +19,9 @@ end
group :test do
gem 'rubocop', '~> 1.4', require: false
gem 'rubocop-shopify', '~> 1.0.7', require: false
gem 'rubocop-performance', require: false
platform :mri, :truffleruby do
gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'master'
gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'inline-comment'
end
end
+2 -36
View File
@@ -1,43 +1,9 @@
# Liquid Change Log
## 5.3.0 (unreleased)
### Fixes
* StandardFilter: Fix missing @context on iterations (#1525) [Thierry Joyal]
### Deprecation
* Condition#evaluate to require mandatory context argument in Liquid 6.0.0 (#1527) [Thierry Joyal]
## 5.2.0 2022-03-01
## Unreleased
### Features
* Add `remove_last`, and `replace_last` filters (#1422) [Anders Hagbard]
* Eagerly cache global filters (#1524) [Jean Boussier]
### Fixes
* Fix some internal errors in filters from invalid input (#1476) [Dylan Thacker-Smith]
* Allow dash in filter kwarg name for consistency with Liquid::C (#1518) [CP Clermont]
## 5.1.0 / 2021-09-09
### Features
* Add `base64_encode`, `base64_decode`, `base64_url_safe_encode`, and `base64_url_safe_decode` filters (#1450) [Daniel Insley]
* Introduce `to_liquid_value` in `Liquid::Drop` (#1441) [Michael Go]
### Fixes
* Fix support for using a String subclass for the liquid source (#1421) [Dylan Thacker-Smith]
* Add `ParseTreeVisitor` to `RangeLookup` (#1470) [CP Clermont]
* Translate `RangeError` to `Liquid::Error` for `truncatewords` with large int (#1431) [Dylan Thacker-Smith]
## 5.0.1 / 2021-03-24
### Fixes
* Add ParseTreeVisitor to Echo tag (#1414) [CP Clermont]
* Test with ruby 3.0 as the latest ruby version (#1398) [Dylan Thacker-Smith]
* Handle carriage return in newlines_to_br (#1391) [Unending]
### Performance Improvements
* Use split limit in truncatewords (#1361) [Dylan Thacker-Smith]
* Allow `#` to be used as an inline comment tag (#1401) [Dylan Thacker-Smith]
## 5.0.0 / 2021-01-06
+2 -2
View File
@@ -5,7 +5,7 @@
* [Contributing guidelines](CONTRIBUTING.md)
* [Version history](History.md)
* [Liquid documentation from Shopify](https://shopify.dev/api/liquid)
* [Liquid documentation from Shopify](http://docs.shopify.com/themes/liquid-basics)
* [Liquid Wiki at GitHub](https://github.com/Shopify/liquid/wiki)
* [Website](http://liquidmarkup.org/)
@@ -56,7 +56,7 @@ For standard use you can just pass it the content of a file and call render with
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.
it very hard to debug and can lead to unexpected behaviour.
Liquid also comes with a stricter parser that can be used when editing templates to give better error messages
when templates are invalid. You can enable this new parser like this:
+2 -2
View File
@@ -36,7 +36,7 @@ module Liquid
VariableIncompleteEnd = /\}\}?/
QuotedString = /"[^"]*"|'[^']*'/
QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o
TagAttributes = /(\w[\w-]*)\s*\:\s*(#{QuotedFragment})/o
TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/o
AnyStartingTag = /#{TagStart}|#{VariableStart}/o
PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om
@@ -59,8 +59,8 @@ require 'liquid/forloop_drop'
require 'liquid/extensions'
require 'liquid/errors'
require 'liquid/interrupts'
require 'liquid/strainer_template'
require 'liquid/strainer_factory'
require 'liquid/strainer_template'
require 'liquid/expression'
require 'liquid/context'
require 'liquid/parser_switching'
+4 -16
View File
@@ -4,8 +4,8 @@ require 'English'
module Liquid
class BlockBody
LiquidTagToken = /\A\s*(\w+)\s*(.*?)\z/o
FullToken = /\A#{TagStart}#{WhitespaceControl}?(\s*)(\w+)(\s*)(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
LiquidTagToken = /\A\s*(\w+|#)\s*(.*?)\z/o
FullToken = /\A#{TagStart}#{WhitespaceControl}?(\s*)(\w+|#)(\s*)(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
WhitespaceOrNothing = /\A\s*\z/
TAGSTART = "{%"
@@ -35,18 +35,6 @@ module Liquid
super
end
# @public_docs
# @type tag
# @category theme
# @title liquid
# @summary
# Allows you to write multiple tags within one set of delimiters.
# @description
# Use the [`echo`](/api/liquid/tags/theme-tags#echo) tag to output an expression within a `liquid` tag.
# @syntax
# {% liquid
# statement
# %}
private def parse_for_liquid_tag(tokenizer, parse_context)
while (token = tokenizer.shift)
unless token.empty? || token =~ WhitespaceOrNothing
@@ -243,8 +231,8 @@ module Liquid
end
def create_variable(token, parse_context)
if token =~ ContentOfVariable
markup = Regexp.last_match(1)
token.scan(ContentOfVariable) do |content|
markup = content.first
return Variable.new(markup, parse_context)
end
BlockBody.raise_missing_variable_terminator(token, parse_context)
+4 -10
View File
@@ -8,7 +8,7 @@ module Liquid
# c = Condition.new(1, '==', 1)
# c.evaluate #=> true
#
class Condition # :nodoc:
class Condition #:nodoc:
@@operators = {
'==' => ->(cond, left, right) { cond.send(:equal_variables, left, right) },
'!=' => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
@@ -61,7 +61,7 @@ module Liquid
@child_condition = nil
end
def evaluate(context = deprecated_default_context)
def evaluate(context = Context.new)
condition = self
result = nil
loop do
@@ -134,8 +134,8 @@ module Liquid
# 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))
left = context.evaluate(left)
right = context.evaluate(right)
operation = self.class.operators[op] || raise(Liquid::ArgumentError, "Unknown operator #{op}")
@@ -150,12 +150,6 @@ module Liquid
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
[
+1 -1
View File
@@ -124,7 +124,7 @@ module Liquid
# context['var'] = 'hi'
# end
#
# context['var'] #=> nil
# context['var] #=> nil
def stack(new_scope = {})
push(new_scope)
yield
+10 -11
View File
@@ -10,23 +10,21 @@ module Liquid
'empty' => ''
}.freeze
INTEGERS_REGEX = /\A(-?\d+)\z/
FLOATS_REGEX = /\A(-?\d[\d\.]+)\z/
SINGLE_QUOTED_STRING = /\A\s*'(.*)'\s*\z/m
DOUBLE_QUOTED_STRING = /\A\s*"(.*)"\s*\z/m
INTEGERS_REGEX = /\A\s*(-?\d+)\s*\z/
FLOATS_REGEX = /\A\s*(-?\d[\d\.]+)\s*\z/
# 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/
RANGES_REGEX = /\A\s*\(\s*(?>(\S+)\s*\.\.)\s*(\S+)\s*\)\s*\z/
def self.parse(markup)
return nil unless markup
markup = markup.strip
if (markup.start_with?('"') && markup.end_with?('"')) ||
(markup.start_with?("'") && markup.end_with?("'"))
return markup[1..-2]
end
case markup
when nil
nil
when SINGLE_QUOTED_STRING, DOUBLE_QUOTED_STRING
Regexp.last_match(1)
when INTEGERS_REGEX
Regexp.last_match(1).to_i
when RANGES_REGEX
@@ -34,6 +32,7 @@ module Liquid
when FLOATS_REGEX
Regexp.last_match(1).to_f
else
markup = markup.strip
if LITERALS.key?(markup)
LITERALS[markup]
else
+1
View File
@@ -22,6 +22,7 @@
tag_never_closed: "'%{block_name}' tag was never closed"
table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
render: "Syntax error in tag 'render' - Template name must be a quoted string"
inline_comment_invalid: "Syntax error in tag '#' - Each line of comments must be prefixed by the '#' character"
argument:
include: "Argument error in tag 'include' - Illegal template name"
disabled:
-8
View File
@@ -12,8 +12,6 @@ module Liquid
end
end
attr_reader :start_obj, :end_obj
def initialize(start_obj, end_obj)
@start_obj = start_obj
@end_obj = end_obj
@@ -37,11 +35,5 @@ module Liquid
Utils.to_integer(input)
end
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[@node.start_obj, @node.end_obj]
end
end
end
end
+47 -385
View File
@@ -1,12 +1,10 @@
# frozen_string_literal: true
require 'cgi'
require 'base64'
require 'bigdecimal'
module Liquid
module StandardFilters
MAX_INT = (1 << 31) - 1
HTML_ESCAPE = {
'&' => '&amp;',
'>' => '&gt;',
@@ -28,47 +26,20 @@ module Liquid
end
# convert an input string to DOWNCASE
#
# @public_docs
# @type filter
# @summary Converts a string into lowercase.
# @category string
# @syntax {{ string | downcase }}
# @return string
def downcase(input)
input.to_s.downcase
end
# convert an input string to UPCASE
#
# @public_docs
# @type filter
# @summary Converts a string into uppercase.
# @category string
# @syntax {{ string | upcase }}
# @return string
def upcase(input)
input.to_s.upcase
end
# capitalize words in the input sentence
#
# @public_docs
# @type filter
# @summary Capitalizes the first word in a string.
# @category string
# @syntax {{ string | capitalize }}
# @return string
# capitalize words in the input centence
def capitalize(input)
input.to_s.capitalize
end
# @public_docs
# @type filter
# @summary Escapes a string.
# @category string
# @syntax {{ string | escape }}
# @return string
def escape(input)
CGI.escapeHTML(input.to_s) unless input.nil?
end
@@ -91,53 +62,6 @@ module Liquid
result
end
# @public_docs
# @type filter
# @title base64_encode
# @summary Encodes a string into Base64.
# @category string
# @syntax {{ string | base64_encode }}
# @return string
def base64_encode(input)
Base64.strict_encode64(input.to_s)
end
# @public_docs
# @type filter
# @summary Decodes a string from Base64.
# @category string
# @syntax {{ string | base64_decode }}
# @return string
def base64_decode(input)
Base64.strict_decode64(input.to_s)
rescue ::ArgumentError
raise Liquid::ArgumentError, "invalid base64 provided to base64_decode"
end
# @public_docs
# @type filter
# @summary Encodes a string into URL-safe Base64
# @category string
# @syntax {{ string | base64_url_safe_encode }}
# @return string
# @description
# To produce URL-safe Base64, this filter uses `-`` and `_`` in place of `+`` and `/``.
def base64_url_safe_encode(input)
Base64.urlsafe_encode64(input.to_s)
end
# @public_docs
# @type filter
# @summary Decodes a string from URL-safe Base64.
# @category string
# @syntax {{ string | base64_url_safe_decode }}
# @return string
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
@@ -149,34 +73,7 @@ module Liquid
end
end
# @public_docs
# @type filter
# @category string
# @summary
# Truncates a string down to a specified number of characters.
# @description
# By default, an ellipsis (`...`)
# is appended to the truncated string.
#
# > Tip:
# > The number of characters in both default and custom ellipses is included in the
# > character count for the truncated string. For example, if you want to truncate a string
# > down to ten characters and use the default ellipsis (`...`), then you should set the
# > character count parameter to `13`.
#
# ### Custom ellipsis
#
# The `truncate` filter accepts an optional parameter to specify a custom ellipsis to be
# appended to the truncated string.
#
# ### No ellipsis
#
# If you don't want an ellipsis appended to your truncated string, then you can set the
# ellipsis parameter to a blank string (`''`).
# @syntax {{ string | truncate: character_count, ellipsis }}
# @required_param character_count [number] The number of characters to include in the truncated string. Includes the characters in the ellipsis.
# @optional_param ellipsis [string] The format of the ellipsis appended to the truncated string. Default is `...`. Pass a blank string (`''`) to remove the ellipsis completely.
# @return string
# Truncate a string down to x characters
def truncate(input, length = 50, truncate_string = "...")
return if input.nil?
input_str = input.to_s
@@ -190,40 +87,13 @@ module Liquid
input_str.length > length ? input_str[0...l].concat(truncate_string_str) : input_str
end
# @public_docs
# @type filter
# @category string
# @summary
# Truncates a string down to a specified number of words.
# @description
# By default, an ellipsis (`...`) is appended to the truncated string.
#
# ### Custom ellipsis
#
# The `truncate` filter accepts an optional parameter to specify a custom ellipsis to be
# appended to the truncated string.
#
# ### No ellipsis
#
# If you don't want an ellipsis appended to your truncated string, then you can set the
# ellipsis parameter to a blank string (`''`).
# @syntax {{ string | truncatewords: word_count, ellipsis }}
# @required_param word_count [number] The number of words to include in the truncated string. Includes the characters in the ellipsis.
# @optional_param ellipsis [string] The format of the ellipsis appended to the truncated string. Default is `...`. Pass a blank string (`''`) to remove the ellipsis completely.
# @return string
def truncatewords(input, words = 15, truncate_string = "...")
return if input.nil?
input = input.to_s
words = Utils.to_integer(words)
words = 1 if words <= 0
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
wordlist = input.split(" ", words + 1)
return input if wordlist.length <= words
wordlist.pop
@@ -239,46 +109,18 @@ module Liquid
input.to_s.split(pattern.to_s)
end
# @public_docs
# @type filter
# @category string
# @summary
# Strips all whitespace, such as tabs, spaces, and newlines, from the left and right sides of a string.
# @syntax {{ string | strip }}
# @return string
def strip(input)
input.to_s.strip
end
# @public_docs
# @type filter
# @category string
# @summary
# Strips all whitespace, such as tabs, spaces, and newlines, from the left side of a string.
# @syntax {{ string | lstrip }}
# @return string
def lstrip(input)
input.to_s.lstrip
end
# @public_docs
# @type filter
# @category string
# @summary
# Strips all whitespace, such as tabs, spaces, and newlines, from the right side of a string.
# @syntax {{ string | rstrip }}
# @return string
def rstrip(input)
input.to_s.rstrip
end
# @public_docs
# @type filter
# @category string
# @summary
# Strips all HTML tags from a string.
# @syntax {{ string | strip_html }}
# @return string
def strip_html(input)
empty = ''
result = input.to_s.gsub(STRIP_HTML_BLOCKS, empty)
@@ -286,36 +128,18 @@ module Liquid
result
end
# @public_docs
# @type filter
# @category string
# @summary
# Strips all line breaks and newlines from a string.
# @syntax {{ string | strip_newlines }}
# @return string
# Remove all newlines from the string
def strip_newlines(input)
input.to_s.gsub(/\r?\n/, '')
end
# @public_docs
# @type filter
# @category array
# @summary Joins the elements in an array. The result is a single string.
# @syntax {{ array | join: ', ' }}
# @required_param delimiter [string] The separator to join the array elements with.
# @return string
# Join elements of the array with certain character between them
def join(input, glue = ' ')
InputIterator.new(input, context).join(glue)
end
# @public_docs
# @type filter
# @category array
# @summary Sorts the elements of an array.
# @description The order of the sorted array is case-sensitive.
# @syntax {{ array | sort: property }}
# @optional_param property [string] The property of the element to sort by.
# @return array
# Sort elements of the array
# provide optional property with which to sort an array of hashes or drops
def sort(input, property = nil)
ary = InputIterator.new(input, context)
@@ -361,23 +185,17 @@ module Liquid
if ary.empty?
[]
elsif target_value.nil?
ary.select do |item|
item[property]
elsif ary.first.respond_to?(:[]) && target_value.nil?
begin
ary.select { |item| item[property] }
rescue TypeError
raise_property_error(property)
rescue NoMethodError
return nil unless item.respond_to?(:[])
raise
end
else
ary.select do |item|
item[property] == target_value
elsif ary.first.respond_to?(:[])
begin
ary.select { |item| item[property] == target_value }
rescue TypeError
raise_property_error(property)
rescue NoMethodError
return nil unless item.respond_to?(:[])
raise
end
end
end
@@ -391,36 +209,22 @@ module Liquid
ary.uniq
elsif ary.empty? # The next two cases assume a non-empty array.
[]
else
ary.uniq do |item|
item[property]
elsif ary.first.respond_to?(:[])
begin
ary.uniq { |a| a[property] }
rescue TypeError
raise_property_error(property)
rescue NoMethodError
return nil unless item.respond_to?(:[])
raise
end
end
end
# @public_docs
# @type tag
# @category array
# @summary Reverses the order of the items in an array.
# @syntax {{ array | reverse }}
# @return array
# Reverse the elements of an array
def reverse(input)
ary = InputIterator.new(input, context)
ary.reverse
end
# @public_docs
# @type tag
# @category array
# @summary Accepts an array element's property as a parameter and creates an array out of the property value for each array element.
# @required_param property [string] The property to extract from the element.
# @syntax {{ array | map: 'property' }}
# @return array
# map/collect on a given property
def map(input, property)
InputIterator.new(input, context).map do |e|
e = e.call if e.is_a?(Proc)
@@ -445,26 +249,16 @@ module Liquid
ary.compact
elsif ary.empty? # The next two cases assume a non-empty array.
[]
else
ary.reject do |item|
item[property].nil?
elsif ary.first.respond_to?(:[])
begin
ary.reject { |a| a[property].nil? }
rescue TypeError
raise_property_error(property)
rescue NoMethodError
return nil unless item.respond_to?(:[])
raise
end
end
end
# @public_docs
# @summary Replaces all occurrences of a string with a substring.
# @type filter
# @category string
# @syntax {{ string | replace: original_string, replacement_string }}
# @required_param original_string [string] The string to replace.
# @required_param replacement_string [string] The replacement string.
# @return string
# Replace occurrences of a string with another
def replace(input, string, replacement = '')
input.to_s.gsub(string.to_s, replacement.to_s)
end
@@ -474,71 +268,21 @@ module Liquid
input.to_s.sub(string.to_s, replacement.to_s)
end
# Replace the last occurrences of a string with another
def replace_last(input, string, replacement)
input = input.to_s
string = string.to_s
replacement = replacement.to_s
start_index = input.rindex(string)
return input unless start_index
output = input.dup
output[start_index, string.length] = replacement
output
end
# @public_docs
# @summary Removes a substring from a string.
# @type filter
# @category string
# @syntax {{ string | remove: substring }}
# @required_param substring [string] The substring to remove from the string.
# @return string
# remove a substring
def remove(input, string)
replace(input, string, '')
input.to_s.gsub(string.to_s, '')
end
# @public_docs
# @summary Removes the first occurrences of a substring.
# @type filter
# @category string
# @syntax {{ string | remove_first: substring }}
# @required_param substring [string] The substring to remove from the string.
# @return string
# remove the first occurrences of a substring
def remove_first(input, string)
replace_first(input, string, '')
end
# remove the last occurences of a substring
def remove_last(input, string)
replace_last(input, string, '')
input.to_s.sub(string.to_s, '')
end
# add one string to another
#
# @public_docs
# @type filter
# @summary Appends characters to a string.
# @category string
# @syntax {{ string | append: to_append }}
# @required_param to_append [string] characters to append to the original string
# @return string
def append(input, string)
input.to_s + string.to_s
end
# @public_docs
# @type filter
# @category array
# @summary Concatenates (combines) an array with another array.
# @description
# The resulting array contains all the elements of the original arrays.
# `concat` won't remove duplicate entries from the concatenated array unless you also use the [`uniq`](/api/liquid/filters/array-filters#uniq) filter.
# @syntax {{ first_array | concat: second_array }}
# @required_param second_array [array] The array to concatenate with the primary array.
# @return array
def concat(input, array)
unless array.respond_to?(:to_ary)
raise ArgumentError, "concat filter requires an array argument"
@@ -546,13 +290,7 @@ module Liquid
InputIterator.new(input, context).concat(array)
end
# @public_docs
# @summary Prepends a string to another string.
# @type filter
# @category string
# @syntax {{ string | prepend: additional_string }}
# @required_param additional_string [string] The string to prepend to the original string.
# @return string
# prepend a string to another
def prepend(input, string)
string.to_s + input.to_s
end
@@ -601,101 +339,58 @@ module Liquid
date.strftime(format.to_s)
end
# @public_docs
# @type filter
# @category array
# @summary Returns the first element of an array.
# @syntax {{ array | first }}
# Get the first element of the passed in array
#
# Example:
# {{ product.images | first | to_img }}
#
def first(array)
array.first if array.respond_to?(:first)
end
# @public_docs
# @type filter
# @category array
# @summary Returns the last element of an array, or the last character inside of a string.
# @syntax {{ array | last }}
# Get the last element of the passed in array
#
# Example:
# {{ product.images | last | to_img }}
#
def last(array)
array.last if array.respond_to?(:last)
end
# @public_docs
# @syntax {{ number | abs }}
# @summary Returns the absolute value of a number.
# @type filter
# @category math
# @return number
# @description
# `abs` will also work on a string if the string only contains a number.
# absolute value
def abs(input)
result = Utils.to_number(input).abs
result.is_a?(BigDecimal) ? result.to_f : result
end
# @public_docs
# @summary Adds a number to an output.
# @type filter
# @category math
# @syntax {{ number | plus: number }}
# @required_param number [number] The number to add to the original number.
# @return number
# addition
def plus(input, operand)
apply_operation(input, operand, :+)
end
# @public_docs
# @summary Subtracts a number from an output.
# @type filter
# @category math
# @syntax {{ number | minus: number }}
# @return number
# subtraction
def minus(input, operand)
apply_operation(input, operand, :-)
end
# @public_docs
# @summary Multiplies an output by a number.
# @type filter
# @category math
# @syntax {{ number | times: number }}
# @required_param number [number] The number to multiply the original number by.
# @return number
# multiplication
def times(input, operand)
apply_operation(input, operand, :*)
end
# @public_docs
# @summary Divides an output by a number. The output is rounded down to the nearest integer.
# @type filter
# @category math
# @syntax {{ number | divided_by: number }}
# @return number
# division
def divided_by(input, operand)
apply_operation(input, operand, :/)
rescue ::ZeroDivisionError => e
raise Liquid::ZeroDivisionError, e.message
end
# @public_docs
# @summary Divides an output by a number and returns the remainder.
# @type filter
# @category math
# @syntax {{ number | modulo: number }}
# @required_param number [number] The number to divide the original number by.
# @return number
def modulo(input, operand)
apply_operation(input, operand, :%)
rescue ::ZeroDivisionError => e
raise Liquid::ZeroDivisionError, e.message
end
# @public_docs
# @summary Rounds the output to the nearest integer or specified number of decimals.
# @type filter
# @category math
# @syntax {{ number | round }}
# @optional_param decimals [number] The number of decimal places to round to.
# @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)
@@ -705,37 +400,18 @@ module Liquid
raise Liquid::FloatDomainError, e.message
end
# @public_docs
# @summary Rounds an output up to the nearest integer.
# @type filter
# @category math
# @syntax {{ number | ceil }}
# @return number
def ceil(input)
Utils.to_number(input).ceil.to_i
rescue ::FloatDomainError => e
raise Liquid::FloatDomainError, e.message
end
# @public_docs
# @summary Rounds an output down to the nearest integer.
# @type filter
# @category math
# @syntax {{ number | floor }}
# @return number
def floor(input)
Utils.to_number(input).floor.to_i
rescue ::FloatDomainError => e
raise Liquid::FloatDomainError, e.message
end
# @public_docs
# @summary Limits a number to a minimum value.
# @type filter
# @category math
# @syntax {{ number | at_least: number }}
# @required_param number [number] The minimum valid value.
# @return number
def at_least(input, n)
min_value = Utils.to_number(n)
@@ -744,13 +420,6 @@ module Liquid
result.is_a?(BigDecimal) ? result.to_f : result
end
# @public_docs
# @summary Limits a number to a maximum value.
# @type filter
# @category math
# @syntax {{ number | at_most: number }}
# @required_param number [number] The maximum valid value.
# @return number
def at_most(input, n)
max_value = Utils.to_number(n)
@@ -771,7 +440,7 @@ module Liquid
#
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 = options['allow_false'] ? input.nil? : !input
false_check || (input.respond_to?(:empty?) && input.empty?) ? default_value : input
end
@@ -789,16 +458,10 @@ module Liquid
end
def nil_safe_compare(a, b)
result = a <=> b
if result
result
elsif a.nil?
1
elsif b.nil?
-1
if !a.nil? && !b.nil?
a <=> b
else
raise Liquid::ArgumentError, "cannot sort values of incompatible types"
a.nil? ? 1 : -1
end
end
@@ -853,9 +516,8 @@ module Liquid
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)
yield(e.respond_to?(:to_liquid) ? e.to_liquid : e)
end
end
end
+1 -5
View File
@@ -31,11 +31,7 @@ module Liquid
if @registers.key?(key)
@registers.fetch(key)
elsif default != UNDEFINED
if block_given?
@static.fetch(key, &block)
else
@static.fetch(key, default)
end
@static.fetch(key, default, &block)
else
@static.fetch(key, &block)
end
+10 -11
View File
@@ -7,26 +7,25 @@ module Liquid
def add_global_filter(filter)
strainer_class_cache.clear
GlobalCache.add_filter(filter)
global_filters << filter
end
def create(context, filters = [])
strainer_from_cache(filters).new(context)
end
GlobalCache = Class.new(StrainerTemplate)
private
def global_filters
@global_filters ||= []
end
def strainer_from_cache(filters)
if filters.empty?
GlobalCache
else
strainer_class_cache[filters] ||= begin
klass = Class.new(GlobalCache)
filters.each { |f| klass.add_filter(f) }
klass
end
strainer_class_cache[filters] ||= begin
klass = Class.new(StrainerTemplate)
global_filters.each { |f| klass.add_filter(f) }
filters.each { |f| klass.add_filter(f) }
klass
end
end
-5
View File
@@ -31,11 +31,6 @@ module Liquid
filter_methods.include?(method.to_s)
end
def inherited(subclass)
super
subclass.instance_variable_set(:@filter_methods, @filter_methods.dup)
end
private
def filter_methods
+1 -29
View File
@@ -1,27 +1,6 @@
# frozen_string_literal: true
module Liquid
# @public_docs
# @title case
# @type tag
# @category controlflow
# @summary Creates a switch statement to execute a particular block of code when a variable has a specified value.
# @description
# `case` initializes the switch statement, and `when` statements define the various conditions.
#
# An optional `else` statement at the end of the case provides code to execute if none of the conditions are met.
# @syntax
# {% case variable %}
# {% when value1 %}
# statement1
# {% when value2 %}
# statement2
# {% when value3 %}
# statement3
# ...
# {% else %}
# else_statement
# {% endcase %}
class Case < Block
Syntax = /(#{QuotedFragment})/o
WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om
@@ -73,14 +52,7 @@ module Liquid
@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
elsif block.evaluate(context)
execute_else_block = false
block.attachment.render_to_output_buffer(context, output)
end
+1 -12
View File
@@ -1,17 +1,6 @@
# frozen_string_literal: true
module Liquid
# @public_docs
# @type tag
# @category theme
# @title comment
# @summary
# Allows you to comment out parts of a Liquid file.
# Any text within the opening and closing `comment` blocks won't be output,
# and any Liquid code won't be executed.
# @syntax
# {% comment %}
# statement
# {% endcomment %}
class Comment < Block
def render_to_output_buffer(_context, output)
output
-13
View File
@@ -13,19 +13,6 @@ module Liquid
# <div class="red"> Item four </div>
# <div class="green"> Item five</div>
#
# @public_docs
# @title cycle
# @type tag
# @category iteration
# @summary Loops through a group of strings and prints them in the order that they were passed as parameters.
# @description
# Each time `cycle` is called, the next string that was passed as a parameter is printed.
#
# `cycle` must be used within a `for` loop block.
# @syntax
# {% for condition %}
# {% cycle value1, value2 ... %}
# {% endfor %}
class Cycle < Tag
SimpleSyntax = /\A#{QuotedFragment}+/o
NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om
+10 -18
View File
@@ -1,19 +1,17 @@
# frozen_string_literal: true
module Liquid
# @public_docs
# @type tag
# @category theme
# @title echo
# @summary
# Outputs an expression, or Liquid object, in the rendered HTML.
# Works the same as wrapping an expression in double curly brace delimiters `{{ }}`.
# Works inside the [`liquid`](/api/liquid/tags/theme-tags#liquid) tag and supports [filters](/api/liquid/filters).
# @syntax
# {% echo 'string' %}
# Echo outputs an expression
#
# {% echo monkey %}
# {% echo user.name %}
#
# This is identical to variable output syntax, like {{ foo }}, but works
# inside {% liquid %} tags. The full syntax is supported, including filters:
#
# {% echo user | link %}
#
class Echo < Tag
attr_reader :variable
def initialize(tag_name, markup, parse_context)
super
@variable = Variable.new(markup, parse_context)
@@ -22,12 +20,6 @@ module Liquid
def render(context)
@variable.render_to_output_buffer(context, +'')
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[@node.variable]
end
end
end
Template.register_tag('echo', Echo)
-14
View File
@@ -45,20 +45,6 @@ module Liquid
# forloop.last:: Returns true if the item is the last item.
# forloop.parentloop:: Provides access to the parent loop, if present.
#
# @public_docs
# @title for
# @type tag
# @category iteration
# @summary Repeatedly executes a block of code.
# @description
# You can output a maximum of 50 results per page with `for` loops. In cases where there are more than 50 results,
# use the [`paginate`](/api/liquid/tags/theme-tags#paginate) tag to split them across multiple pages.
#
# For a full list of attributes available within a `for` loop, refer to the [`forloop`](/api/liquid/objects/for-loops) object.
# @syntax
# {% for variable in iterable parameters %}
# @optional_param limit [number] Exit the `for` loop at the specified index.
# @optional_param offset [number] Start the `for` loop at the specified index.
class For < Block
Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
+8 -13
View File
@@ -3,15 +3,14 @@
module Liquid
# If is the conditional block
#
# @public_docs
# @title if
# @type tag
# @category controlflow
# @summary Executes a block of code only if a certain condition is met (if the result is `truthy`).
# @syntax
# {% if variable operator value %}
# statement
# {% if user.admin %}
# Admin user!
# {% else %}
# Not admin user
# {% endif %}
#
# There are {% if count < 5 %} less {% else %} more {% endif %} items than you need.
#
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
@@ -51,11 +50,7 @@ module Liquid
def render_to_output_buffer(context, output)
@blocks.each do |block|
result = Liquid::Utils.to_liquid_value(
block.evaluate(context)
)
if result
if block.evaluate(context)
return block.attachment.render_to_output_buffer(context, output)
end
end
+25
View File
@@ -0,0 +1,25 @@
# 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.
if markup.match?(/\n\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
Template.register_tag('#', InlineComment)
end
-10
View File
@@ -1,16 +1,6 @@
# frozen_string_literal: true
module Liquid
# @public_docs
# @type tag
# @category theme
# @title raw
# @summary
# Allows you to output Liquid code on a page without it being parsed.
# @syntax
# {% raw %}
# liquid
# {% endraw %}
class Raw < Block
Syntax = /\A\s*\z/
FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
-22
View File
@@ -1,28 +1,6 @@
# frozen_string_literal: true
module Liquid
# @public_docs
# @type tag
# @category theme
# @title render
# @summary
# Renders a snippet from the **snippets** folder of a theme, or [code for an app block](/themes/architecture/sections/section-schema#render-app-blocks).
# You don't need to write the file's `.liquid` extension.
# @description
# When a snippet is rendered, the code inside it doesn't automatically have access to the variables assigned
# using [variable tags](/api/liquid/tags/variable-tags) within the snippet's parent template.
# Similarly, variables assigned within the snippet can't be accessed by the code outside of the snippet.
# This encapsulation increases performance and helps make theme code easier to understand and maintain.
#
# You can't use an [`include`](/api/liquid/tags/theme-tags#include) tag inside of a snippet rendered using the `render` tag.
#
# > Tip:
# > This tag replaces the deprecated [`include`](/api/liquid/tags/theme-tags#include) tag.
# @syntax
# {% render reference %}
# @optional_param with [string] Pass an object to use in the snippet. Use with the `as` parameter.
# @optional_param for [string] Render the snippet once for each value of an enumerable object. Use with the `as` parameter. When using the `for` parameter, the [`forloop`](/api/liquid/objects/for-loops) object is accessible within the snippet.
# @optional_param as [string] The variable that the object referenced by a `with` or `for` parameter represents within the snippet.
class Render < Tag
FOR = 'for'
SYNTAX = /(#{QuotedString}+)(\s+(with|#{FOR})\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
-17
View File
@@ -1,23 +1,6 @@
# frozen_string_literal: true
module Liquid
# @public_docs
# @title tablerow
# @type tag
# @category iteration
# @summary Generates rows for an HTML table.
# @description
# Must be wrapped in opening `<table>` and closing `</table>` HTML tags.
# For a full list of attributes available within a `tablerow` loop, refer to the [`tablerow`](/api/liquid/objects/tablerow) object.
# @syntax
# <table>
# {% tablerow variable in interable parameters %}
# content
# {% endtablerow %}
# </table>
# @optional_param cols [number] The number of columns the table should have.
# @optional_param limit [number] Exit the `tablerow` loop at the specified index.
# @optional_param offset [number] Start the `tablerow` loop at the specified index.
class TableRow < Block
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
+6 -19
View File
@@ -3,34 +3,21 @@
require_relative 'if'
module Liquid
# @public_docs
# @title unless
# @type tag
# @category controlflow
# @summary Executes a block of code only if a certain condition is not met (if the result is `falsy`).
# @syntax
# {% unless variable operator value %}
# statement
# {% endunless %}
# Unless is a conditional just like 'if' but works on the inverse logic.
#
# {% unless x < 0 %} x is greater than zero {% endunless %}
#
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
unless first_block.evaluate(context)
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
if block.evaluate(context)
return block.attachment.render_to_output_buffer(context, output)
end
end
+2 -2
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.to_s.to_str
@source = source
@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.empty?
return [] if @source.to_s.empty?
return @source.split("\n") if @for_liquid_tag
-8
View File
@@ -81,13 +81,5 @@ 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
-3
View File
@@ -40,9 +40,6 @@ 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?(:[]) &&
+1 -1
View File
@@ -2,5 +2,5 @@
# frozen_string_literal: true
module Liquid
VERSION = "5.3.0.alpha"
VERSION = "5.0.0"
end
+1 -9
View File
@@ -3,19 +3,11 @@
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
db = YAML.load_file("#{__dir__}/vision.database.yml")
# From vision source
db['products'].each do |product|
+14 -12
View File
@@ -24,7 +24,7 @@ class ContextSensitiveDrop < Liquid::Drop
end
end
class Category
class Category < Liquid::Drop
attr_accessor :name
def initialize(name)
@@ -36,9 +36,8 @@ class Category
end
end
class CategoryDrop < Liquid::Drop
class CategoryDrop
attr_accessor :category, :context
def initialize(category)
@category = category
end
@@ -406,42 +405,45 @@ class ContextTest < Minitest::Test
end
def test_lambda_is_called_once
@global = 0
@context['callcount'] = proc {
@global += 1
@global ||= 0
@global += 1
@global.to_s
}
assert_equal('1', @context['callcount'])
assert_equal('1', @context['callcount'])
assert_equal('1', @context['callcount'])
@global = nil
end
def test_nested_lambda_is_called_once
@global = 0
@context['callcount'] = { "lambda" => proc {
@global += 1
@global ||= 0
@global += 1
@global.to_s
} }
assert_equal('1', @context['callcount.lambda'])
assert_equal('1', @context['callcount.lambda'])
assert_equal('1', @context['callcount.lambda'])
@global = nil
end
def test_lambda_in_array_is_called_once
@global = 0
@context['callcount'] = [1, 2, proc {
@global += 1
@global ||= 0
@global += 1
@global.to_s
}, 4, 5]
assert_equal('1', @context['callcount[2]'])
assert_equal('1', @context['callcount[2]'])
assert_equal('1', @context['callcount[2]'])
@global = nil
end
def test_access_to_context_from_proc
-24
View File
@@ -1,24 +0,0 @@
# frozen_string_literal: true
require 'test_helper'
class FilterKwargTest < Minitest::Test
module KwargFilter
def html_tag(_tag, attributes)
attributes
.map { |key, value| "#{key}='#{value}'" }
.join(' ')
end
end
include Liquid
def test_can_parse_data_kwargs
with_global_filter(KwargFilter) do
assert_equal(
"data-src='src' data-widths='100, 200'",
Template.parse("{{ 'img' | html_tag: data-src: 'src', data-widths: '100, 200' }}").render(nil, nil)
)
end
end
end
+6 -33
View File
@@ -3,27 +3,6 @@
require 'test_helper'
class ProfilerTest < Minitest::Test
class TestDrop < Liquid::Drop
def initialize(value)
super()
@value = value
end
def to_s
artificial_execution_time
@value
end
private
# Monotonic clock precision fluctuate based on the operating system
# By introducing a small sleep we ensure ourselves to register a non zero unit of time
def artificial_execution_time
sleep(Process.clock_getres(Process::CLOCK_MONOTONIC))
end
end
include Liquid
class ProfilingFileSystem
@@ -219,22 +198,16 @@ class ProfilerTest < Minitest::Test
def test_profiling_supports_self_time
t = Template.parse("{% for item in collection %} {{ item }} {% endfor %}", profile: true)
collection = [
TestDrop.new("one"),
TestDrop.new("two"),
]
output = t.render!("collection" => collection)
assert_equal(" one two ", output)
t.render!("collection" => ["one", "two"])
leaf = t.profiler[0].children[0]
assert_operator(leaf.self_time, :>, 0.0)
assert_operator(leaf.self_time, :>, 0)
end
def test_profiling_supports_total_time
t = Template.parse("{% if true %} {{ test }} {% endif %}", profile: true)
output = t.render!("test" => TestDrop.new("one"))
assert_equal(" one ", output)
t = Template.parse("{% if true %} {% increment test %} {{ test }} {% endif %}", profile: true)
t.render!
assert_operator(t.profiler[0].total_time, :>, 0.0)
assert_operator(t.profiler[0].total_time, :>, 0)
end
end
+33 -120
View File
@@ -3,6 +3,10 @@
require 'test_helper'
class Filters
include Liquid::StandardFilters
end
class TestThing
attr_reader :foo
@@ -25,24 +29,8 @@ class TestThing
end
class TestDrop < Liquid::Drop
def initialize(value:)
@value = value
end
attr_reader :value
def registers
@context.registers
end
end
class TestModel
def initialize(value:)
@value = value
end
def to_liquid
TestDrop.new(value: @value)
def test
"testfoo"
end
end
@@ -65,13 +53,10 @@ class NumberLikeThing < Liquid::Drop
end
class StandardFiltersTest < Minitest::Test
Filters = Class.new(Liquid::StrainerTemplate)
Filters.add_filter(Liquid::StandardFilters)
include Liquid
def setup
@filters = Filters.new(Context.new)
@filters = Filters.new
end
def test_size
@@ -160,40 +145,6 @@ 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('[email protected]'))
assert_equal('1', @filters.url_encode(1))
@@ -220,17 +171,13 @@ class StandardFiltersTest < Minitest::Test
assert_equal('one two three', @filters.truncatewords('one two three'))
assert_equal(
'Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221;...',
@filters.truncatewords('Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221; x 16&#8221; x 10.5&#8221; high) with cover.', 15)
@filters.truncatewords('Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221; x 16&#8221; x 10.5&#8221; high) with cover.', 15)
)
assert_equal("测试测试测试测试", @filters.truncatewords('测试测试测试测试', 5))
assert_equal('one two1', @filters.truncatewords("one two three", 2, 1))
assert_equal('one two three...', @filters.truncatewords("one two\tthree\nfour", 3))
assert_equal('one two...', @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
@@ -274,8 +221,8 @@ class StandardFiltersTest < Minitest::Test
{ "price" => 1, "handle" => "gamma" },
{ "price" => 2, "handle" => "epsilon" },
{ "price" => 4, "handle" => "alpha" },
{ "handle" => "beta" },
{ "handle" => "delta" },
{ "handle" => "beta" },
]
assert_equal(expectation, @filters.sort(input, "price"))
end
@@ -378,9 +325,8 @@ class StandardFiltersTest < Minitest::Test
assert_equal(["foo"], @filters.uniq("foo"))
assert_equal([1, 3, 2, 4], @filters.uniq([1, 1, 3, 2, 3, 1, 4, 3, 2, 1]))
assert_equal([{ "a" => 1 }, { "a" => 3 }, { "a" => 2 }], @filters.uniq([{ "a" => 1 }, { "a" => 3 }, { "a" => 1 }, { "a" => 2 }], "a"))
test_drop = TestDrop.new(value: "test")
test_drop_alternate = TestDrop.new(value: "test")
assert_equal([test_drop], @filters.uniq([test_drop, test_drop_alternate], 'value'))
testdrop = TestDrop.new
assert_equal([testdrop], @filters.uniq([testdrop, TestDrop.new], 'test'))
end
def test_uniq_empty_array
@@ -439,16 +385,6 @@ class StandardFiltersTest < Minitest::Test
assert_template_result("woot: 1", '{{ foo | map: "whatever" }}', "foo" => [t])
end
def test_map_calls_context=
model = TestModel.new(value: "test")
template = Template.parse('{{ foo | map: "registers" }}')
template.registers[:test] = 1234
template.assigns['foo'] = [model]
assert_template_result("{:test=>1234}", template.render!)
end
def test_map_on_hashes
assert_template_result("4217", '{{ thing | map: "foo" | map: "bar" }}',
"thing" => { "foo" => [{ "bar" => 42 }, { "bar" => 17 }] })
@@ -467,9 +403,9 @@ class StandardFiltersTest < Minitest::Test
end
def test_map_over_proc
drop = TestDrop.new(value: "testfoo")
drop = TestDrop.new
p = proc { drop }
templ = '{{ procs | map: "value" }}'
templ = '{{ procs | map: "test" }}'
assert_template_result("testfoo", templ, "procs" => [p])
end
@@ -565,31 +501,19 @@ class StandardFiltersTest < Minitest::Test
end
def test_replace
assert_equal('b b b b', @filters.replace('a a a a', 'a', 'b'))
assert_equal('2 2 2 2', @filters.replace('1 1 1 1', '1', 2))
assert_equal('2 2 2 2', @filters.replace('1 1 1 1', 1, 2))
assert_equal('1 1 1 1', @filters.replace('1 1 1 1', 2, 3))
assert_template_result('2 2 2 2', "{{ '1 1 1 1' | replace: '1', 2 }}")
assert_equal('b a a a', @filters.replace_first('a a a a', 'a', 'b'))
assert_equal('2 1 1 1', @filters.replace_first('1 1 1 1', '1', 2))
assert_equal('2 1 1 1', @filters.replace_first('1 1 1 1', 1, 2))
assert_equal('1 1 1 1', @filters.replace_first('1 1 1 1', 2, 3))
assert_template_result('2 1 1 1', "{{ '1 1 1 1' | replace_first: '1', 2 }}")
assert_equal('a a a b', @filters.replace_last('a a a a', 'a', 'b'))
assert_equal('1 1 1 2', @filters.replace_last('1 1 1 1', 1, 2))
assert_equal('1 1 1 1', @filters.replace_last('1 1 1 1', 2, 3))
assert_template_result('1 1 1 2', "{{ '1 1 1 1' | replace_last: '1', 2 }}")
end
def test_remove
assert_equal(' ', @filters.remove("a a a a", 'a'))
assert_template_result(' ', "{{ '1 1 1 1' | remove: 1 }}")
assert_equal('b a a', @filters.remove_first("a b a a", 'a '))
assert_template_result(' 1 1 1', "{{ '1 1 1 1' | remove_first: 1 }}")
assert_equal('a a b', @filters.remove_last("a a b a", ' a'))
assert_template_result('1 1 1 ', "{{ '1 1 1 1' | remove_last: 1 }}")
assert_equal(' ', @filters.remove("1 1 1 1", 1))
assert_equal('a a a', @filters.remove_first("a a a a", 'a '))
assert_equal(' 1 1 1', @filters.remove_first("1 1 1 1", 1))
assert_template_result('a a a', "{{ 'a a a a' | remove_first: 'a ' }}")
end
def test_pipes_in_string_arguments
@@ -766,8 +690,6 @@ 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
@@ -778,8 +700,6 @@ 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
@@ -808,18 +728,6 @@ class StandardFiltersTest < Minitest::Test
assert_equal(expectation, @filters.where(input, "ok"))
end
def test_where_string_keys
input = [
"alpha", "beta", "gamma", "delta"
]
expectation = [
"beta",
]
assert_equal(expectation, @filters.where(input, "be"))
end
def test_where_no_key_set
input = [
{ "handle" => "alpha", "ok" => true },
@@ -865,7 +773,7 @@ class StandardFiltersTest < Minitest::Test
end
def test_all_filters_never_raise_non_liquid_exception
test_drop = TestDrop.new(value: "test")
test_drop = TestDrop.new
test_drop.context = Context.new
test_enum = TestEnumerable.new
test_enum.context = Context.new
@@ -890,14 +798,19 @@ class StandardFiltersTest < Minitest::Test
{ 1 => "bar" },
["foo", 123, nil, true, false, Drop, ["foo"], { foo: "bar" }],
]
StandardFilters.public_instance_methods(false).each do |method|
arg_count = @filters.method(method).arity
arg_count *= -1 if arg_count < 0
test_types.repeated_permutation(arg_count) do |args|
@filters.send(method, *args)
rescue Liquid::Error
nil
test_types.each do |first|
test_types.each do |other|
(@filters.methods - Object.methods).each do |method|
arg_count = @filters.method(method).arity
arg_count *= -1 if arg_count < 0
inputs = [first]
inputs << ([other] * (arg_count - 1)) if arg_count > 1
begin
@filters.send(method, *inputs)
rescue Liquid::ArgumentError, Liquid::ZeroDivisionError
nil
end
end
end
end
end
+2 -2
View File
@@ -96,12 +96,12 @@ class IncludeTagTest < Minitest::Test
def test_include_tag_with_alias
assert_template_result("Product: Draft 151cm ",
"{% include 'product_alias' with products[0] as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
"{% include 'product_alias' with products[0] as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end
def test_include_tag_for_alias
assert_template_result("Product: Draft 151cm Product: Element 155cm ",
"{% include 'product_alias' for products as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
"{% include 'product_alias' for products as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end
def test_include_tag_with_default_name
@@ -0,0 +1,59 @@
# frozen_string_literal: true
require 'test_helper'
class InlineCommentTest < Minitest::Test
include Liquid
def test_tag_in_different_styles
assert_template_result('', '{% # This text gets ignored %}')
assert_template_result('', '{%# This text gets ignored #%}')
assert_template_result('', '{%# This text gets ignored %}')
assert_template_result('', '{%#- This text gets ignored -#%}')
end
def test_test_syntax_error
assert_template_result('fail', '{% #This doesnt work %}')
assert false
rescue
# ok good
end
def test_tag_ws_stripping
assert_template_result('', ' {%#- This text gets ignored -#%} ')
end
def test_comment_inline_tag
assert_template_result('ok', '{% echo "ok" # output something from a tag %}')
end
def test_comment_line_before_tag
assert_template_result('ok', '{% # this sort of comment also
echo "ok" %}')
end
def test_comment_inline_variable
assert_template_result('ok', '{{ "ok" # output something from a variable }}')
assert_template_result('ok', '{{ "OK" | downcase # output something from a variable }}')
end
def test_inside_liquid_tag
source = <<~LIQUID
{%- liquid
echo "before("
# This text gets ignored
echo ")after"
-%}
LIQUID
assert_template_result('before()after', source)
end
def test_multiline
assert_template_result('', '{% # this sort of comment also
# will just work, because it parses
# as a single call to the "#" tag %}')
end
end
+7 -7
View File
@@ -151,7 +151,7 @@ class RenderTagTest < Minitest::Test
)
assert_template_result("Product: Draft 151cm ",
"{% render 'product' with products[0] %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
"{% render 'product' with products[0] %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end
def test_render_tag_with_alias
@@ -161,7 +161,7 @@ class RenderTagTest < Minitest::Test
)
assert_template_result("Product: Draft 151cm ",
"{% render 'product_alias' with products[0] as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
"{% render 'product_alias' with products[0] as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end
def test_render_tag_for_alias
@@ -171,7 +171,7 @@ class RenderTagTest < Minitest::Test
)
assert_template_result("Product: Draft 151cm Product: Element 155cm ",
"{% render 'product_alias' for products as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
"{% render 'product_alias' for products as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end
def test_render_tag_for
@@ -181,7 +181,7 @@ class RenderTagTest < Minitest::Test
)
assert_template_result("Product: Draft 151cm Product: Element 155cm ",
"{% render 'product' for products %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
"{% render 'product' for products %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end
def test_render_tag_forloop
@@ -190,7 +190,7 @@ class RenderTagTest < Minitest::Test
)
assert_template_result("Product: Draft 151cm first index:1 Product: Element 155cm last index:2 ",
"{% render 'product' for products %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
"{% render 'product' for products %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end
def test_render_tag_for_drop
@@ -199,7 +199,7 @@ class RenderTagTest < Minitest::Test
)
assert_template_result("123",
"{% render 'loop' for loop as value %}", "loop" => TestEnumerable.new)
"{% render 'loop' for loop as value %}", "loop" => TestEnumerable.new)
end
def test_render_tag_with_drop
@@ -208,6 +208,6 @@ class RenderTagTest < Minitest::Test
)
assert_template_result("TestEnumerable",
"{% render 'loop' with loop as value %}", "loop" => TestEnumerable.new)
"{% render 'loop' with loop as value %}", "loop" => TestEnumerable.new)
end
end
-14
View File
@@ -323,18 +323,4 @@ 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
-31
View File
@@ -15,33 +15,6 @@ 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'))
@@ -131,8 +104,4 @@ 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
+10 -48
View File
@@ -72,21 +72,21 @@ module Minitest
end
def with_global_filter(*globals)
original_global_cache = Liquid::StrainerFactory::GlobalCache
Liquid::StrainerFactory.send(:remove_const, :GlobalCache)
Liquid::StrainerFactory.const_set(:GlobalCache, Class.new(Liquid::StrainerTemplate))
original_global_filters = Liquid::StrainerFactory.instance_variable_get(:@global_filters)
Liquid::StrainerFactory.instance_variable_set(:@global_filters, [])
globals.each do |global|
Liquid::StrainerFactory.add_global_filter(global)
end
Liquid::StrainerFactory.send(:strainer_class_cache).clear
globals.each do |global|
Liquid::Template.register_filter(global)
end
yield
ensure
Liquid::StrainerFactory.send(:strainer_class_cache).clear
begin
yield
ensure
Liquid::StrainerFactory.send(:remove_const, :GlobalCache)
Liquid::StrainerFactory.const_set(:GlobalCache, original_global_cache)
Liquid::StrainerFactory.send(:strainer_class_cache).clear
end
Liquid::StrainerFactory.instance_variable_set(:@global_filters, original_global_filters)
end
def with_error_mode(mode)
@@ -119,44 +119,6 @@ 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'
+14 -27
View File
@@ -10,8 +10,8 @@ class ConditionUnitTest < Minitest::Test
end
def test_basic_condition
assert_equal(false, Condition.new(1, '==', 2).evaluate(Context.new))
assert_equal(true, Condition.new(1, '==', 1).evaluate(Context.new))
assert_equal(false, Condition.new(1, '==', 2).evaluate)
assert_equal(true, Condition.new(1, '==', 1).evaluate)
end
def test_default_operators_evalute_true
@@ -67,11 +67,11 @@ class ConditionUnitTest < Minitest::Test
end
def test_hash_compare_backwards_compatibility
assert_nil(Condition.new({}, '>', 2).evaluate(Context.new))
assert_nil(Condition.new(2, '>', {}).evaluate(Context.new))
assert_equal(false, Condition.new({}, '==', 2).evaluate(Context.new))
assert_equal(true, Condition.new({ 'a' => 1 }, '==', 'a' => 1).evaluate(Context.new))
assert_equal(true, Condition.new({ 'a' => 2 }, 'contains', 'a').evaluate(Context.new))
assert_nil(Condition.new({}, '>', 2).evaluate)
assert_nil(Condition.new(2, '>', {}).evaluate)
assert_equal(false, Condition.new({}, '==', 2).evaluate)
assert_equal(true, Condition.new({ 'a' => 1 }, '==', 'a' => 1).evaluate)
assert_equal(true, Condition.new({ 'a' => 2 }, 'contains', 'a').evaluate)
end
def test_contains_works_on_arrays
@@ -106,29 +106,30 @@ class ConditionUnitTest < Minitest::Test
def test_or_condition
condition = Condition.new(1, '==', 2)
assert_equal(false, condition.evaluate(Context.new))
assert_equal(false, condition.evaluate)
condition.or(Condition.new(2, '==', 1))
assert_equal(false, condition.evaluate(Context.new))
assert_equal(false, condition.evaluate)
condition.or(Condition.new(1, '==', 1))
assert_equal(true, condition.evaluate(Context.new))
assert_equal(true, condition.evaluate)
end
def test_and_condition
condition = Condition.new(1, '==', 1)
assert_equal(true, condition.evaluate(Context.new))
assert_equal(true, condition.evaluate)
condition.and(Condition.new(2, '==', 2))
assert_equal(true, condition.evaluate(Context.new))
assert_equal(true, condition.evaluate)
condition.and(Condition.new(2, '==', 1))
assert_equal(false, condition.evaluate(Context.new))
assert_equal(false, condition.evaluate)
end
def test_should_allow_custom_proc_operator
@@ -147,20 +148,6 @@ class ConditionUnitTest < Minitest::Test
assert_evaluates_true(VariableLookup.new("one"), '==', VariableLookup.new("another"))
end
def test_default_context_is_deprecated
if Gem::Version.new(Liquid::VERSION) >= Gem::Version.new('6.0.0')
flunk("Condition#evaluate without a context argument is to be removed")
end
_out, err = capture_io do
assert_equal(true, Condition.new(1, '==', 1).evaluate)
end
expected = "DEPRECATION WARNING: Condition#evaluate without a context argument is deprecated" \
" and will be removed from Liquid 6.0.0."
assert_includes(err.lines.map(&:strip), expected)
end
private
def assert_evaluates_true(left, op, right)
-14
View File
@@ -26,13 +26,6 @@ class ParseTreeVisitorTest < Minitest::Test
)
end
def test_echo
assert_equal(
["test"],
visit(%({% echo test %}))
)
end
def test_if_condition
assert_equal(
["test"],
@@ -159,13 +152,6 @@ class ParseTreeVisitorTest < Minitest::Test
)
end
def test_for_range
assert_equal(
["test"],
visit(%({% for x in (1..test) %}{% endfor %}))
)
end
def test_tablerow_in
assert_equal(
["test"],
+1 -2
View File
@@ -52,8 +52,7 @@ class StrainerFactoryUnitTest < Minitest::Test
/\ALiquid error: wrong number of arguments \((1 for 0|given 1, expected 0)\)\z/,
exception.message
)
source = AccessScopeFilters.instance_method(:public_filter).source_location
assert_equal(source.map(&:to_s), exception.backtrace[0].split(':')[0..1])
assert_equal(exception.backtrace[0].split(':')[0], __FILE__)
end
def test_strainer_only_invokes_public_filter_methods
+1 -1
View File
@@ -57,8 +57,8 @@ class StrainerTemplateUnitTest < Minitest::Test
end
def test_add_filter_does_not_raise_when_module_overrides_previously_registered_method
strainer = Context.new.strainer
with_global_filter do
strainer = Context.new.strainer
strainer.class.add_filter(PublicMethodOverrideFilter)
assert(strainer.class.send(:filter_methods).include?('public_filter'))
end