Compare commits

..
Author SHA1 Message Date
Shaina Raskas 9eeaf89665 remove unnecessary tags 2022-03-16 16:13:02 -04:00
Pierre-Olivier BédardandShaina Raskas a77c37db0a Docs: YARD tags for some resources 2022-03-16 10:57:56 -04:00
Thierry JoyalandGitHub 7357dcf185 Merge pull request #1536 from Shopify/flaky-profiler-test-v2
Add artificial execution time in profiler tests
2022-03-07 10:06:13 -05:00
Thierry JoyalandGitHub 68c3827ef2 Merge pull request #1525 from Shopify/standardfilter/fix-missing-context-on-iterations
[StandardFilter] Fix missing @context on iterations
2022-03-07 09:19:18 -05:00
Thierry Joyal 4af38bc549 [StandardFilter] Fix missing @context on iterations 2022-03-07 09:17:07 -05:00
Thierry Joyal df241abf70 Add artificial execution time in profiler tests 2022-03-07 08:50:03 -05:00
10f8337209 Test Ruby 3.1 in CI (#1533)
Co-authored-by: Dylan Thacker-Smith <[email protected]>
2022-03-04 13:23:04 -05:00
Thierry JoyalandGitHub 5ed0410a8b Merge pull request #1534 from Shopify/context-test-cleanup
Context test cleanup
2022-03-04 11:31:00 -05:00
Thierry Joyal 0f5220c391 ContextTest: Classes to use appropriate ancestor 2022-03-04 09:14:57 -05:00
Thierry Joyal 7a23f46fab ContextTest: Cleanup global variable assignments 2022-03-04 09:10:38 -05:00
Peter ZhuandGitHub 3f7edf00b9 Merge pull request #1531 from Shopify/pz-array-fetch-warning
Fix warning about block and default value
2022-03-02 16:25:23 -05:00
Thierry JoyalandGitHub b4a2a79e26 Merge pull request #1527 from Shopify/condition/receive-mandatory-context-argument
Condition#evaluate to receive mandatory context argument
2022-03-02 15:04:24 -05:00
Thierry Joyal 1d2bee1f60 Condition#evaluate to receive mandatory context argument 2022-03-02 14:35:31 -05:00
Peter Zhu 01e6eec97a Fix warning about block and default value
Ruby's Array#fetch accepts either a default value or a block, but not
both. If both are passed in, then it uses the block and outputs this
warning:

```
lib/liquid/static_registers.rb:34: warning: block supersedes default value argument
```
2022-03-02 14:33:12 -05:00
Jean Boussier fbdab19358 We're in 2022... 2022-03-02 18:27:25 +01:00
Thierry JoyalandGitHub ce85ac5d3d Merge pull request #1529 from Shopify/tests/standard-filters-with-context
StandardFiltersTest: Initialize following production code paths with context
2022-03-02 08:44:16 -05:00
Thierry Joyal c0ffee16a3 StandardFiltersTest: Initialize following production code paths with context 2022-03-01 16:01:00 +00:00
Jean Boussier a7eb33fa39 Release 5.2.0 2022-03-01 16:18:49 +01:00
Jean byroot BoussierandGitHub 1a85e98793 Merge pull request #1524 from Shopify/global-constant-cache
Eagerly cache global filters
2022-03-01 16:14:31 +01:00
Jean Boussier c588337aac Eagerly cache global filters
Including a module can cause Ruby's global constant cache to be busted
if the included module contain constants. So that's something you don't
want to happen at "runtime", otherwise it will severely degrade performance
and if you are using YJIT or MJIT most of the compiled code will be invalidated.

To limit the impact of this, we can pre-include the global filters,
as they're generally registered during boot, that limits the problem
to non-global filters.
2022-03-01 13:40:40 +01:00
Dylan Thacker-SmithandGitHub 97f7922457 Add missing changelog entry for PR #1518 (#1521) 2022-02-24 14:46:26 -05:00
0d83e64cfe Add replace_last and remove_last filters (#1422)
Co-authored-by: ADTC <[email protected]>
Co-authored-by: Dylan Thacker-Smith <[email protected]>
2022-02-24 14:02:15 -05:00
Dylan Thacker-SmithandGitHub 0d5e01ae98 Fix some internal errors in filters from invalid input. (#1476)
These fixes came from improving the corresponding test, so these might not
actually be causing problems in practice.
2022-02-24 09:17:37 -05:00
Charles-Philippe ClermontandGitHub 15eaa49e48 Merge pull request #1518 from Shopify/fix/kwarg-key-name-liquid-c-inconsistency
Fix kwarg parsing inconsistency with Liquid::C
2022-02-14 13:22:12 -05:00
Tobias LütkeandGitHub 91c54c579d Merge pull request #1477 from Watson1978/performance
Increase parsing performance
2022-02-14 12:25:19 -05:00
Charles-P. Clermont 1310c4978d Fix kwarg parsing inconsistency with Liquid::C
Liquid::C parses liquid filter arguments with dashes in them, Liquid does not.

For tags that accept kwargs and dumps them on the HTML tag, this is an important feature.

e.g. {{ ... | image_tag: loading: 'lazy', data-something: 'value!' }}

Without this change, Liquid would incorrectly parse the
`data-something` kwarg as a single argument and would skip over the
invalid characters.

See https://github.com/Shopify/theme-check/issues/539 for more context
2022-02-11 15:10:07 -05:00
shainaraskasandGitHub 3de1db3c3a Merge pull request #1509 from Shopify/1508-shopify-docs-link
Fix Shopify documentation link
2022-01-20 12:06:16 -05:00
Shaina Raskas 03522caaf8 fix Shopify documentation link 2022-01-20 09:40:48 -05:00
Shaina Raskas 7acea2a9c9 Revert "fix Shopify documentation link"
This reverts commit d8ef698539.
2022-01-20 09:37:26 -05:00
Shaina Raskas d8ef698539 fix Shopify documentation link 2022-01-20 09:27:00 -05:00
Watson ebdfdb80e5 Detect quoted string using String#{start_with?, end_with?} to reduce Regexp#=== calling 2021-09-26 04:30:49 +09:00
Watson 95e9fa5010 Use String#=~ and Regexp.last_match instead to retrieve the markup content
If the first value is only used obtained with String#scan,
it will increase the performance if replace with `String#=~` and `Regexp.last_match`.

### Environment
- MacBook Air (M1, 2020)
- macOS 12.0 beta 7
- Apple M1
- Ruby 3.0.2

### Test code
```ruby
require 'benchmark/ips'

WhitespaceControl           = '-'
VariableStart               = /\{\{/
VariableEnd                 = /\}\}/

ContentOfVariable   = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
token = "{{item.product.featured_image | product_img_url: 'thumb' }}"

Benchmark.ips do |x|
  x.report("String#scan")  { token.scan(ContentOfVariable) {|content| break }  }
  x.report("String#match") { m = token.match(ContentOfVariable); m[1] }
  x.report("String#=~")    { token =~ ContentOfVariable; Regexp.last_match(1) }

  x.compare!
end
```

### Result
```
Warming up --------------------------------------
         String#scan   135.724k i/100ms
        String#match   117.397k i/100ms
           String#=~   151.637k i/100ms
Calculating -------------------------------------
         String#scan      1.351M (± 0.8%) i/s -      6.786M in   5.021955s
        String#match      1.169M (± 1.3%) i/s -      5.870M in   5.020429s
           String#=~      1.520M (± 0.9%) i/s -      7.733M in   5.087427s

Comparison:
           String#=~:  1520250.9 i/s
         String#scan:  1351399.0 i/s - 1.12x  (± 0.00) slower
        String#match:  1169384.1 i/s - 1.30x  (± 0.00) slower
```
2021-09-26 04:12:53 +09:00
Anders SøgaardandGitHub db3999a008 Improve where filter tests (#1472) 2021-09-16 10:02:39 -04:00
Marc-André CournoyerandGitHub 10e2aa8d5b Merge pull request #1471 from Shopify/bump-5.1.0
Prep for 5.1.0 release
2021-09-15 15:15:13 -04:00
7c4114671b Remove to_liquid_value fixes in changelog
Co-authored-by: Dylan Thacker-Smith <[email protected]>
2021-09-13 10:53:46 -04:00
Marc-André Cournoyer b01de9d325 Prep for 5.1.0 release 2021-09-09 14:28:45 -04:00
Zac ClayandGitHub a5369c26a8 Add missing quote in comment (#1468) 2021-09-09 14:23:11 -04:00
Charles-Philippe ClermontandGitHub a03de8f9ea Merge pull request #1470 from Shopify/fix/range-parse-tree-visitor
Add ParseTreeVisitor to RangeLookup
2021-09-09 11:39:14 -04:00
Charles-P. Clermont e86fe27259 Fix lint 2021-09-09 11:22:39 -04:00
Charles-P. Clermont c8906d05b9 Add ParseTreeVisitor to RangeLookup 2021-09-09 11:22:39 -04:00
Michael GoandGitHub 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 GoandGitHub 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 ZhuandGitHub 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 GoandGitHub 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
34 changed files with 793 additions and 183 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ jobs:
matrix:
entry:
- { ruby: 2.5, allowed-failure: false } # minimum supported
- { ruby: 3.0, allowed-failure: false } # latest
- { ruby: 3.1, allowed-failure: false } # latest
- { ruby: ruby-head, allowed-failure: true }
name: test (${{ matrix.entry.ruby }})
steps:
+22 -1
View File
@@ -1,12 +1,33 @@
# Liquid Change Log
## 5.0.2 (unreleased)
## 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
### 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
+2 -2
View File
@@ -5,7 +5,7 @@
* [Contributing guidelines](CONTRIBUTING.md)
* [Version history](History.md)
* [Liquid documentation from Shopify](http://docs.shopify.com/themes/liquid-basics)
* [Liquid documentation from Shopify](https://shopify.dev/api/liquid)
* [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+)\s*\:\s*(#{QuotedFragment})/o
TagAttributes = /(\w[\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_factory'
require 'liquid/strainer_template'
require 'liquid/strainer_factory'
require 'liquid/expression'
require 'liquid/context'
require 'liquid/parser_switching'
+14 -2
View File
@@ -35,6 +35,18 @@ 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
@@ -231,8 +243,8 @@ module Liquid
end
def create_variable(token, parse_context)
token.scan(ContentOfVariable) do |content|
markup = content.first
if token =~ ContentOfVariable
markup = Regexp.last_match(1)
return Variable.new(markup, parse_context)
end
BlockBody.raise_missing_variable_terminator(token, parse_context)
+8 -2
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 = Context.new)
def evaluate(context = deprecated_default_context)
condition = self
result = nil
loop do
@@ -150,6 +150,12 @@ 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
+11 -10
View File
@@ -10,21 +10,23 @@ module Liquid
'empty' => ''
}.freeze
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/
INTEGERS_REGEX = /\A(-?\d+)\z/
FLOATS_REGEX = /\A(-?\d[\d\.]+)\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+)\s*\)\s*\z/
RANGES_REGEX = /\A\(\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
@@ -32,7 +34,6 @@ module Liquid
when FLOATS_REGEX
Regexp.last_match(1).to_f
else
markup = markup.strip
if LITERALS.key?(markup)
LITERALS[markup]
else
+8
View File
@@ -12,6 +12,8 @@ module Liquid
end
end
attr_reader :start_obj, :end_obj
def initialize(start_obj, end_obj)
@start_obj = start_obj
@end_obj = end_obj
@@ -35,5 +37,11 @@ module Liquid
Utils.to_integer(input)
end
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[@node.start_obj, @node.end_obj]
end
end
end
end
+357 -47
View File
@@ -28,20 +28,47 @@ 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 centence
# 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
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
@@ -64,20 +91,47 @@ 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
@@ -95,7 +149,34 @@ module Liquid
end
end
# Truncate a string down to x characters
# @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
def truncate(input, length = 50, truncate_string = "...")
return if input.nil?
input_str = input.to_s
@@ -109,6 +190,27 @@ 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
@@ -137,18 +239,46 @@ 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)
@@ -156,18 +286,36 @@ module Liquid
result
end
# Remove all newlines from the string
# @public_docs
# @type filter
# @category string
# @summary
# Strips all line breaks and newlines from a string.
# @syntax {{ string | strip_newlines }}
# @return string
def strip_newlines(input)
input.to_s.gsub(/\r?\n/, '')
end
# Join elements of the array with certain character between them
# @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
def join(input, glue = ' ')
InputIterator.new(input, context).join(glue)
end
# Sort elements of the array
# provide optional property with which to sort an array of hashes or drops
# @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
def sort(input, property = nil)
ary = InputIterator.new(input, context)
@@ -213,17 +361,23 @@ module Liquid
if ary.empty?
[]
elsif ary.first.respond_to?(:[]) && target_value.nil?
begin
ary.select { |item| item[property] }
elsif target_value.nil?
ary.select do |item|
item[property]
rescue TypeError
raise_property_error(property)
rescue NoMethodError
return nil unless item.respond_to?(:[])
raise
end
elsif ary.first.respond_to?(:[])
begin
ary.select { |item| item[property] == target_value }
else
ary.select do |item|
item[property] == target_value
rescue TypeError
raise_property_error(property)
rescue NoMethodError
return nil unless item.respond_to?(:[])
raise
end
end
end
@@ -237,22 +391,36 @@ module Liquid
ary.uniq
elsif ary.empty? # The next two cases assume a non-empty array.
[]
elsif ary.first.respond_to?(:[])
begin
ary.uniq { |a| a[property] }
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
# Reverse the elements of an array
# @public_docs
# @type tag
# @category array
# @summary Reverses the order of the items in an array.
# @syntax {{ array | reverse }}
# @return array
def reverse(input)
ary = InputIterator.new(input, context)
ary.reverse
end
# map/collect on a given property
# @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
def map(input, property)
InputIterator.new(input, context).map do |e|
e = e.call if e.is_a?(Proc)
@@ -277,16 +445,26 @@ module Liquid
ary.compact
elsif ary.empty? # The next two cases assume a non-empty array.
[]
elsif ary.first.respond_to?(:[])
begin
ary.reject { |a| a[property].nil? }
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
# Replace occurrences of a string with another
# @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
def replace(input, string, replacement = '')
input.to_s.gsub(string.to_s, replacement.to_s)
end
@@ -296,21 +474,71 @@ module Liquid
input.to_s.sub(string.to_s, replacement.to_s)
end
# remove a substring
def remove(input, string)
input.to_s.gsub(string.to_s, '')
# 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
# remove the first occurrences of a substring
# @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
def remove(input, string)
replace(input, string, '')
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
def remove_first(input, string)
input.to_s.sub(string.to_s, '')
replace_first(input, string, '')
end
# remove the last occurences of a substring
def remove_last(input, string)
replace_last(input, string, '')
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"
@@ -318,7 +546,13 @@ module Liquid
InputIterator.new(input, context).concat(array)
end
# prepend a string to another
# @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
def prepend(input, string)
string.to_s + input.to_s
end
@@ -367,58 +601,101 @@ module Liquid
date.strftime(format.to_s)
end
# Get the first element of the passed in array
#
# Example:
# {{ product.images | first | to_img }}
#
# @public_docs
# @type filter
# @category array
# @summary Returns the first element of an array.
# @syntax {{ array | first }}
def first(array)
array.first if array.respond_to?(:first)
end
# Get the last element of the passed in array
#
# Example:
# {{ product.images | last | to_img }}
#
# @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 }}
def last(array)
array.last if array.respond_to?(:last)
end
# absolute value
# @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.
def abs(input)
result = Utils.to_number(input).abs
result.is_a?(BigDecimal) ? result.to_f : result
end
# addition
# @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
def plus(input, operand)
apply_operation(input, operand, :+)
end
# subtraction
# @public_docs
# @summary Subtracts a number from an output.
# @type filter
# @category math
# @syntax {{ number | minus: number }}
# @return number
def minus(input, operand)
apply_operation(input, operand, :-)
end
# multiplication
# @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
def times(input, operand)
apply_operation(input, operand, :*)
end
# division
# @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
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)
@@ -428,18 +705,37 @@ 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)
@@ -448,6 +744,13 @@ 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)
@@ -468,7 +771,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
@@ -486,10 +789,16 @@ module Liquid
end
def nil_safe_compare(a, b)
if !a.nil? && !b.nil?
a <=> b
result = a <=> b
if result
result
elsif a.nil?
1
elsif b.nil?
-1
else
a.nil? ? 1 : -1
raise Liquid::ArgumentError, "cannot sort values of incompatible types"
end
end
@@ -544,8 +853,9 @@ 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.respond_to?(:to_liquid) ? e.to_liquid : e)
yield(e)
end
end
end
+5 -1
View File
@@ -31,7 +31,11 @@ module Liquid
if @registers.key?(key)
@registers.fetch(key)
elsif default != UNDEFINED
@static.fetch(key, default, &block)
if block_given?
@static.fetch(key, &block)
else
@static.fetch(key, default)
end
else
@static.fetch(key, &block)
end
+11 -10
View File
@@ -7,25 +7,26 @@ module Liquid
def add_global_filter(filter)
strainer_class_cache.clear
global_filters << filter
GlobalCache.add_filter(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)
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
if filters.empty?
GlobalCache
else
strainer_class_cache[filters] ||= begin
klass = Class.new(GlobalCache)
filters.each { |f| klass.add_filter(f) }
klass
end
end
end
+5
View File
@@ -31,6 +31,11 @@ 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
+21
View File
@@ -1,6 +1,27 @@
# 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
+12 -1
View File
@@ -1,6 +1,17 @@
# 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,6 +13,19 @@ 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 -10
View File
@@ -1,16 +1,16 @@
# frozen_string_literal: true
module Liquid
# 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 %}
#
# @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' %}
class Echo < Tag
attr_reader :variable
+14
View File
@@ -45,6 +45,20 @@ 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 -7
View File
@@ -3,14 +3,15 @@
module Liquid
# If is the conditional block
#
# {% if user.admin %}
# Admin user!
# {% else %}
# Not admin user
# @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
# {% 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
+10
View File
@@ -1,6 +1,16 @@
# 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,6 +1,28 @@
# 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,6 +1,23 @@
# 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
+12 -6
View File
@@ -3,10 +3,15 @@
require_relative 'if'
module Liquid
# Unless is a conditional just like 'if' but works on the inverse logic.
#
# {% unless x < 0 %} x is greater than zero {% endunless %}
#
# @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 %}
class Unless < If
def render_to_output_buffer(context, output)
# First condition is interpreted backwards ( if not )
@@ -21,8 +26,9 @@ module Liquid
# After the first condition unless works just like if
@blocks[1..-1].each do |block|
result = block.evaluate(context)
result = result.to_liquid_value if result.is_a?(Liquid::Drop)
result = Liquid::Utils.to_liquid_value(
block.evaluate(context)
)
if result
return block.attachment.render_to_output_buffer(context, output)
+1 -1
View File
@@ -2,5 +2,5 @@
# frozen_string_literal: true
module Liquid
VERSION = "5.0.2.alpha"
VERSION = "5.3.0.alpha"
end
+9 -1
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|
+12 -14
View File
@@ -24,7 +24,7 @@ class ContextSensitiveDrop < Liquid::Drop
end
end
class Category < Liquid::Drop
class Category
attr_accessor :name
def initialize(name)
@@ -36,8 +36,9 @@ class Category < Liquid::Drop
end
end
class CategoryDrop
class CategoryDrop < Liquid::Drop
attr_accessor :category, :context
def initialize(category)
@category = category
end
@@ -405,45 +406,42 @@ class ContextTest < Minitest::Test
end
def test_lambda_is_called_once
@global = 0
@context['callcount'] = proc {
@global ||= 0
@global += 1
@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 ||= 0
@global += 1
@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 ||= 0
@global += 1
@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
@@ -0,0 +1,24 @@
# 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
+33 -6
View File
@@ -3,6 +3,27 @@
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
@@ -198,16 +219,22 @@ class ProfilerTest < Minitest::Test
def test_profiling_supports_self_time
t = Template.parse("{% for item in collection %} {{ item }} {% endfor %}", profile: true)
t.render!("collection" => ["one", "two"])
leaf = t.profiler[0].children[0]
collection = [
TestDrop.new("one"),
TestDrop.new("two"),
]
output = t.render!("collection" => collection)
assert_equal(" one two ", output)
assert_operator(leaf.self_time, :>, 0)
leaf = t.profiler[0].children[0]
assert_operator(leaf.self_time, :>, 0.0)
end
def test_profiling_supports_total_time
t = Template.parse("{% if true %} {% increment test %} {{ test }} {% endif %}", profile: true)
t.render!
t = Template.parse("{% if true %} {{ test }} {% endif %}", profile: true)
output = t.render!("test" => TestDrop.new("one"))
assert_equal(" one ", output)
assert_operator(t.profiler[0].total_time, :>, 0)
assert_operator(t.profiler[0].total_time, :>, 0.0)
end
end
+81 -32
View File
@@ -3,10 +3,6 @@
require 'test_helper'
class Filters
include Liquid::StandardFilters
end
class TestThing
attr_reader :foo
@@ -29,8 +25,24 @@ class TestThing
end
class TestDrop < Liquid::Drop
def test
"testfoo"
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)
end
end
@@ -53,10 +65,13 @@ 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
@filters = Filters.new(Context.new)
end
def test_size
@@ -259,8 +274,8 @@ class StandardFiltersTest < Minitest::Test
{ "price" => 1, "handle" => "gamma" },
{ "price" => 2, "handle" => "epsilon" },
{ "price" => 4, "handle" => "alpha" },
{ "handle" => "delta" },
{ "handle" => "beta" },
{ "handle" => "delta" },
]
assert_equal(expectation, @filters.sort(input, "price"))
end
@@ -363,8 +378,9 @@ 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"))
testdrop = TestDrop.new
assert_equal([testdrop], @filters.uniq([testdrop, TestDrop.new], 'test'))
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'))
end
def test_uniq_empty_array
@@ -423,6 +439,16 @@ 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 }] })
@@ -441,9 +467,9 @@ class StandardFiltersTest < Minitest::Test
end
def test_map_over_proc
drop = TestDrop.new
drop = TestDrop.new(value: "testfoo")
p = proc { drop }
templ = '{{ procs | map: "test" }}'
templ = '{{ procs | map: "value" }}'
assert_template_result("testfoo", templ, "procs" => [p])
end
@@ -539,19 +565,31 @@ class StandardFiltersTest < Minitest::Test
end
def test_replace
assert_equal('2 2 2 2', @filters.replace('1 1 1 1', '1', 2))
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 1 1 1', @filters.replace_first('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('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_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 ' }}")
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 }}")
end
def test_pipes_in_string_arguments
@@ -728,6 +766,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
@@ -738,6 +778,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
@@ -766,6 +808,18 @@ 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 },
@@ -811,7 +865,7 @@ class StandardFiltersTest < Minitest::Test
end
def test_all_filters_never_raise_non_liquid_exception
test_drop = TestDrop.new
test_drop = TestDrop.new(value: "test")
test_drop.context = Context.new
test_enum = TestEnumerable.new
test_enum.context = Context.new
@@ -836,19 +890,14 @@ class StandardFiltersTest < Minitest::Test
{ 1 => "bar" },
["foo", 123, nil, true, false, Drop, ["foo"], { foo: "bar" }],
]
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
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
end
end
end
+10 -10
View File
@@ -72,21 +72,21 @@ module Minitest
end
def with_global_filter(*globals)
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
original_global_cache = Liquid::StrainerFactory::GlobalCache
Liquid::StrainerFactory.send(:remove_const, :GlobalCache)
Liquid::StrainerFactory.const_set(:GlobalCache, Class.new(Liquid::StrainerTemplate))
globals.each do |global|
Liquid::Template.register_filter(global)
end
yield
ensure
Liquid::StrainerFactory.send(:strainer_class_cache).clear
Liquid::StrainerFactory.instance_variable_set(:@global_filters, original_global_filters)
begin
yield
ensure
Liquid::StrainerFactory.send(:remove_const, :GlobalCache)
Liquid::StrainerFactory.const_set(:GlobalCache, original_global_cache)
Liquid::StrainerFactory.send(:strainer_class_cache).clear
end
end
def with_error_mode(mode)
+27 -14
View File
@@ -10,8 +10,8 @@ class ConditionUnitTest < Minitest::Test
end
def test_basic_condition
assert_equal(false, Condition.new(1, '==', 2).evaluate)
assert_equal(true, Condition.new(1, '==', 1).evaluate)
assert_equal(false, Condition.new(1, '==', 2).evaluate(Context.new))
assert_equal(true, Condition.new(1, '==', 1).evaluate(Context.new))
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)
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)
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))
end
def test_contains_works_on_arrays
@@ -106,30 +106,29 @@ class ConditionUnitTest < Minitest::Test
def test_or_condition
condition = Condition.new(1, '==', 2)
assert_equal(false, condition.evaluate)
assert_equal(false, condition.evaluate(Context.new))
condition.or(Condition.new(2, '==', 1))
assert_equal(false, condition.evaluate)
assert_equal(false, condition.evaluate(Context.new))
condition.or(Condition.new(1, '==', 1))
assert_equal(true, condition.evaluate)
assert_equal(true, condition.evaluate(Context.new))
end
def test_and_condition
condition = Condition.new(1, '==', 1)
assert_equal(true, condition.evaluate)
assert_equal(true, condition.evaluate(Context.new))
condition.and(Condition.new(2, '==', 2))
assert_equal(true, condition.evaluate)
assert_equal(true, condition.evaluate(Context.new))
condition.and(Condition.new(2, '==', 1))
assert_equal(false, condition.evaluate)
assert_equal(false, condition.evaluate(Context.new))
end
def test_should_allow_custom_proc_operator
@@ -148,6 +147,20 @@ 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)
+7
View File
@@ -159,6 +159,13 @@ 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"],
+2 -1
View File
@@ -52,7 +52,8 @@ class StrainerFactoryUnitTest < Minitest::Test
/\ALiquid error: wrong number of arguments \((1 for 0|given 1, expected 0)\)\z/,
exception.message
)
assert_equal(exception.backtrace[0].split(':')[0], __FILE__)
source = AccessScopeFilters.instance_method(:public_filter).source_location
assert_equal(source.map(&:to_s), exception.backtrace[0].split(':')[0..1])
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