Compare commits

..
100 changed files with 2485 additions and 3373 deletions
+2
View File
@@ -0,0 +1,2 @@
enabled:
- cla
-22
View File
@@ -1,22 +0,0 @@
name: Contributor License Agreement (CLA)
on:
pull_request_target:
types: [opened, synchronize]
issue_comment:
types: [created]
jobs:
cla:
runs-on: ubuntu-latest
if: |
(github.event.issue.pull_request
&& !github.event.issue.pull_request.merged_at
&& contains(github.event.comment.body, 'signed')
)
|| (github.event.pull_request && !github.event.pull_request.merged)
steps:
- uses: Shopify/shopify-cla-action@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
cla-token: ${{ secrets.CLA_TOKEN }}
+17 -13
View File
@@ -1,35 +1,39 @@
name: Liquid name: Liquid
on: [push, pull_request] on: [push, pull_request]
env:
BUNDLE_JOBS: 4
BUNDLE_RETRY: 3
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
entry: entry:
- { ruby: 2.7, allowed-failure: false } # minimum supported - { ruby: 2.5, allowed-failure: false } # minimum supported
- { ruby: 3.2, allowed-failure: false } # latest - { ruby: 3.0, allowed-failure: false } # latest
- { ruby: ruby-head, allowed-failure: true } - { ruby: ruby-head, allowed-failure: true }
name: Test Ruby ${{ matrix.entry.ruby }} name: test (${{ matrix.entry.ruby }})
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v2
- uses: ruby/setup-ruby@v1 - uses: ruby/setup-ruby@v1
with: with:
ruby-version: ${{ matrix.entry.ruby }} ruby-version: ${{ matrix.entry.ruby }}
bundler-cache: true - uses: actions/cache@v1
with:
path: vendor/bundle
key: ${{ runner.os }}-gems-${{ hashFiles('Gemfile') }}
restore-keys: ${{ runner.os }}-gems-
- run: bundle install --jobs=3 --retry=3 --path=vendor/bundle
- run: bundle exec rake - run: bundle exec rake
continue-on-error: ${{ matrix.entry.allowed-failure }} continue-on-error: ${{ matrix.entry.allowed-failure }}
memory_profile: memory_profile:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v2
- uses: ruby/setup-ruby@v1 - uses: ruby/setup-ruby@v1
with: with:
ruby-version: 2.7 ruby-version: 2.7
bundler-cache: true - uses: actions/cache@v1
with:
path: vendor/bundle
key: ${{ runner.os }}-gems-${{ hashFiles('Gemfile') }}
restore-keys: ${{ runner.os }}-gems-
- run: bundle install --jobs=3 --retry=3 --path=vendor/bundle
- run: bundle exec rake memory_profile:run - run: bundle exec rake memory_profile:run
File diff suppressed because it is too large Load Diff
+2 -13
View File
@@ -1,7 +1,5 @@
inherit_gem:
rubocop-shopify: rubocop.yml
inherit_from: inherit_from:
- 'https://shopify.github.io/ruby-style-guide/rubocop.yml'
- .rubocop_todo.yml - .rubocop_todo.yml
require: rubocop-performance require: rubocop-performance
@@ -10,20 +8,11 @@ Performance:
Enabled: true Enabled: true
AllCops: AllCops:
TargetRubyVersion: 2.7 TargetRubyVersion: 2.4
NewCops: disable NewCops: disable
SuggestExtensions: false
Exclude: Exclude:
- 'vendor/bundle/**/*' - 'vendor/bundle/**/*'
Naming/MethodName: Naming/MethodName:
Exclude: Exclude:
- 'example/server/liquid_servlet.rb' - 'example/server/liquid_servlet.rb'
Style/ClassMethodsDefinitions:
Enabled: false
# liquid filter calls were being mistaken to be calls on arrays
Style/ConcatArrayLiterals:
Exclude:
- 'test/integration/standard_filter_test.rb'
+14 -137
View File
@@ -1,74 +1,27 @@
# This configuration was generated by # This configuration was generated by
# `rubocop --auto-gen-config` # `rubocop --auto-gen-config`
# on 2022-05-18 19:25:47 UTC using RuboCop version 1.29.1. # on 2020-12-11 18:53:41 UTC using RuboCop version 1.6.1.
# The point is for the user to remove these configuration records # The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base. # one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new # Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again. # versions of RuboCop, may require this file to be generated again.
# Offense count: 1 # Offense count: 2
# This cop supports safe auto-correction (--auto-correct). # Cop supports --auto-correct.
# Configuration parameters: TreatCommentsAsGroupSeparators, ConsiderPunctuation, Include. # Configuration parameters: EnforcedStyle.
# Include: **/*.gemspec # SupportedStyles: runtime_error, standard_error
Gemspec/OrderedDependencies: Lint/InheritException:
Exclude: Exclude:
- 'liquid.gemspec' - 'lib/liquid/interrupts.rb'
# Offense count: 6 # Offense count: 113
# This cop supports safe auto-correction (--auto-correct). # Cop supports --auto-correct.
Layout/ClosingHeredocIndentation: # Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
Exclude: # URISchemes: http, https
- 'test/integration/tags/for_tag_test.rb' Layout/LineLength:
Max: 260
# Offense count: 34
# This cop supports safe auto-correction (--auto-correct).
Layout/EmptyLineAfterGuardClause:
Exclude:
- 'lib/liquid/block.rb'
- 'lib/liquid/block_body.rb'
- 'lib/liquid/context.rb'
- 'lib/liquid/drop.rb'
- 'lib/liquid/lexer.rb'
- 'lib/liquid/parser.rb'
- 'lib/liquid/profiler/hooks.rb'
- 'lib/liquid/standardfilters.rb'
- 'lib/liquid/tags/for.rb'
- 'lib/liquid/tags/if.rb'
- 'lib/liquid/utils.rb'
- 'lib/liquid/variable.rb'
- 'lib/liquid/variable_lookup.rb'
- 'performance/shopify/money_filter.rb'
- 'performance/shopify/paginate.rb'
# Offense count: 8 # Offense count: 8
# This cop supports safe auto-correction (--auto-correct).
# Configuration parameters: AllowAliasSyntax, AllowedMethods.
# AllowedMethods: alias_method, public, protected, private
Layout/EmptyLinesAroundAttributeAccessor:
Exclude:
- 'lib/liquid/template.rb'
- 'test/integration/filter_test.rb'
- 'test/integration/tags/include_tag_test.rb'
- 'test/unit/strainer_template_unit_test.rb'
# Offense count: 17
# This cop supports safe auto-correction (--auto-correct).
# Configuration parameters: EnforcedStyle, IndentationWidth.
# SupportedStyles: aligned, indented
Layout/LineEndStringConcatenationIndentation:
Exclude:
- 'test/integration/tags/for_tag_test.rb'
- 'test/integration/tags/increment_tag_test.rb'
# Offense count: 1
# This cop supports safe auto-correction (--auto-correct).
# Configuration parameters: EnforcedStyle, IndentationWidth.
# SupportedStyles: aligned, indented
Layout/MultilineOperationIndentation:
Exclude:
- 'lib/liquid/expression.rb'
# Offense count: 9
Lint/MissingSuper: Lint/MissingSuper:
Exclude: Exclude:
- 'lib/liquid/forloop_drop.rb' - 'lib/liquid/forloop_drop.rb'
@@ -80,7 +33,7 @@ Lint/MissingSuper:
- 'test/integration/tags/for_tag_test.rb' - 'test/integration/tags/for_tag_test.rb'
- 'test/integration/tags/table_row_test.rb' - 'test/integration/tags/table_row_test.rb'
# Offense count: 44 # Offense count: 43
Naming/ConstantName: Naming/ConstantName:
Exclude: Exclude:
- 'lib/liquid.rb' - 'lib/liquid.rb'
@@ -98,83 +51,7 @@ Naming/ConstantName:
- 'performance/shopify/paginate.rb' - 'performance/shopify/paginate.rb'
- 'test/integration/tags/include_tag_test.rb' - 'test/integration/tags/include_tag_test.rb'
# Offense count: 9
# Configuration parameters: CheckIdentifiers, CheckConstants, CheckVariables, CheckStrings, CheckSymbols, CheckComments, CheckFilepaths, FlaggedTerms.
Naming/InclusiveLanguage:
Exclude:
- 'lib/liquid/drop.rb'
- 'lib/liquid/parse_context.rb'
- 'test/integration/drop_test.rb'
- 'test/integration/tags/if_else_tag_test.rb'
# Offense count: 2 # Offense count: 2
Style/ClassVars: Style/ClassVars:
Exclude: Exclude:
- 'lib/liquid/condition.rb' - 'lib/liquid/condition.rb'
# Offense count: 3
# This cop supports safe auto-correction (--auto-correct).
Style/ExplicitBlockArgument:
Exclude:
- 'test/integration/context_test.rb'
- 'test/integration/tag/disableable_test.rb'
- 'test/integration/tags/for_tag_test.rb'
# Offense count: 2982
# This cop supports safe auto-correction (--auto-correct).
# Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline.
# SupportedStyles: single_quotes, double_quotes
Style/StringLiterals:
Enabled: false
# Offense count: 20
# This cop supports safe auto-correction (--auto-correct).
# Configuration parameters: EnforcedStyle.
# SupportedStyles: single_quotes, double_quotes
Style/StringLiteralsInInterpolation:
Exclude:
- 'lib/liquid/condition.rb'
- 'lib/liquid/strainer_template.rb'
- 'lib/liquid/tag/disableable.rb'
- 'performance/shopify/shop_filter.rb'
- 'performance/shopify/tag_filter.rb'
# Offense count: 6
# This cop supports safe auto-correction (--auto-correct).
# Configuration parameters: EnforcedStyleForMultiline.
# SupportedStylesForMultiline: comma, consistent_comma, no_comma
Style/TrailingCommaInArrayLiteral:
Exclude:
- 'example/server/example_servlet.rb'
- 'lib/liquid/condition.rb'
- 'test/integration/context_test.rb'
- 'test/integration/standard_filter_test.rb'
- 'test/unit/parse_tree_visitor_test.rb'
# Offense count: 1
# This cop supports safe auto-correction (--auto-correct).
# Configuration parameters: EnforcedStyleForMultiline.
# SupportedStylesForMultiline: comma, consistent_comma, no_comma
Style/TrailingCommaInHashLiteral:
Exclude:
- 'lib/liquid/expression.rb'
# Offense count: 19
# This cop supports safe auto-correction (--auto-correct).
# Configuration parameters: EnforcedStyle, MinSize, WordRegex.
# SupportedStyles: percent, brackets
Style/WordArray:
Exclude:
- 'lib/liquid/tags/if.rb'
- 'liquid.gemspec'
- 'test/integration/assign_test.rb'
- 'test/integration/context_test.rb'
- 'test/integration/drop_test.rb'
- 'test/integration/standard_filter_test.rb'
# Offense count: 117
# This cop supports safe auto-correction (--auto-correct).
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns, IgnoredPatterns.
# URISchemes: http, https
Layout/LineLength:
Max: 260
+1 -4
View File
@@ -5,7 +5,6 @@
* Bugfixes * Bugfixes
* Performance improvements * Performance improvements
* Features that are likely to be useful to the majority of Liquid users * 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 ## 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) * 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 does not include tests
* Code that breaks existing 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 ## Workflow
* [Sign the CLA](https://cla.shopify.com/) if you haven't already
* Fork the Liquid repository * Fork the Liquid repository
* Create a new branch in your fork * 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 * If it makes sense, add tests for your code and/or run a performance benchmark
* Make sure all tests pass (`bundle exec rake`) * Make sure all tests pass (`bundle exec rake`)
* Create a pull request * Create a pull request
+2 -3
View File
@@ -18,11 +18,10 @@ group :benchmark, :test do
end end
group :test do group :test do
gem 'rubocop', '~> 1.44.0' gem 'rubocop', '~> 1.4', require: false
gem 'rubocop-shopify', '~> 2.12.0', require: false
gem 'rubocop-performance', require: false gem 'rubocop-performance', require: false
platform :mri, :truffleruby do 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
end end
+2 -53
View File
@@ -1,60 +1,9 @@
# Liquid Change Log # Liquid Change Log
## 5.4.0 2022-07-29 ## Unreleased
### Breaking Changes
* Drop support for end-of-life Ruby versions (2.5 and 2.6) (#1578) [Andy Waite]
### Features ### Features
* Allow `#` to be used as an inline comment tag (#1498) [CP Clermont] * Allow `#` to be used as an inline comment tag (#1401) [Dylan Thacker-Smith]
### Fixes
* `PartialCache` now shares snippet cache with subcontexts by default (#1553) [Chris AtLee]
* Hash registers no longer leak into subcontexts as static registers (#1564) [Chris AtLee]
* Fix `ParseTreeVisitor` for `with` variable expressions in `Render` tag (#1596) [CP Clermont]
### Changed
* Liquid::Context#registers now always returns a Liquid::Registers object, though supports the most used Hash functions for compatibility (#1553)
## 5.3.0 2022-03-22
### Fixes
* StandardFilter: Fix missing @context on iterations (#1525) [Thierry Joyal]
* Fix warning about block and default value in `static_registers.rb` (#1531) [Peter Zhu]
### 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
### 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]
## 5.0.0 / 2021-01-06 ## 5.0.0 / 2021-01-06
+4 -4
View File
@@ -5,7 +5,7 @@
* [Contributing guidelines](CONTRIBUTING.md) * [Contributing guidelines](CONTRIBUTING.md)
* [Version history](History.md) * [Version history](History.md)
* [Liquid documentation from Shopify](https://shopify.dev/docs/docs/api/liquid) * [Liquid documentation from Shopify](http://docs.shopify.com/themes/liquid-basics)
* [Liquid Wiki at GitHub](https://github.com/Shopify/liquid/wiki) * [Liquid Wiki at GitHub](https://github.com/Shopify/liquid/wiki)
* [Website](http://liquidmarkup.org/) * [Website](http://liquidmarkup.org/)
@@ -56,20 +56,20 @@ 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. 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 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 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: when templates are invalid. You can enable this new parser like this:
```ruby ```ruby
Liquid::Template.error_mode = :strict # Raises a SyntaxError when invalid syntax is used Liquid::Template.error_mode = :strict # Raises a SyntaxError when invalid syntax is used
Liquid::Template.error_mode = :warn # Adds strict errors to template.errors but continues as normal Liquid::Template.error_mode = :warn # Adds errors to template.errors but continues as normal
Liquid::Template.error_mode = :lax # The default mode, accepts almost anything. Liquid::Template.error_mode = :lax # The default mode, accepts almost anything.
``` ```
If you want to set the error mode only on specific templates you can pass `:error_mode` as an option to `parse`: If you want to set the error mode only on specific templates you can pass `:error_mode` as an option to `parse`:
```ruby ```ruby
Liquid::Template.parse(source, error_mode: :strict) Liquid::Template.parse(source, :error_mode => :strict)
``` ```
This is useful for doing things like enabling strict mode only in the theme editor. This is useful for doing things like enabling strict mode only in the theme editor.
+5 -9
View File
@@ -30,18 +30,14 @@ class Servlet < LiquidServlet
private private
def products_list def products_list
[ [{ 'name' => 'Arbor Draft', 'price' => 39900, 'description' => 'the *arbor draft* is a excellent product' },
{ 'name' => 'Arbor Draft', 'price' => 39900, 'description' => 'the *arbor draft* is a excellent product' }, { 'name' => 'Arbor Element', 'price' => 40000, 'description' => 'the *arbor element* rocks for freestyling' },
{ 'name' => 'Arbor Element', 'price' => 40000, 'description' => 'the *arbor element* rocks for freestyling' }, { 'name' => 'Arbor Diamond', 'price' => 59900, 'description' => 'the *arbor diamond* is a made up product because im obsessed with arbor and have no creativity' }]
{ 'name' => 'Arbor Diamond', 'price' => 59900, 'description' => 'the *arbor diamond* is a made up product because im obsessed with arbor and have no creativity' }
]
end end
def more_products_list def more_products_list
[ [{ 'name' => 'Arbor Catalyst', 'price' => 39900, 'description' => 'the *arbor catalyst* is an advanced drop-through for freestyle and flatground performance and versatility' },
{ 'name' => 'Arbor Catalyst', 'price' => 39900, 'description' => 'the *arbor catalyst* is an advanced drop-through for freestyle and flatground performance and versatility' }, { 'name' => 'Arbor Fish', 'price' => 40000, 'description' => 'the *arbor fish* is a compact pin that features an extended wheelbase and time-honored teardrop shape' }]
{ 'name' => 'Arbor Fish', 'price' => 40000, 'description' => 'the *arbor fish* is a compact pin that features an extended wheelbase and time-honored teardrop shape' }
]
end end
def description def description
+5 -5
View File
@@ -29,7 +29,6 @@ module Liquid
WhitespaceControl = '-' WhitespaceControl = '-'
TagStart = /\{\%/ TagStart = /\{\%/
TagEnd = /\%\}/ TagEnd = /\%\}/
TagName = /#|\w+/
VariableSignature = /\(?[\w\-\.\[\]]\)?/ VariableSignature = /\(?[\w\-\.\[\]]\)?/
VariableSegment = /[\w\-]/ VariableSegment = /[\w\-]/
VariableStart = /\{\{/ VariableStart = /\{\{/
@@ -37,11 +36,11 @@ module Liquid
VariableIncompleteEnd = /\}\}?/ VariableIncompleteEnd = /\}\}?/
QuotedString = /"[^"]*"|'[^']*'/ QuotedString = /"[^"]*"|'[^']*'/
QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o
TagAttributes = /(\w[\w-]*)\s*\:\s*(#{QuotedFragment})/o TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/o
AnyStartingTag = /#{TagStart}|#{VariableStart}/o AnyStartingTag = /#{TagStart}|#{VariableStart}/o
PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om
VariableParser = /\[(?>[^\[\]]+|\g<0>)*\]|#{VariableSegment}+\??/o VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o
RAISE_EXCEPTION_LAMBDA = ->(_e) { raise } RAISE_EXCEPTION_LAMBDA = ->(_e) { raise }
@@ -60,8 +59,8 @@ require 'liquid/forloop_drop'
require 'liquid/extensions' require 'liquid/extensions'
require 'liquid/errors' require 'liquid/errors'
require 'liquid/interrupts' require 'liquid/interrupts'
require 'liquid/strainer_template'
require 'liquid/strainer_factory' require 'liquid/strainer_factory'
require 'liquid/strainer_template'
require 'liquid/expression' require 'liquid/expression'
require 'liquid/context' require 'liquid/context'
require 'liquid/parser_switching' require 'liquid/parser_switching'
@@ -84,7 +83,8 @@ require 'liquid/tokenizer'
require 'liquid/parse_context' require 'liquid/parse_context'
require 'liquid/partial_cache' require 'liquid/partial_cache'
require 'liquid/usage' require 'liquid/usage'
require 'liquid/registers' require 'liquid/register'
require 'liquid/static_registers'
require 'liquid/template_factory' require 'liquid/template_factory'
# Load all the tags of the standard library # Load all the tags of the standard library
+4 -8
View File
@@ -36,17 +36,13 @@ module Liquid
# @api private # @api private
def self.raise_unknown_tag(tag, block_name, block_delimiter, parse_context) def self.raise_unknown_tag(tag, block_name, block_delimiter, parse_context)
if tag == 'else' if tag == 'else'
raise SyntaxError, parse_context.locale.t( raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_else",
"errors.syntax.unexpected_else", block_name: block_name)
block_name: block_name,
)
elsif tag.start_with?('end') elsif tag.start_with?('end')
raise SyntaxError, parse_context.locale.t( raise SyntaxError, parse_context.locale.t("errors.syntax.invalid_delimiter",
"errors.syntax.invalid_delimiter",
tag: tag, tag: tag,
block_name: block_name, block_name: block_name,
block_delimiter: block_delimiter, block_delimiter: block_delimiter)
)
else else
raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag", tag: tag) raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag", tag: tag)
end end
+8 -16
View File
@@ -4,8 +4,8 @@ require 'English'
module Liquid module Liquid
class BlockBody class BlockBody
LiquidTagToken = /\A\s*(#{TagName})\s*(.*?)\z/o LiquidTagToken = /\A\s*(\w+|#)\s*(.*?)\z/o
FullToken = /\A#{TagStart}#{WhitespaceControl}?(\s*)(#{TagName})(\s*)(.*?)#{WhitespaceControl}?#{TagEnd}\z/om FullToken = /\A#{TagStart}#{WhitespaceControl}?(\s*)(\w+|#)(\s*)(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
WhitespaceOrNothing = /\A\s*\z/ WhitespaceOrNothing = /\A\s*\z/
TAGSTART = "{%" TAGSTART = "{%"
@@ -37,7 +37,7 @@ module Liquid
private def parse_for_liquid_tag(tokenizer, parse_context) private def parse_for_liquid_tag(tokenizer, parse_context)
while (token = tokenizer.shift) while (token = tokenizer.shift)
unless token.empty? || token.match?(WhitespaceOrNothing) unless token.empty? || token =~ WhitespaceOrNothing
unless token =~ LiquidTagToken unless token =~ LiquidTagToken
# line isn't empty but didn't match tag syntax, yield and let the # line isn't empty but didn't match tag syntax, yield and let the
# caller raise a syntax error # caller raise a syntax error
@@ -109,22 +109,14 @@ module Liquid
end end
end end
private def handle_invalid_tag_token(token, parse_context) private def parse_for_document(tokenizer, parse_context)
if token.end_with?('%}')
yield token, token
else
BlockBody.raise_missing_tag_terminator(token, parse_context)
end
end
private def parse_for_document(tokenizer, parse_context, &block)
while (token = tokenizer.shift) while (token = tokenizer.shift)
next if token.empty? next if token.empty?
case case
when token.start_with?(TAGSTART) when token.start_with?(TAGSTART)
whitespace_handler(token, parse_context) whitespace_handler(token, parse_context)
unless token =~ FullToken unless token =~ FullToken
return handle_invalid_tag_token(token, parse_context, &block) BlockBody.raise_missing_tag_terminator(token, parse_context)
end end
tag_name = Regexp.last_match(2) tag_name = Regexp.last_match(2)
markup = Regexp.last_match(4) markup = Regexp.last_match(4)
@@ -158,7 +150,7 @@ module Liquid
end end
parse_context.trim_whitespace = false parse_context.trim_whitespace = false
@nodelist << token @nodelist << token
@blank &&= token.match?(WhitespaceOrNothing) @blank &&= !!(token =~ WhitespaceOrNothing)
end end
parse_context.line_number = tokenizer.line_number parse_context.line_number = tokenizer.line_number
end end
@@ -239,8 +231,8 @@ module Liquid
end end
def create_variable(token, parse_context) def create_variable(token, parse_context)
if token =~ ContentOfVariable token.scan(ContentOfVariable) do |content|
markup = Regexp.last_match(1) markup = content.first
return Variable.new(markup, parse_context) return Variable.new(markup, parse_context)
end end
BlockBody.raise_missing_variable_terminator(token, parse_context) BlockBody.raise_missing_variable_terminator(token, parse_context)
+6 -14
View File
@@ -8,7 +8,7 @@ module Liquid
# c = Condition.new(1, '==', 1) # c = Condition.new(1, '==', 1)
# c.evaluate #=> true # c.evaluate #=> true
# #
class Condition # :nodoc: class Condition #:nodoc:
@@operators = { @@operators = {
'==' => ->(cond, left, right) { cond.send(:equal_variables, left, right) }, '==' => ->(cond, left, right) { cond.send(:equal_variables, left, right) },
'!=' => ->(cond, left, right) { !cond.send(:equal_variables, left, right) }, '!=' => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
@@ -61,7 +61,7 @@ module Liquid
@child_condition = nil @child_condition = nil
end end
def evaluate(context = deprecated_default_context) def evaluate(context = Context.new)
condition = self condition = self
result = nil result = nil
loop do loop do
@@ -134,8 +134,8 @@ module Liquid
# return this as the result. # return this as the result.
return context.evaluate(left) if op.nil? return context.evaluate(left) if op.nil?
left = Liquid::Utils.to_liquid_value(context.evaluate(left)) left = context.evaluate(left)
right = Liquid::Utils.to_liquid_value(context.evaluate(right)) right = context.evaluate(right)
operation = self.class.operators[op] || raise(Liquid::ArgumentError, "Unknown operator #{op}") operation = self.class.operators[op] || raise(Liquid::ArgumentError, "Unknown operator #{op}")
@@ -150,19 +150,11 @@ module Liquid
end end
end end
def deprecated_default_context
warn("DEPRECATION WARNING: Condition#evaluate without a context argument is deprecated" \
" and will be removed from Liquid 6.0.0.")
Context.new
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children def children
[ [
@node.left, @node.left, @node.right,
@node.right, @node.child_condition, @node.attachment
@node.child_condition,
@node.attachment
].compact ].compact
end end
end end
+4 -8
View File
@@ -26,9 +26,9 @@ module Liquid
@environments = [environments] @environments = [environments]
@environments.flatten! @environments.flatten!
@static_environments = [static_environments].flatten(1).freeze @static_environments = [static_environments].flat_map(&:freeze).freeze
@scopes = [(outer_scope || {})] @scopes = [(outer_scope || {})]
@registers = registers.is_a?(Registers) ? registers : Registers.new(registers) @registers = registers
@errors = [] @errors = []
@partial = false @partial = false
@strict_variables = false @strict_variables = false
@@ -39,10 +39,6 @@ module Liquid
@global_filter = nil @global_filter = nil
@disabled_tags = {} @disabled_tags = {}
@registers.static[:cached_partials] ||= {}
@registers.static[:file_system] ||= Liquid::Template.file_system
@registers.static[:template_factory] ||= Liquid::TemplateFactory.new
self.exception_renderer = Template.default_exception_renderer self.exception_renderer = Template.default_exception_renderer
if rethrow_errors if rethrow_errors
self.exception_renderer = Liquid::RAISE_EXCEPTION_LAMBDA self.exception_renderer = Liquid::RAISE_EXCEPTION_LAMBDA
@@ -128,7 +124,7 @@ module Liquid
# context['var'] = 'hi' # context['var'] = 'hi'
# end # end
# #
# context['var'] #=> nil # context['var] #=> nil
def stack(new_scope = {}) def stack(new_scope = {})
push(new_scope) push(new_scope)
yield yield
@@ -144,7 +140,7 @@ module Liquid
self.class.build( self.class.build(
resource_limits: resource_limits, resource_limits: resource_limits,
static_environments: static_environments, static_environments: static_environments,
registers: Registers.new(registers), registers: StaticRegisters.new(registers)
).tap do |subcontext| ).tap do |subcontext|
subcontext.base_scope_depth = base_scope_depth + 1 subcontext.base_scope_depth = base_scope_depth + 1
subcontext.exception_renderer = exception_renderer subcontext.exception_renderer = exception_renderer
-4
View File
@@ -25,10 +25,6 @@ module Liquid
class Drop class Drop
attr_writer :context attr_writer :context
def initialize
@context = nil
end
# Catch all for the method # Catch all for the method
def liquid_method_missing(method) def liquid_method_missing(method)
return nil unless @context&.strict_variables return nil unless @context&.strict_variables
+11 -15
View File
@@ -3,33 +3,28 @@
module Liquid module Liquid
class Expression class Expression
LITERALS = { LITERALS = {
nil => nil, nil => nil, 'nil' => nil, 'null' => nil, '' => nil,
'nil' => nil,
'null' => nil,
'' => nil,
'true' => true, 'true' => true,
'false' => false, 'false' => false,
'blank' => '', 'blank' => '',
'empty' => '' 'empty' => ''
}.freeze }.freeze
INTEGERS_REGEX = /\A(-?\d+)\z/ SINGLE_QUOTED_STRING = /\A\s*'(.*)'\s*\z/m
FLOATS_REGEX = /\A(-?\d[\d\.]+)\z/ 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 # Use an atomic group (?>...) to avoid pathological backtracing from
# malicious input as described in https://github.com/Shopify/liquid/issues/1357 # 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) 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 case markup
when nil
nil
when SINGLE_QUOTED_STRING, DOUBLE_QUOTED_STRING
Regexp.last_match(1)
when INTEGERS_REGEX when INTEGERS_REGEX
Regexp.last_match(1).to_i Regexp.last_match(1).to_i
when RANGES_REGEX when RANGES_REGEX
@@ -37,6 +32,7 @@ module Liquid
when FLOATS_REGEX when FLOATS_REGEX
Regexp.last_match(1).to_f Regexp.last_match(1).to_f
else else
markup = markup.strip
if LITERALS.key?(markup) if LITERALS.key?(markup)
LITERALS[markup] LITERALS[markup]
else else
+5 -45
View File
@@ -1,11 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs
# @liquid_type object
# @liquid_name forloop
# @liquid_summary
# Information about a parent [`for` loop](/docs/api/liquid/tags/for).
class ForloopDrop < Drop class ForloopDrop < Drop
def initialize(name, length, parentloop) def initialize(name, length, parentloop)
@name = name @name = name
@@ -14,68 +9,33 @@ module Liquid
@index = 0 @index = 0
end end
# @liquid_public_docs attr_reader :length, :parentloop
# @liquid_name length
# @liquid_summary
# The total number of iterations in the loop.
# @liquid_return [number]
attr_reader :length
# @liquid_public_docs def name
# @liquid_name parentloop Usage.increment('forloop_drop_name')
# @liquid_summary @name
# The parent `forloop` object. end
# @liquid_description
# If the current `for` loop isn't nested inside another `for` loop, then `nil` is returned.
# @liquid_return [forloop]
attr_reader :parentloop
attr_reader :name
# @liquid_public_docs
# @liquid_summary
# The 1-based index of the current iteration.
# @liquid_return [number]
def index def index
@index + 1 @index + 1
end end
# @liquid_public_docs
# @liquid_summary
# The 0-based index of the current iteration.
# @liquid_return [number]
def index0 def index0
@index @index
end end
# @liquid_public_docs
# @liquid_summary
# The 1-based index of the current iteration, in reverse order.
# @liquid_return [number]
def rindex def rindex
@length - @index @length - @index
end end
# @liquid_public_docs
# @liquid_summary
# The 0-based index of the current iteration, in reverse order.
# @liquid_return [number]
def rindex0 def rindex0
@length - @index - 1 @length - @index - 1
end end
# @liquid_public_docs
# @liquid_summary
# Returns `true` if the current iteration is the first. Returns `false` if not.
# @liquid_return [boolean]
def first def first
@index == 0 @index == 0
end end
# @liquid_public_docs
# @liquid_summary
# Returns `true` if the current iteration is the last. Returns `false` if not.
# @liquid_return [boolean]
def last def last
@index == @length - 1 @index == @length - 1
end end
+3 -2
View File
@@ -18,7 +18,6 @@ module Liquid
IDENTIFIER = /[a-zA-Z_][\w-]*\??/ IDENTIFIER = /[a-zA-Z_][\w-]*\??/
SINGLE_STRING_LITERAL = /'[^\']*'/ SINGLE_STRING_LITERAL = /'[^\']*'/
DOUBLE_STRING_LITERAL = /"[^\"]*"/ DOUBLE_STRING_LITERAL = /"[^\"]*"/
STRING_LITERAL = Regexp.union(SINGLE_STRING_LITERAL, DOUBLE_STRING_LITERAL)
NUMBER_LITERAL = /-?\d+(\.\d+)?/ NUMBER_LITERAL = /-?\d+(\.\d+)?/
DOTDOT = /\.\./ DOTDOT = /\.\./
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/ COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/
@@ -36,7 +35,9 @@ module Liquid
break if @ss.eos? break if @ss.eos?
tok = if (t = @ss.scan(COMPARISON_OPERATOR)) tok = if (t = @ss.scan(COMPARISON_OPERATOR))
[:comparison, t] [:comparison, t]
elsif (t = @ss.scan(STRING_LITERAL)) elsif (t = @ss.scan(SINGLE_STRING_LITERAL))
[:string, t]
elsif (t = @ss.scan(DOUBLE_STRING_LITERAL))
[:string, t] [:string, t]
elsif (t = @ss.scan(NUMBER_LITERAL)) elsif (t = @ss.scan(NUMBER_LITERAL))
[:number, t] [:number, t]
+6 -6
View File
@@ -13,16 +13,16 @@
for_invalid_attribute: "Invalid attribute in for loop. Valid attributes are limit and offset" for_invalid_attribute: "Invalid attribute in for loop. Valid attributes are limit and offset"
if: "Syntax Error in tag 'if' - Valid syntax: if [expression]" if: "Syntax Error in tag 'if' - Valid syntax: if [expression]"
include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]" include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]"
inline_comment_invalid: "Syntax error in tag '#' - Each line of comments must be prefixed by the '#' character" unknown_tag: "Unknown tag '%{tag}'"
invalid_delimiter: "'%{tag}' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}" invalid_delimiter: "'%{tag}' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}"
render: "Syntax error in tag 'render' - Template name must be a quoted string"
table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
tag_never_closed: "'%{block_name}' tag was never closed"
tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"
unexpected_else: "%{block_name} tag does not expect 'else' tag" unexpected_else: "%{block_name} tag does not expect 'else' tag"
unexpected_outer_tag: "Unexpected outer '%{tag}' tag" unexpected_outer_tag: "Unexpected outer '%{tag}' tag"
unknown_tag: "Unknown tag '%{tag}'" tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"
variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}" variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}"
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: argument:
include: "Argument error in tag 'include' - Illegal template name" include: "Argument error in tag 'include' - Illegal template name"
disabled: disabled:
+4 -12
View File
@@ -3,27 +3,19 @@
module Liquid module Liquid
class PartialCache class PartialCache
def self.load(template_name, context:, parse_context:) def self.load(template_name, context:, parse_context:)
cached_partials = context.registers[:cached_partials] cached_partials = (context.registers[:cached_partials] ||= {})
cached = cached_partials[template_name] cached = cached_partials[template_name]
return cached if cached return cached if cached
file_system = context.registers[:file_system] file_system = (context.registers[:file_system] ||= Liquid::Template.file_system)
source = file_system.read_template_file(template_name) source = file_system.read_template_file(template_name)
parse_context.partial = true parse_context.partial = true
template_factory = context.registers[:template_factory] template_factory = (context.registers[:template_factory] ||= Liquid::TemplateFactory.new)
template = template_factory.for(template_name) template = template_factory.for(template_name)
begin partial = template.parse(source, parse_context)
partial = template.parse(source, parse_context)
rescue Liquid::Error => e
e.template_name = template&.name || template_name
raise e
end
partial.name ||= template_name
cached_partials[template_name] = partial cached_partials[template_name] = partial
ensure ensure
parse_context.partial = false parse_context.partial = false
+1 -19
View File
@@ -8,22 +8,10 @@ module Liquid
if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate) if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate)
new(start_obj, end_obj) new(start_obj, end_obj)
else else
begin start_obj.to_i..end_obj.to_i
start_obj.to_i..end_obj.to_i
rescue NoMethodError
invalid_expr = start_markup unless start_obj.respond_to?(:to_i)
invalid_expr ||= end_markup unless end_obj.respond_to?(:to_i)
if invalid_expr
raise Liquid::SyntaxError, "Invalid expression type '#{invalid_expr}' in range expression"
end
raise
end
end end
end end
attr_reader :start_obj, :end_obj
def initialize(start_obj, end_obj) def initialize(start_obj, end_obj)
@start_obj = start_obj @start_obj = start_obj
@end_obj = end_obj @end_obj = end_obj
@@ -47,11 +35,5 @@ module Liquid
Utils.to_integer(input) Utils.to_integer(input)
end end
end end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[@node.start_obj, @node.end_obj]
end
end
end end
end end
+6
View File
@@ -0,0 +1,6 @@
# frozen_string_literal: true
module Liquid
class Register
end
end
-51
View File
@@ -1,51 +0,0 @@
# frozen_string_literal: true
module Liquid
class Registers
attr_reader :static
def initialize(registers = {})
@static = registers.is_a?(Registers) ? registers.static : registers
@changes = {}
end
def []=(key, value)
@changes[key] = value
end
def [](key)
if @changes.key?(key)
@changes[key]
else
@static[key]
end
end
def delete(key)
@changes.delete(key)
end
UNDEFINED = Object.new
def fetch(key, default = UNDEFINED, &block)
if @changes.key?(key)
@changes.fetch(key)
elsif default != UNDEFINED
if block_given?
@static.fetch(key, &block)
else
@static.fetch(key, default)
end
else
@static.fetch(key, &block)
end
end
def key?(key)
@changes.key?(key) || @static.key?(key)
end
end
# Alias for backwards compatibility
StaticRegisters = Registers
end
+80 -512
View File
@@ -1,19 +1,10 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'cgi' require 'cgi'
require 'base64'
require 'bigdecimal' require 'bigdecimal'
module Liquid module Liquid
module StandardFilters module StandardFilters
MAX_I32 = (1 << 31) - 1
private_constant :MAX_I32
MIN_I64 = -(1 << 63)
MAX_I64 = (1 << 63) - 1
I64_RANGE = MIN_I64..MAX_I64
private_constant :MIN_I64, :MAX_I64, :I64_RANGE
HTML_ESCAPE = { HTML_ESCAPE = {
'&' => '&amp;', '&' => '&amp;',
'>' => '&gt;', '>' => '&gt;',
@@ -25,103 +16,43 @@ module Liquid
STRIP_HTML_BLOCKS = Regexp.union( STRIP_HTML_BLOCKS = Regexp.union(
%r{<script.*?</script>}m, %r{<script.*?</script>}m,
/<!--.*?-->/m, /<!--.*?-->/m,
%r{<style.*?</style>}m, %r{<style.*?</style>}m
) )
STRIP_HTML_TAGS = /<.*?>/m STRIP_HTML_TAGS = /<.*?>/m
# @liquid_public_docs # Return the size of an array or of an string
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Returns the size of a string or array.
# @liquid_description
# The size of a string is the number of characters that the string includes. The size of an array is the number of items
# in the array.
# @liquid_syntax variable | size
# @liquid_return [number]
def size(input) def size(input)
input.respond_to?(:size) ? input.size : 0 input.respond_to?(:size) ? input.size : 0
end end
# @liquid_public_docs # convert an input string to DOWNCASE
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Converts a string to all lowercase characters.
# @liquid_syntax string | downcase
# @liquid_return [string]
def downcase(input) def downcase(input)
input.to_s.downcase input.to_s.downcase
end end
# @liquid_public_docs # convert an input string to UPCASE
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Converts a string to all uppercase characters.
# @liquid_syntax string | upcase
# @liquid_return [string]
def upcase(input) def upcase(input)
input.to_s.upcase input.to_s.upcase
end end
# @liquid_public_docs # capitalize words in the input centence
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Capitalizes the first word in a string.
# @liquid_syntax string | capitalize
# @liquid_return [string]
def capitalize(input) def capitalize(input)
input.to_s.capitalize input.to_s.capitalize
end end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Escapes special characters in HTML, such as `<>`, `'`, and `&`, and converts characters into escape sequences. The filter doesn't effect characters within the string that dont have a corresponding escape sequence.".
# @liquid_syntax string | escape
# @liquid_return [string]
def escape(input) def escape(input)
CGI.escapeHTML(input.to_s) unless input.nil? CGI.escapeHTML(input.to_s) unless input.nil?
end end
alias_method :h, :escape alias_method :h, :escape
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Escapes a string without changing characters that have already been escaped.
# @liquid_syntax string | escape_once
# @liquid_return [string]
def escape_once(input) def escape_once(input)
input.to_s.gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE) input.to_s.gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE)
end end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Converts any URL-unsafe characters in a string to the
# [percent-encoded](https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding) equivalent.
# @liquid_description
# > Note:
# > Spaces are converted to a `+` character, instead of a percent-encoded character.
# @liquid_syntax string | url_encode
# @liquid_return [string]
def url_encode(input) def url_encode(input)
CGI.escape(input.to_s) unless input.nil? CGI.escape(input.to_s) unless input.nil?
end end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Decodes any [percent-encoded](https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding) characters
# in a string.
# @liquid_syntax string | url_decode
# @liquid_return [string]
def url_decode(input) def url_decode(input)
return if input.nil? return if input.nil?
@@ -131,94 +62,18 @@ module Liquid
result result
end end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Encodes a string to [Base64 format](https://developer.mozilla.org/en-US/docs/Glossary/Base64).
# @liquid_syntax string | base64_encode
# @liquid_return [string]
def base64_encode(input)
Base64.strict_encode64(input.to_s)
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Decodes a string in [Base64 format](https://developer.mozilla.org/en-US/docs/Glossary/Base64).
# @liquid_syntax string | base64_decode
# @liquid_return [string]
def base64_decode(input)
Base64.strict_decode64(input.to_s)
rescue ::ArgumentError
raise Liquid::ArgumentError, "invalid base64 provided to base64_decode"
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Encodes a string to URL-safe [Base64 format](https://developer.mozilla.org/en-US/docs/Glossary/Base64).
# @liquid_syntax string | base64_url_safe_encode
# @liquid_return [string]
def base64_url_safe_encode(input)
Base64.urlsafe_encode64(input.to_s)
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Decodes a string in URL-safe [Base64 format](https://developer.mozilla.org/en-US/docs/Glossary/Base64).
# @liquid_syntax string | base64_url_safe_decode
# @liquid_return [string]
def base64_url_safe_decode(input)
Base64.urlsafe_decode64(input.to_s)
rescue ::ArgumentError
raise Liquid::ArgumentError, "invalid base64 provided to base64_url_safe_decode"
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Returns a substring or series of array items, starting at a given 0-based index.
# @liquid_description
# By default, the substring has a length of one character, and the array series has one array item. However, you can
# provide a second parameter to specify the number of characters or array items.
# @liquid_syntax string | slice
# @liquid_return [string]
def slice(input, offset, length = nil) def slice(input, offset, length = nil)
offset = Utils.to_integer(offset) offset = Utils.to_integer(offset)
length = length ? Utils.to_integer(length) : 1 length = length ? Utils.to_integer(length) : 1
begin if input.is_a?(Array)
if input.is_a?(Array) input.slice(offset, length) || []
input.slice(offset, length) || [] else
else input.to_s.slice(offset, length) || ''
input.to_s.slice(offset, length) || ''
end
rescue RangeError
if I64_RANGE.cover?(length) && I64_RANGE.cover?(offset)
raise # unexpected error
end
offset = offset.clamp(I64_RANGE)
length = length.clamp(I64_RANGE)
retry
end end
end end
# @liquid_public_docs # Truncate a string down to x characters
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Truncates a string down to a given number of characters.
# @liquid_description
# If the specified number of characters is less than the length of the string, then an ellipsis (`...`) is appended to
# the truncated string. The ellipsis is included in the character count of the truncated string.
# @liquid_syntax string | truncate: number
# @liquid_return [string]
def truncate(input, length = 50, truncate_string = "...") def truncate(input, length = 50, truncate_string = "...")
return if input.nil? return if input.nil?
input_str = input.to_s input_str = input.to_s
@@ -232,90 +87,40 @@ module Liquid
input_str.length > length ? input_str[0...l].concat(truncate_string_str) : input_str input_str.length > length ? input_str[0...l].concat(truncate_string_str) : input_str
end end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Truncates a string down to a given number of words.
# @liquid_description
# If the specified number of words is less than the number of words in the string, then an ellipsis (`...`) is appended to
# the truncated string.
#
# > Caution:
# > HTML tags are treated as words, so you should strip any HTML from truncated content. If you don't strip HTML, then
# > closing HTML tags can be removed, which can result in unexpected behavior.
# @liquid_syntax string | truncatewords: number
# @liquid_return [string]
def truncatewords(input, words = 15, truncate_string = "...") def truncatewords(input, words = 15, truncate_string = "...")
return if input.nil? return if input.nil?
input = input.to_s input = input.to_s
words = Utils.to_integer(words) words = Utils.to_integer(words)
words = 1 if words <= 0 words = 1 if words <= 0
wordlist = begin wordlist = input.split(" ", words + 1)
input.split(" ", words + 1)
rescue RangeError
# integer too big for String#split, but we can semantically assume no truncation is needed
return input if words + 1 > MAX_I32
raise # unexpected error
end
return input if wordlist.length <= words return input if wordlist.length <= words
wordlist.pop wordlist.pop
wordlist.join(" ").concat(truncate_string.to_s) wordlist.join(" ").concat(truncate_string.to_s)
end end
# @liquid_public_docs # Split input string into an array of substrings separated by given pattern.
# @liquid_type filter #
# @liquid_category string # Example:
# @liquid_summary # <div class="summary">{{ post | split '//' | first }}</div>
# Splits a string into an array of substrings based on a given separator. #
# @liquid_syntax string | split: string
# @liquid_return [array[string]]
def split(input, pattern) def split(input, pattern)
input.to_s.split(pattern.to_s) input.to_s.split(pattern.to_s)
end end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Strips all whitespace from the left and right of a string.
# @liquid_syntax string | strip
# @liquid_return [string]
def strip(input) def strip(input)
input.to_s.strip input.to_s.strip
end end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Strips all whitespace from the left of a string.
# @liquid_syntax string | lstrip
# @liquid_return [string]
def lstrip(input) def lstrip(input)
input.to_s.lstrip input.to_s.lstrip
end end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Strips all whitespace from the right of a string.
# @liquid_syntax string | rstrip
# @liquid_return [string]
def rstrip(input) def rstrip(input)
input.to_s.rstrip input.to_s.rstrip
end end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Strips all HTML tags from a string.
# @liquid_syntax string | strip_html
# @liquid_return [string]
def strip_html(input) def strip_html(input)
empty = '' empty = ''
result = input.to_s.gsub(STRIP_HTML_BLOCKS, empty) result = input.to_s.gsub(STRIP_HTML_BLOCKS, empty)
@@ -323,35 +128,18 @@ module Liquid
result result
end end
# @liquid_public_docs # Remove all newlines from the string
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Strips all newline characters (line breaks) from a string.
# @liquid_syntax string | strip_newlines
# @liquid_return [string]
def strip_newlines(input) def strip_newlines(input)
input.to_s.gsub(/\r?\n/, '') input.to_s.gsub(/\r?\n/, '')
end end
# @liquid_public_docs # Join elements of the array with certain character between them
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Combines all of the items in an array into a single string, separated by a space.
# @liquid_syntax array | join
# @liquid_return [string]
def join(input, glue = ' ') def join(input, glue = ' ')
InputIterator.new(input, context).join(glue) InputIterator.new(input, context).join(glue)
end end
# @liquid_public_docs # Sort elements of the array
# @liquid_type filter # provide optional property with which to sort an array of hashes or drops
# @liquid_category array
# @liquid_summary
# Sorts the items in an array in case-sensitive alphabetical, or numerical, order.
# @liquid_syntax array | sort
# @liquid_return [array[untyped]]
def sort(input, property = nil) def sort(input, property = nil)
ary = InputIterator.new(input, context) ary = InputIterator.new(input, context)
@@ -370,17 +158,8 @@ module Liquid
end end
end end
# @liquid_public_docs # Sort elements of an array ignoring case if strings
# @liquid_type filter # provide optional property with which to sort an array of hashes or drops
# @liquid_category array
# @liquid_summary
# Sorts the items in an array in case-insensitive alphabetical order.
# @liquid_description
# > Caution:
# > You shouldn't use the `sort_natural` filter to sort numerical values. When comparing items an array, each item is converted to a
# > string, so sorting on numerical values can lead to unexpected results.
# @liquid_syntax array | sort_natural
# @liquid_return [array[untyped]]
def sort_natural(input, property = nil) def sort_natural(input, property = nil)
ary = InputIterator.new(input, context) ary = InputIterator.new(input, context)
@@ -399,48 +178,30 @@ module Liquid
end end
end end
# @liquid_public_docs # Filter the elements of an array to those with a certain property value.
# @liquid_type filter # By default the target is any truthy value.
# @liquid_category array
# @liquid_summary
# Filters an array to include only items with a specific property value.
# @liquid_description
# This requires you to provide both the property name and the associated value.
# @liquid_syntax array | where: string, string
# @liquid_return [array[untyped]]
def where(input, property, target_value = nil) def where(input, property, target_value = nil)
ary = InputIterator.new(input, context) ary = InputIterator.new(input, context)
if ary.empty? if ary.empty?
[] []
elsif target_value.nil? elsif ary.first.respond_to?(:[]) && target_value.nil?
ary.select do |item| begin
item[property] ary.select { |item| item[property] }
rescue TypeError rescue TypeError
raise_property_error(property) raise_property_error(property)
rescue NoMethodError
return nil unless item.respond_to?(:[])
raise
end end
else elsif ary.first.respond_to?(:[])
ary.select do |item| begin
item[property] == target_value ary.select { |item| item[property] == target_value }
rescue TypeError rescue TypeError
raise_property_error(property) raise_property_error(property)
rescue NoMethodError
return nil unless item.respond_to?(:[])
raise
end end
end end
end end
# @liquid_public_docs # Remove duplicate elements from an array
# @liquid_type filter # provide optional property with which to determine uniqueness
# @liquid_category array
# @liquid_summary
# Removes any duplicate items in an array.
# @liquid_syntax array | uniq
# @liquid_return [array[untyped]]
def uniq(input, property = nil) def uniq(input, property = nil)
ary = InputIterator.new(input, context) ary = InputIterator.new(input, context)
@@ -448,37 +209,22 @@ module Liquid
ary.uniq ary.uniq
elsif ary.empty? # The next two cases assume a non-empty array. elsif ary.empty? # The next two cases assume a non-empty array.
[] []
else elsif ary.first.respond_to?(:[])
ary.uniq do |item| begin
item[property] ary.uniq { |a| a[property] }
rescue TypeError rescue TypeError
raise_property_error(property) raise_property_error(property)
rescue NoMethodError
return nil unless item.respond_to?(:[])
raise
end end
end end
end end
# @liquid_public_docs # Reverse the elements of an array
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Reverses the order of the items in an array.
# @liquid_syntax array | reverse
# @liquid_return [array[untyped]]
def reverse(input) def reverse(input)
ary = InputIterator.new(input, context) ary = InputIterator.new(input, context)
ary.reverse ary.reverse
end end
# @liquid_public_docs # map/collect on a given property
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Creates an array of values from a specific property of the items in an array.
# @liquid_syntax array | map: string
# @liquid_return [array[untyped]]
def map(input, property) def map(input, property)
InputIterator.new(input, context).map do |e| InputIterator.new(input, context).map do |e|
e = e.call if e.is_a?(Proc) e = e.call if e.is_a?(Proc)
@@ -494,13 +240,8 @@ module Liquid
raise_property_error(property) raise_property_error(property)
end end
# @liquid_public_docs # Remove nils within an array
# @liquid_type filter # provide optional property with which to check for nil
# @liquid_category array
# @liquid_summary
# Removes any `nil` items from an array.
# @liquid_syntax array | compact
# @liquid_return [array[untyped]]
def compact(input, property = nil) def compact(input, property = nil)
ary = InputIterator.new(input, context) ary = InputIterator.new(input, context)
@@ -508,116 +249,40 @@ module Liquid
ary.compact ary.compact
elsif ary.empty? # The next two cases assume a non-empty array. elsif ary.empty? # The next two cases assume a non-empty array.
[] []
else elsif ary.first.respond_to?(:[])
ary.reject do |item| begin
item[property].nil? ary.reject { |a| a[property].nil? }
rescue TypeError rescue TypeError
raise_property_error(property) raise_property_error(property)
rescue NoMethodError
return nil unless item.respond_to?(:[])
raise
end end
end end
end end
# @liquid_public_docs # Replace occurrences of a string with another
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Replaces any instance of a substring inside a string with a given string.
# @liquid_syntax string | replace: string, string
# @liquid_return [string]
def replace(input, string, replacement = '') def replace(input, string, replacement = '')
input.to_s.gsub(string.to_s, replacement.to_s) input.to_s.gsub(string.to_s, replacement.to_s)
end end
# @liquid_public_docs # Replace the first occurrences of a string with another
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Replaces the first instance of a substring inside a string with a given string.
# @liquid_syntax string | replace_first: string, string
# @liquid_return [string]
def replace_first(input, string, replacement = '') def replace_first(input, string, replacement = '')
input.to_s.sub(string.to_s, replacement.to_s) input.to_s.sub(string.to_s, replacement.to_s)
end end
# @liquid_public_docs # remove a substring
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Replaces the last instance of a substring inside a string with a given string.
# @liquid_syntax string | replace_last: string, string
# @liquid_return [string]
def replace_last(input, string, replacement)
input = 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
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Removes any instance of a substring inside a string.
# @liquid_syntax string | remove: string
# @liquid_return [string]
def remove(input, string) def remove(input, string)
replace(input, string, '') input.to_s.gsub(string.to_s, '')
end end
# @liquid_public_docs # remove the first occurrences of a substring
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Removes the first instance of a substring inside a string.
# @liquid_syntax string | remove_first: string
# @liquid_return [string]
def remove_first(input, string) def remove_first(input, string)
replace_first(input, string, '') input.to_s.sub(string.to_s, '')
end end
# @liquid_public_docs # add one string to another
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Removes the last instance of a substring inside a string.
# @liquid_syntax string | remove_last: string
# @liquid_return [string]
def remove_last(input, string)
replace_last(input, string, '')
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Adds a given string to the end of a string.
# @liquid_syntax string | append: string
# @liquid_return [string]
def append(input, string) def append(input, string)
input.to_s + string.to_s input.to_s + string.to_s
end end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Concatenates (combines) two arrays.
# @liquid_description
# > Note:
# > The `concat` filter won't filter out duplicates. If you want to remove duplicates, then you need to use the
# > [`uniq` filter](/docs/api/liquid/filters/uniq).
# @liquid_syntax array | concat: array
# @liquid_return [array[untyped]]
def concat(input, array) def concat(input, array)
unless array.respond_to?(:to_ary) unless array.respond_to?(:to_ary)
raise ArgumentError, "concat filter requires an array argument" raise ArgumentError, "concat filter requires an array argument"
@@ -625,24 +290,12 @@ module Liquid
InputIterator.new(input, context).concat(array) InputIterator.new(input, context).concat(array)
end end
# @liquid_public_docs # prepend a string to another
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Adds a given string to the beginning of a string.
# @liquid_syntax string | prepend: string
# @liquid_return [string]
def prepend(input, string) def prepend(input, string)
string.to_s + input.to_s string.to_s + input.to_s
end end
# @liquid_public_docs # Add <br /> tags in front of all newlines in input string
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Converts newlines (`\n`) in a string to HTML line breaks (`<br>`).
# @liquid_syntax string | newline_to_br
# @liquid_return [string]
def newline_to_br(input) def newline_to_br(input)
input.to_s.gsub(/\r?\n/, "<br />\n") input.to_s.gsub(/\r?\n/, "<br />\n")
end end
@@ -686,106 +339,58 @@ module Liquid
date.strftime(format.to_s) date.strftime(format.to_s)
end end
# @liquid_public_docs # Get the first element of the passed in array
# @liquid_type filter #
# @liquid_category array # Example:
# @liquid_summary # {{ product.images | first | to_img }}
# Returns the first item in an array. #
# @liquid_syntax array | first
# @liquid_return [untyped]
def first(array) def first(array)
array.first if array.respond_to?(:first) array.first if array.respond_to?(:first)
end end
# @liquid_public_docs # Get the last element of the passed in array
# @liquid_type filter #
# @liquid_category array # Example:
# @liquid_summary # {{ product.images | last | to_img }}
# Returns the last item in an array. #
# @liquid_syntax array | last
# @liquid_return [untyped]
def last(array) def last(array)
array.last if array.respond_to?(:last) array.last if array.respond_to?(:last)
end end
# @liquid_public_docs # absolute value
# @liquid_type filter
# @liquid_category math
# @liquid_summary
# Returns the absolute value of a number.
# @liquid_syntax number | abs
# @liquid_return [number]
def abs(input) def abs(input)
result = Utils.to_number(input).abs result = Utils.to_number(input).abs
result.is_a?(BigDecimal) ? result.to_f : result result.is_a?(BigDecimal) ? result.to_f : result
end end
# @liquid_public_docs # addition
# @liquid_type filter
# @liquid_category math
# @liquid_summary
# Adds two numbers.
# @liquid_syntax number | plus: number
# @liquid_return [number]
def plus(input, operand) def plus(input, operand)
apply_operation(input, operand, :+) apply_operation(input, operand, :+)
end end
# @liquid_public_docs # subtraction
# @liquid_type filter
# @liquid_category math
# @liquid_summary
# Subtracts a given number from another number.
# @liquid_syntax number | minus: number
# @liquid_return [number]
def minus(input, operand) def minus(input, operand)
apply_operation(input, operand, :-) apply_operation(input, operand, :-)
end end
# @liquid_public_docs # multiplication
# @liquid_type filter
# @liquid_category math
# @liquid_summary
# Multiplies a number by a given number.
# @liquid_syntax number | times: number
# @liquid_return [number]
def times(input, operand) def times(input, operand)
apply_operation(input, operand, :*) apply_operation(input, operand, :*)
end end
# @liquid_public_docs # division
# @liquid_type filter
# @liquid_category math
# @liquid_summary
# Divides a number by a given number. The `divided_by` filter produces a result of the same type as the divisor. This means if you divide by an integer, the result will be an integer, and if you divide by a float, the result will be a float.
# @liquid_syntax number | divided_by: number
# @liquid_return [number]
def divided_by(input, operand) def divided_by(input, operand)
apply_operation(input, operand, :/) apply_operation(input, operand, :/)
rescue ::ZeroDivisionError => e rescue ::ZeroDivisionError => e
raise Liquid::ZeroDivisionError, e.message raise Liquid::ZeroDivisionError, e.message
end end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category math
# @liquid_summary
# Returns the remainder of dividing a number by a given number.
# @liquid_syntax number | modulo: number
# @liquid_return [number]
def modulo(input, operand) def modulo(input, operand)
apply_operation(input, operand, :%) apply_operation(input, operand, :%)
rescue ::ZeroDivisionError => e rescue ::ZeroDivisionError => e
raise Liquid::ZeroDivisionError, e.message raise Liquid::ZeroDivisionError, e.message
end end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category math
# @liquid_summary
# Rounds a number to the nearest integer.
# @liquid_syntax number | round
# @liquid_return [number]
def round(input, n = 0) def round(input, n = 0)
result = Utils.to_number(input).round(Utils.to_number(n)) result = Utils.to_number(input).round(Utils.to_number(n))
result = result.to_f if result.is_a?(BigDecimal) result = result.to_f if result.is_a?(BigDecimal)
@@ -795,39 +400,18 @@ module Liquid
raise Liquid::FloatDomainError, e.message raise Liquid::FloatDomainError, e.message
end end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category math
# @liquid_summary
# Rounds a number up to the nearest integer.
# @liquid_syntax number | ceil
# @liquid_return [number]
def ceil(input) def ceil(input)
Utils.to_number(input).ceil.to_i Utils.to_number(input).ceil.to_i
rescue ::FloatDomainError => e rescue ::FloatDomainError => e
raise Liquid::FloatDomainError, e.message raise Liquid::FloatDomainError, e.message
end end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category math
# @liquid_summary
# Rounds a number down to the nearest integer.
# @liquid_syntax number | floor
# @liquid_return [number]
def floor(input) def floor(input)
Utils.to_number(input).floor.to_i Utils.to_number(input).floor.to_i
rescue ::FloatDomainError => e rescue ::FloatDomainError => e
raise Liquid::FloatDomainError, e.message raise Liquid::FloatDomainError, e.message
end end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category math
# @liquid_summary
# Limits a number to a minimum value.
# @liquid_syntax number | at_least
# @liquid_return [number]
def at_least(input, n) def at_least(input, n)
min_value = Utils.to_number(n) min_value = Utils.to_number(n)
@@ -836,13 +420,6 @@ module Liquid
result.is_a?(BigDecimal) ? result.to_f : result result.is_a?(BigDecimal) ? result.to_f : result
end end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category math
# @liquid_summary
# Limits a number to a maximum value.
# @liquid_syntax number | at_most
# @liquid_return [number]
def at_most(input, n) def at_most(input, n)
max_value = Utils.to_number(n) max_value = Utils.to_number(n)
@@ -851,21 +428,19 @@ module Liquid
result.is_a?(BigDecimal) ? result.to_f : result result.is_a?(BigDecimal) ? result.to_f : result
end end
# @liquid_public_docs # Set a default value when the input is nil, false or empty
# @liquid_type filter #
# @liquid_category default # Example:
# @liquid_summary # {{ product.title | default: "No Title" }}
# Sets a default value for any variable whose value is one of the following: #
# Use `allow_false` when an input should only be tested against nil or empty and not false.
#
# Example:
# {{ product.title | default: "No Title", allow_false: true }}
# #
# - [`empty`](/docs/api/liquid/basics#empty)
# - [`false`](/docs/api/liquid/basics#truthy-and-falsy)
# - [`nil`](/docs/api/liquid/basics#nil)
# @liquid_syntax variable | default: variable
# @liquid_return [untyped]
# @liquid_optional_param allow_false [boolean] Whether to use false values instead of the default.
def default(input, default_value = '', options = {}) def default(input, default_value = '', options = {})
options = {} unless options.is_a?(Hash) 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 false_check || (input.respond_to?(:empty?) && input.empty?) ? default_value : input
end end
@@ -883,16 +458,10 @@ module Liquid
end end
def nil_safe_compare(a, b) def nil_safe_compare(a, b)
result = a <=> b if !a.nil? && !b.nil?
a <=> b
if result
result
elsif a.nil?
1
elsif b.nil?
-1
else else
raise Liquid::ArgumentError, "cannot sort values of incompatible types" a.nil? ? 1 : -1
end end
end end
@@ -947,9 +516,8 @@ module Liquid
def each def each
@input.each do |e| @input.each do |e|
e = e.respond_to?(:to_liquid) ? e.to_liquid : e
e.context = @context if e.respond_to?(:context=) e.context = @context if e.respond_to?(:context=)
yield(e) yield(e.respond_to?(:to_liquid) ? e.to_liquid : e)
end end
end end
end end
+44
View File
@@ -0,0 +1,44 @@
# frozen_string_literal: true
module Liquid
class StaticRegisters
attr_reader :static
def initialize(registers = {})
@static = registers.is_a?(StaticRegisters) ? registers.static : registers
@registers = {}
end
def []=(key, value)
@registers[key] = value
end
def [](key)
if @registers.key?(key)
@registers[key]
else
@static[key]
end
end
def delete(key)
@registers.delete(key)
end
UNDEFINED = Object.new
def fetch(key, default = UNDEFINED, &block)
if @registers.key?(key)
@registers.fetch(key)
elsif default != UNDEFINED
@static.fetch(key, default, &block)
else
@static.fetch(key, &block)
end
end
def key?(key)
@registers.key?(key) || @static.key?(key)
end
end
end
+10 -15
View File
@@ -7,30 +7,25 @@ module Liquid
def add_global_filter(filter) def add_global_filter(filter)
strainer_class_cache.clear strainer_class_cache.clear
GlobalCache.add_filter(filter) global_filters << filter
end end
def create(context, filters = []) def create(context, filters = [])
strainer_from_cache(filters).new(context) strainer_from_cache(filters).new(context)
end end
def global_filter_names
GlobalCache.filter_method_names
end
GlobalCache = Class.new(StrainerTemplate)
private private
def global_filters
@global_filters ||= []
end
def strainer_from_cache(filters) def strainer_from_cache(filters)
if filters.empty? strainer_class_cache[filters] ||= begin
GlobalCache klass = Class.new(StrainerTemplate)
else global_filters.each { |f| klass.add_filter(f) }
strainer_class_cache[filters] ||= begin filters.each { |f| klass.add_filter(f) }
klass = Class.new(GlobalCache) klass
filters.each { |f| klass.add_filter(f) }
klass
end
end end
end end
-9
View File
@@ -31,15 +31,6 @@ module Liquid
filter_methods.include?(method.to_s) filter_methods.include?(method.to_s)
end end
def inherited(subclass)
super
subclass.instance_variable_set(:@filter_methods, @filter_methods.dup)
end
def filter_method_names
filter_methods.map(&:to_s).to_a
end
private private
def filter_methods def filter_methods
+1 -58
View File
@@ -1,11 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs
# @liquid_type object
# @liquid_name tablerowloop
# @liquid_summary
# Information about a parent [`tablerow` loop](/docs/api/liquid/tags/tablerow).
class TablerowloopDrop < Drop class TablerowloopDrop < Drop
def initialize(length, cols) def initialize(length, cols)
@length = length @length = length
@@ -15,92 +10,40 @@ module Liquid
@index = 0 @index = 0
end end
# @liquid_public_docs attr_reader :length, :col, :row
# @liquid_summary
# The total number of iterations in the loop.
# @liquid_return [number]
attr_reader :length
# @liquid_public_docs
# @liquid_summary
# The 1-based index of the current column.
# @liquid_return [number]
attr_reader :col
# @liquid_public_docs
# @liquid_summary
# The 1-based index of current row.
# @liquid_return [number]
attr_reader :row
# @liquid_public_docs
# @liquid_summary
# The 1-based index of the current iteration.
# @liquid_return [number]
def index def index
@index + 1 @index + 1
end end
# @liquid_public_docs
# @liquid_summary
# The 0-based index of the current iteration.
# @liquid_return [number]
def index0 def index0
@index @index
end end
# @liquid_public_docs
# @liquid_summary
# The 0-based index of the current column.
# @liquid_return [number]
def col0 def col0
@col - 1 @col - 1
end end
# @liquid_public_docs
# @liquid_summary
# The 1-based index of the current iteration, in reverse order.
# @liquid_return [number]
def rindex def rindex
@length - @index @length - @index
end end
# @liquid_public_docs
# @liquid_summary
# The 0-based index of the current iteration, in reverse order.
# @liquid_return [number]
def rindex0 def rindex0
@length - @index - 1 @length - @index - 1
end end
# @liquid_public_docs
# @liquid_summary
# Returns `true` if the current iteration is the first. Returns `false` if not.
# @liquid_return [boolean]
def first def first
@index == 0 @index == 0
end end
# @liquid_public_docs
# @liquid_summary
# Returns `true` if the current iteration is the last. Returns `false` if not.
# @liquid_return [boolean]
def last def last
@index == @length - 1 @index == @length - 1
end end
# @liquid_public_docs
# @liquid_summary
# Returns `true` if the current column is the first in the row. Returns `false` if not.
# @liquid_return [boolean]
def col_first def col_first
@col == 1 @col == 1
end end
# @liquid_public_docs
# @liquid_summary
# Returns `true` if the current column is the last in the row. Returns `false` if not.
# @liquid_return [boolean]
def col_last def col_last
@col == @cols @col == @cols
end end
+2 -8
View File
@@ -14,18 +14,12 @@ module Liquid
end end
def disable_tags(*tag_names) def disable_tags(*tag_names)
tag_names += disabled_tags @disabled_tags ||= []
define_singleton_method(:disabled_tags) { tag_names } @disabled_tags.concat(tag_names)
prepend(Disabler) prepend(Disabler)
end end
private :new private :new
protected
def disabled_tags
[]
end
end end
def initialize(tag_name, markup, parse_context) def initialize(tag_name, markup, parse_context)
+8
View File
@@ -3,6 +3,14 @@
module Liquid module Liquid
class Tag class Tag
module Disabler module Disabler
module ClassMethods
attr_reader :disabled_tags
end
def self.prepended(base)
base.extend(ClassMethods)
end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
context.with_disabled_tags(self.class.disabled_tags) do context.with_disabled_tags(self.class.disabled_tags) do
super super
+8 -12
View File
@@ -1,18 +1,14 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs # Assign sets a variable in your template.
# @liquid_type tag #
# @liquid_category variable # {% assign foo = 'monkey' %}
# @liquid_name assign #
# @liquid_summary # You can then use the variable later in the page.
# Creates a new variable. #
# @liquid_description # {{ foo }}
# You can create variables of any [basic type](/docs/api/liquid/basics#types), [object](/docs/api/liquid/objects), or object property. #
# @liquid_syntax
# {% assign variable_name = value %}
# @liquid_syntax_keyword variable_name The name of the variable being created.
# @liquid_syntax_keyword value The value you want to assign to the variable.
class Assign < Tag class Assign < Tag
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
-8
View File
@@ -10,14 +10,6 @@ module Liquid
# {% endif %} # {% endif %}
# {% endfor %} # {% endfor %}
# #
# @liquid_public_docs
# @liquid_type tag
# @liquid_category iteration
# @liquid_name break
# @liquid_summary
# Stops a [`for` loop](/docs/api/liquid/tags/for) from iterating.
# @liquid_syntax
# {% break %}
class Break < Tag class Break < Tag
INTERRUPT = BreakInterrupt.new.freeze INTERRUPT = BreakInterrupt.new.freeze
+10 -13
View File
@@ -1,20 +1,17 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs # Capture stores the result of a block into a variable without rendering it inplace.
# @liquid_type tag #
# @liquid_category variable # {% capture heading %}
# @liquid_name capture # Monkeys!
# @liquid_summary
# Creates a new variable with a string value.
# @liquid_description
# You can create complex strings with Liquid logic and variables.
# @liquid_syntax
# {% capture variable %}
# value
# {% endcapture %} # {% endcapture %}
# @liquid_syntax_keyword variable The name of the variable being created. # ...
# @liquid_syntax_keyword value The value you want to assign to the variable. # <h1>{{ heading }}</h1>
#
# Capture is useful for saving content for use later in your template, such as
# in a sidebar or footer.
#
class Capture < Block class Capture < Block
Syntax = /(#{VariableSignature}+)/o Syntax = /(#{VariableSignature}+)/o
+1 -29
View File
@@ -1,27 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category conditional
# @liquid_name case
# @liquid_summary
# Renders a specific expression depending on the value of a specific variable.
# @liquid_syntax
# {% case variable %}
# {% when first_value %}
# first_expression
# {% when second_value %}
# second_expression
# {% else %}
# third_expression
# {% endcase %}
# @liquid_syntax_keyword variable The name of the variable you want to base your case statement on.
# @liquid_syntax_keyword first_value A specific value to check for.
# @liquid_syntax_keyword second_value A specific value to check for.
# @liquid_syntax_keyword first_expression An expression to be rendered when the variable's value matches `first_value`.
# @liquid_syntax_keyword second_expression An expression to be rendered when the variable's value matches `second_value`.
# @liquid_syntax_keyword third_expression An expression to be rendered when the variable's value has no match.
class Case < Block class Case < Block
Syntax = /(#{QuotedFragment})/o Syntax = /(#{QuotedFragment})/o
WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om
@@ -73,14 +52,7 @@ module Liquid
@blocks.each do |block| @blocks.each do |block|
if block.else? if block.else?
block.attachment.render_to_output_buffer(context, output) if execute_else_block block.attachment.render_to_output_buffer(context, output) if execute_else_block
next elsif block.evaluate(context)
end
result = Liquid::Utils.to_liquid_value(
block.evaluate(context),
)
if result
execute_else_block = false execute_else_block = false
block.attachment.render_to_output_buffer(context, output) block.attachment.render_to_output_buffer(context, output)
end end
-13
View File
@@ -1,19 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category syntax
# @liquid_name comment
# @liquid_summary
# Prevents an expression from being rendered or output.
# @liquid_description
# Any text inside `comment` tags won't be output, and any Liquid code will be parsed, but not executed.
# @liquid_syntax
# {% comment %}
# content
# {% endcomment %}
# @liquid_syntax_keyword content The content of the comment.
class Comment < Block class Comment < Block
def render_to_output_buffer(_context, output) def render_to_output_buffer(_context, output)
output output
+9 -8
View File
@@ -1,14 +1,15 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs # Continue tag to be used to break out of a for loop.
# @liquid_type tag #
# @liquid_category iteration # == Basic Usage:
# @liquid_name continue # {% for item in collection %}
# @liquid_summary # {% if item.condition %}
# Causes a [`for` loop](/docs/api/liquid/tags/for) to skip to the next iteration. # {% continue %}
# @liquid_syntax # {% endif %}
# {% continue %} # {% endfor %}
#
class Continue < Tag class Continue < Tag
INTERRUPT = ContinueInterrupt.new.freeze INTERRUPT = ContinueInterrupt.new.freeze
+11 -12
View File
@@ -1,19 +1,18 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs # Cycle is usually used within a loop to alternate between values, like colors or DOM classes.
# @liquid_type tag #
# @liquid_category iteration # {% for item in items %}
# @liquid_name cycle # <div class="{% cycle 'red', 'green', 'blue' %}"> {{ item }} </div>
# @liquid_summary # {% end %}
# Loops through a group of strings and outputs them one at a time for each iteration of a [`for` loop](/docs/api/liquid/tags/for). #
# @liquid_description # <div class="red"> Item one </div>
# The `cycle` tag must be used inside a `for` loop. # <div class="green"> Item two </div>
# <div class="blue"> Item three </div>
# <div class="red"> Item four </div>
# <div class="green"> Item five</div>
# #
# > Tip:
# > Use the `cycle` tag to output text in a predictable pattern. For example, to apply odd/even classes to rows in a table.
# @liquid_syntax
# {% cycle string, string, ... %}
class Cycle < Tag class Cycle < Tag
SimpleSyntax = /\A#{QuotedFragment}+/o SimpleSyntax = /\A#{QuotedFragment}+/o
NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om
+20 -22
View File
@@ -1,36 +1,34 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs # decrement is used in a place where one needs to insert a counter
# @liquid_type tag # into a template, and needs the counter to survive across
# @liquid_category variable # multiple instantiations of the template.
# @liquid_name decrement # NOTE: decrement is a pre-decrement, --i,
# @liquid_summary # while increment is post: i++.
# Creates a new variable, with a default value of -1, that's decreased by 1 with each subsequent call.
# @liquid_description
# Variables that are declared with `decrement` are unique to the [layout](/themes/architecture/layouts), [template](/themes/architecture/templates),
# or [section](/themes/architecture/sections) file that they're created in. However, the variable is shared across
# [snippets](/themes/architecture#snippets) included in the file.
# #
# Similarly, variables that are created with `decrement` are independent from those created with [`assign`](/docs/api/liquid/tags/assign) # (To achieve the survival, the application must keep the context)
# and [`capture`](/docs/api/liquid/tags/capture). However, `decrement` and [`increment`](/docs/api/liquid/tags/increment) share #
# variables. # if the variable does not exist, it is created with value 0.
# @liquid_syntax
# {% decrement variable_name %}
# @liquid_syntax_keyword variable_name The name of the variable being decremented.
class Decrement < Tag
attr_reader :variable_name
# Hello: {% decrement variable %}
#
# gives you:
#
# Hello: -1
# Hello: -2
# Hello: -3
#
class Decrement < Tag
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
@variable_name = markup.strip @variable = markup.strip
end end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
counter_environment = context.environments.first value = context.environments.first[@variable] ||= 0
value = counter_environment[@variable_name] || 0
value -= 1 value -= 1
counter_environment[@variable_name] = value context.environments.first[@variable] = value
output << value.to_s output << value.to_s
output output
end end
+9 -24
View File
@@ -1,26 +1,17 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs # Echo outputs an expression
# @liquid_type tag #
# @liquid_category syntax # {% echo monkey %}
# @liquid_name echo # {% echo user.name %}
# @liquid_summary #
# Outputs an expression. # This is identical to variable output syntax, like {{ foo }}, but works
# @liquid_description # inside {% liquid %} tags. The full syntax is supported, including filters:
# Using the `echo` tag is the same as wrapping an expression in curly brackets (`{{` and `}}`). However, unlike the curly #
# bracket method, you can use the `echo` tag inside [`liquid` tags](/docs/api/liquid/tags/liquid). # {% echo user | link %}
# #
# > Tip:
# > You can use [filters](/docs/api/liquid/filters) on expressions inside `echo` tags.
# @liquid_syntax
# {% liquid
# echo expression
# %}
# @liquid_syntax_keyword expression The expression to be output.
class Echo < Tag class Echo < Tag
attr_reader :variable
def initialize(tag_name, markup, parse_context) def initialize(tag_name, markup, parse_context)
super super
@variable = Variable.new(markup, parse_context) @variable = Variable.new(markup, parse_context)
@@ -29,12 +20,6 @@ module Liquid
def render(context) def render(context)
@variable.render_to_output_buffer(context, +'') @variable.render_to_output_buffer(context, +'')
end end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[@node.variable]
end
end
end end
Template.register_tag('echo', Echo) Template.register_tag('echo', Echo)
+46 -25
View File
@@ -1,29 +1,50 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs # "For" iterates over an array or collection.
# @liquid_type tag # Several useful variables are available to you within the loop.
# @liquid_category iteration #
# @liquid_name for # == Basic usage:
# @liquid_summary # {% for item in collection %}
# Renders an expression for every item in an array. # {{ forloop.index }}: {{ item.name }}
# @liquid_description # {% endfor %}
# You can do a maximum of 50 iterations with a `for` loop. If you need to iterate over more than 50 items, then use the #
# [`paginate` tag](/docs/api/liquid/tags/paginate) to split the items over multiple pages. # == Advanced usage:
# {% for item in collection %}
# <div {% if forloop.first %}class="first"{% endif %}>
# Item {{ forloop.index }}: {{ item.name }}
# </div>
# {% else %}
# There is nothing in the collection.
# {% endfor %}
#
# You can also define a limit and offset much like SQL. Remember
# that offset starts at 0 for the first item.
#
# {% for item in collection limit:5 offset:10 %}
# {{ item.name }}
# {% end %}
#
# To reverse the for loop simply use {% for item in collection reversed %} (note that the flag's spelling is different to the filter `reverse`)
#
# == Available variables:
#
# forloop.name:: 'item-collection'
# forloop.length:: Length of the loop
# forloop.index:: The current item's position in the collection;
# forloop.index starts at 1.
# This is helpful for non-programmers who start believe
# the first item in an array is 1, not 0.
# forloop.index0:: The current item's position in the collection
# where the first item is 0
# forloop.rindex:: Number of items remaining in the loop
# (length - index) where 1 is the last item.
# forloop.rindex0:: Number of items remaining in the loop
# where 0 is the last item.
# forloop.first:: Returns true if the item is the first item.
# forloop.last:: Returns true if the item is the last item.
# forloop.parentloop:: Provides access to the parent loop, if present.
# #
# > Tip:
# > Every `for` loop has an associated [`forloop` object](/docs/api/liquid/objects/forloop) with information about the loop.
# @liquid_syntax
# {% for variable in array %}
# expression
# {% endfor %}
# @liquid_syntax_keyword variable The current item in the array.
# @liquid_syntax_keyword array The array to iterate over.
# @liquid_syntax_keyword expression The expression to render for each iteration.
# @liquid_optional_param limit [number] The number of iterations to perform.
# @liquid_optional_param offset [number] The 1-based index to start iterating at.
# @liquid_optional_param range [untyped] A custom numeric range to iterate over.
# @liquid_optional_param reversed [untyped] Iterate in reverse order.
class For < Block class For < Block
Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
@@ -98,12 +119,11 @@ module Liquid
@name = "#{@variable_name}-#{collection_name}" @name = "#{@variable_name}-#{collection_name}"
@reversed = p.id?('reversed') @reversed = p.id?('reversed')
while p.look(:comma) || p.look(:id) while p.look(:id) && p.look(:colon, 1)
p.consume?(:comma)
unless (attribute = p.id?('limit') || p.id?('offset')) unless (attribute = p.id?('limit') || p.id?('offset'))
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_attribute") raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_attribute")
end end
p.consume(:colon) p.consume
set_attribute(attribute, p.expression) set_attribute(attribute, p.expression)
end end
p.consume(:end_of_string) p.consume(:end_of_string)
@@ -178,6 +198,7 @@ module Liquid
case key case key
when 'offset' when 'offset'
@from = if expr == 'continue' @from = if expr == 'continue'
Usage.increment('for_offset_continue')
:continue :continue
else else
parse_expression(expr) parse_expression(expr)
+10 -16
View File
@@ -1,18 +1,16 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs # If is the conditional block
# @liquid_type tag #
# @liquid_category conditional # {% if user.admin %}
# @liquid_name if # Admin user!
# @liquid_summary # {% else %}
# Renders an expression if a specific condition is `true`. # Not admin user
# @liquid_syntax
# {% if condition %}
# expression
# {% endif %} # {% endif %}
# @liquid_syntax_keyword condition The condition to evaluate. #
# @liquid_syntax_keyword expression The expression to render if the condition is met. # There are {% if count < 5 %} less {% else %} more {% endif %} items than you need.
#
class If < Block class If < Block
Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o 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 ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o
@@ -52,11 +50,7 @@ module Liquid
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
@blocks.each do |block| @blocks.each do |block|
result = Liquid::Utils.to_liquid_value( if block.evaluate(context)
block.evaluate(context),
)
if result
return block.attachment.render_to_output_buffer(context, output) return block.attachment.render_to_output_buffer(context, output)
end end
end end
+17 -21
View File
@@ -1,22 +1,20 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs # Include allows templates to relate with other templates
# @liquid_type tag #
# @liquid_category theme # Simply include another template:
# @liquid_name include #
# @liquid_summary # {% include 'product' %}
# Renders a [snippet](/themes/architecture#snippets). #
# @liquid_description # Include a template with a local variable:
# Inside the snippet, you can access and alter variables that are [created](/docs/api/liquid/tags/variable-tags) outside of the #
# snippet. # {% include 'product' with products[0] %}
# @liquid_syntax #
# {% include 'filename' %} # Include a template for a collection:
# @liquid_syntax_keyword filename The name of the snippet to render, without the `.liquid` extension. #
# @liquid_deprecated # {% include 'product' for products %}
# Deprecated because the way that variables are handled reduces performance and makes code harder to both read and maintain.
# #
# The `include` tag has been replaced by [`render`](/docs/api/liquid/tags/render).
class Include < Tag class Include < Tag
prepend Tag::Disableable prepend Tag::Disableable
@@ -52,12 +50,12 @@ module Liquid
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
template_name = context.evaluate(@template_name_expr) template_name = context.evaluate(@template_name_expr)
raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name.is_a?(String) raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name
partial = PartialCache.load( partial = PartialCache.load(
template_name, template_name,
context: context, context: context,
parse_context: parse_context, parse_context: parse_context
) )
context_variable_name = @alias_name || template_name.split('/').last context_variable_name = @alias_name || template_name.split('/').last
@@ -70,11 +68,9 @@ module Liquid
old_template_name = context.template_name old_template_name = context.template_name
old_partial = context.partial old_partial = context.partial
begin begin
context.template_name = partial.name context.template_name = template_name
context.partial = true context.partial = true
context.stack do context.stack do
@attributes.each do |key, value| @attributes.each do |key, value|
context[key] = context.evaluate(value) context[key] = context.evaluate(value)
+17 -22
View File
@@ -1,35 +1,30 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs # increment is used in a place where one needs to insert a counter
# @liquid_type tag # into a template, and needs the counter to survive across
# @liquid_category variable # multiple instantiations of the template.
# @liquid_name increment # (To achieve the survival, the application must keep the context)
# @liquid_summary #
# Creates a new variable, with a default value of 0, that's increased by 1 with each subsequent call. # if the variable does not exist, it is created with value 0.
# @liquid_description #
# Variables that are declared with `increment` are unique to the [layout](/themes/architecture/layouts), [template](/themes/architecture/templates), # Hello: {% increment variable %}
# or [section](/themes/architecture/sections) file that they're created in. However, the variable is shared across #
# [snippets](/themes/architecture#snippets) included in the file. # gives you:
#
# Hello: 0
# Hello: 1
# Hello: 2
# #
# Similarly, variables that are created with `increment` are independent from those created with [`assign`](/docs/api/liquid/tags/assign)
# and [`capture`](/docs/api/liquid/tags/capture). However, `increment` and [`decrement`](/docs/api/liquid/tags/decrement) share
# variables.
# @liquid_syntax
# {% increment variable_name %}
# @liquid_syntax_keyword variable_name The name of the variable being incremented.
class Increment < Tag class Increment < Tag
attr_reader :variable_name
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
@variable_name = markup.strip @variable = markup.strip
end end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
counter_environment = context.environments.first value = context.environments.first[@variable] ||= 0
value = counter_environment[@variable_name] || 0 context.environments.first[@variable] = value + 1
counter_environment[@variable_name] = value + 1
output << value.to_s output << value.to_s
output output
+2 -7
View File
@@ -4,15 +4,10 @@ module Liquid
class InlineComment < Tag class InlineComment < Tag
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
# Semantically, a comment should only ignore everything after it on the line. # Semantically, a comment should only ignore everything after it on the line.
# Currently, this implementation doesn't support mixing a comment with another tag # Currently, this implementation doesn't support mixing a comment with another tag
# but we need to reserve future support for this and prevent the introduction # but we need to reserve future support for this.
# of inline comments from being backward incompatible change. if markup.match?(/\n\s*[^#]/)
#
# As such, we're forcing users to put a # symbol on every line otherwise this
# tag will throw an error.
if markup.match?(/\n\s*[^#\s]/)
raise SyntaxError, options[:locale].t("errors.syntax.inline_comment_invalid") raise SyntaxError, options[:locale].t("errors.syntax.inline_comment_invalid")
end end
end end
+1 -12
View File
@@ -1,20 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category syntax
# @liquid_name raw
# @liquid_summary
# Outputs any Liquid code as text instead of rendering it.
# @liquid_syntax
# {% raw %}
# expression
# {% endraw %}
# @liquid_syntax_keyword expression The expression to be output without being rendered.
class Raw < Block class Raw < Block
Syntax = /\A\s*\z/ Syntax = /\A\s*\z/
FullTokenPossiblyInvalid = /\A(.*)#{TagStart}#{WhitespaceControl}?\s*(\w+)\s*(.*)?#{WhitespaceControl}?#{TagEnd}\z/om FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
def initialize(tag_name, markup, parse_context) def initialize(tag_name, markup, parse_context)
super super
+8 -37
View File
@@ -1,37 +1,13 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category theme
# @liquid_name render
# @liquid_summary
# Renders a [snippet](/themes/architecture#snippets) or [app block](/themes/architecture/sections/section-schema#render-app-blocks).
# @liquid_description
# Inside snippets and app blocks, you can't directly access variables that are [created](/docs/api/liquid/tags/variable-tags) outside
# of the snippet or app block. However, you can [specify variables as parameters](/docs/api/liquid/tags/render#render-passing-variables-to-a-snippet)
# to pass outside variables to snippets.
#
# While you can't directly access created variables, you can access global objects, as well as any objects that are
# directly accessible outside the snippet or app block. For example, a snippet or app block inside the [product template](/themes/architecture/templates/product)
# can access the [`product` object](/docs/api/liquid/objects/product), and a snippet or app block inside a [section](/themes/architecture/sections)
# can access the [`section` object](/docs/api/liquid/objects/section).
#
# Outside a snippet or app block, you can't access variables created inside the snippet or app block.
#
# > Note:
# > When you render a snippet using the `render` tag, you can't use the [`include` tag](/docs/api/liquid/tags/include)
# > inside the snippet.
# @liquid_syntax
# {% render 'filename' %}
# @liquid_syntax_keyword filename The name of the snippet to render, without the `.liquid` extension.
class Render < Tag class Render < Tag
FOR = 'for' FOR = 'for'
SYNTAX = /(#{QuotedString}+)(\s+(with|#{FOR})\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o SYNTAX = /(#{QuotedString}+)(\s+(with|#{FOR})\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
disable_tags "include" disable_tags "include"
attr_reader :template_name_expr, :variable_name_expr, :attributes, :alias_name attr_reader :template_name_expr, :attributes
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
@@ -45,7 +21,7 @@ module Liquid
@alias_name = Regexp.last_match(6) @alias_name = Regexp.last_match(6)
@variable_name_expr = variable_name ? parse_expression(variable_name) : nil @variable_name_expr = variable_name ? parse_expression(variable_name) : nil
@template_name_expr = parse_expression(template_name) @template_name_expr = parse_expression(template_name)
@is_for_loop = (with_or_for == FOR) @for = (with_or_for == FOR)
@attributes = {} @attributes = {}
markup.scan(TagAttributes) do |key, value| markup.scan(TagAttributes) do |key, value|
@@ -53,30 +29,26 @@ module Liquid
end end
end end
def for_loop?
@is_for_loop
end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
render_tag(context, output) render_tag(context, output)
end end
def render_tag(context, output) def render_tag(context, output)
# The expression should be a String literal, which parses to a String object # Though we evaluate this here we will only ever parse it as a string literal.
template_name = @template_name_expr template_name = context.evaluate(@template_name_expr)
raise ::ArgumentError unless template_name.is_a?(String) raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name
partial = PartialCache.load( partial = PartialCache.load(
template_name, template_name,
context: context, context: context,
parse_context: parse_context, parse_context: parse_context
) )
context_variable_name = @alias_name || template_name.split('/').last context_variable_name = @alias_name || template_name.split('/').last
render_partial_func = ->(var, forloop) { render_partial_func = ->(var, forloop) {
inner_context = context.new_isolated_subcontext inner_context = context.new_isolated_subcontext
inner_context.template_name = partial.name inner_context.template_name = template_name
inner_context.partial = true inner_context.partial = true
inner_context['forloop'] = forloop if forloop inner_context['forloop'] = forloop if forloop
@@ -89,7 +61,7 @@ module Liquid
} }
variable = @variable_name_expr ? context.evaluate(@variable_name_expr) : nil variable = @variable_name_expr ? context.evaluate(@variable_name_expr) : nil
if @is_for_loop && variable.respond_to?(:each) && variable.respond_to?(:count) if @for && variable.respond_to?(:each) && variable.respond_to?(:count)
forloop = Liquid::ForloopDrop.new(template_name, variable.count, nil) forloop = Liquid::ForloopDrop.new(template_name, variable.count, nil)
variable.each { |var| render_partial_func.call(var, forloop) } variable.each { |var| render_partial_func.call(var, forloop) }
else else
@@ -103,7 +75,6 @@ module Liquid
def children def children
[ [
@node.template_name_expr, @node.template_name_expr,
@node.variable_name_expr,
] + @node.attributes.values ] + @node.attributes.values
end end
end end
+3 -33
View File
@@ -1,28 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category iteration
# @liquid_name tablerow
# @liquid_summary
# Generates HTML table rows for every item in an array.
# @liquid_description
# The `tablerow` tag must be wrapped in HTML `<table>` and `</table>` tags.
#
# > Tip:
# > Every `tablerow` loop has an associated [`tablerowloop` object](/docs/api/liquid/objects/tablerowloop) with information about the loop.
# @liquid_syntax
# {% tablerow variable in array %}
# expression
# {% endtablerow %}
# @liquid_syntax_keyword variable The current item in the array.
# @liquid_syntax_keyword array The array to iterate over.
# @liquid_syntax_keyword expression The expression to render.
# @liquid_optional_param cols [number] The number of columns that the table should have.
# @liquid_optional_param limit [number] The number of iterations to perform.
# @liquid_optional_param offset [number] The 1-based index to start iterating at.
# @liquid_optional_param range [untyped] A custom numeric range to iterate over.
class TableRow < Block class TableRow < Block
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
@@ -45,13 +23,13 @@ module Liquid
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
(collection = context.evaluate(@collection_name)) || (return '') (collection = context.evaluate(@collection_name)) || (return '')
from = @attributes.key?('offset') ? to_integer(context.evaluate(@attributes['offset'])) : 0 from = @attributes.key?('offset') ? context.evaluate(@attributes['offset']).to_i : 0
to = @attributes.key?('limit') ? from + to_integer(context.evaluate(@attributes['limit'])) : nil to = @attributes.key?('limit') ? from + context.evaluate(@attributes['limit']).to_i : nil
collection = Utils.slice_collection(collection, from, to) collection = Utils.slice_collection(collection, from, to)
length = collection.length length = collection.length
cols = @attributes.key?('cols') ? to_integer(context.evaluate(@attributes['cols'])) : length cols = context.evaluate(@attributes['cols']).to_i
output << "<tr class=\"row1\">\n" output << "<tr class=\"row1\">\n"
context.stack do context.stack do
@@ -82,14 +60,6 @@ module Liquid
super + @node.attributes.values + [@node.collection_name] super + @node.attributes.values + [@node.collection_name]
end end
end end
private
def to_integer(value)
value.to_i
rescue NoMethodError
raise Liquid::ArgumentError, "invalid integer"
end
end end
Template.register_tag('tablerow', TableRow) Template.register_tag('tablerow', TableRow)
+6 -25
View File
@@ -3,40 +3,21 @@
require_relative 'if' require_relative 'if'
module Liquid module Liquid
# @liquid_public_docs # Unless is a conditional just like 'if' but works on the inverse logic.
# @liquid_type tag #
# @liquid_category conditional # {% unless x < 0 %} x is greater than zero {% endunless %}
# @liquid_name unless #
# @liquid_summary
# Renders an expression unless a specific condition is `true`.
# @liquid_description
# > Tip:
# > Similar to the [`if` tag](/docs/api/liquid/tags/if), you can use `elsif` to add more conditions to an `unless` tag.
# @liquid_syntax
# {% unless condition %}
# expression
# {% endunless %}
# @liquid_syntax_keyword condition The condition to evaluate.
# @liquid_syntax_keyword expression The expression to render unless the condition is met.
class Unless < If class Unless < If
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
# First condition is interpreted backwards ( if not ) # First condition is interpreted backwards ( if not )
first_block = @blocks.first first_block = @blocks.first
result = Liquid::Utils.to_liquid_value( unless first_block.evaluate(context)
first_block.evaluate(context),
)
unless result
return first_block.attachment.render_to_output_buffer(context, output) return first_block.attachment.render_to_output_buffer(context, output)
end end
# After the first condition unless works just like if # After the first condition unless works just like if
@blocks[1..-1].each do |block| @blocks[1..-1].each do |block|
result = Liquid::Utils.to_liquid_value( if block.evaluate(context)
block.evaluate(context),
)
if result
return block.attachment.render_to_output_buffer(context, output) return block.attachment.render_to_output_buffer(context, output)
end end
end end
+4 -5
View File
@@ -15,7 +15,7 @@ module Liquid
# template.render('user_name' => 'bob') # template.render('user_name' => 'bob')
# #
class Template class Template
attr_accessor :root, :name attr_accessor :root
attr_reader :resource_limits, :warnings attr_reader :resource_limits, :warnings
class TagRegistry class TagRegistry
@@ -167,14 +167,15 @@ module Liquid
output = nil output = nil
context_register = context.registers.is_a?(StaticRegisters) ? context.registers.static : context.registers
case args.last case args.last
when Hash when Hash
options = args.pop options = args.pop
output = options[:output] if options[:output] output = options[:output] if options[:output]
static_registers = context.registers.static
options[:registers]&.each do |key, register| options[:registers]&.each do |key, register|
static_registers[key] = register context_register[key] = register
end end
apply_options_to_context(context, options) apply_options_to_context(context, options)
@@ -189,8 +190,6 @@ module Liquid
@profiler = context.profiler = Liquid::Profiler.new @profiler = context.profiler = Liquid::Profiler.new
end end
context.template_name ||= name
begin begin
# render the nodelist. # render the nodelist.
@root.render_to_output_buffer(context, output || +'') @root.render_to_output_buffer(context, output || +'')
+4 -10
View File
@@ -5,18 +5,14 @@ module Liquid
attr_reader :line_number, :for_liquid_tag attr_reader :line_number, :for_liquid_tag
def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false) 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) @line_number = line_number || (line_numbers ? 1 : nil)
@for_liquid_tag = for_liquid_tag @for_liquid_tag = for_liquid_tag
@offset = 0
@tokens = tokenize @tokens = tokenize
end end
def shift def shift
token = @tokens[@offset] (token = @tokens.shift) || return
return nil unless token
@offset += 1
if @line_number if @line_number
@line_number += @for_liquid_tag ? 1 : token.count("\n") @line_number += @for_liquid_tag ? 1 : token.count("\n")
@@ -28,16 +24,14 @@ module Liquid
private private
def tokenize def tokenize
return [] if @source.empty? return [] if @source.to_s.empty?
return @source.split("\n") if @for_liquid_tag return @source.split("\n") if @for_liquid_tag
tokens = @source.split(TemplateParser) tokens = @source.split(TemplateParser)
# removes the rogue empty element at the beginning of the array # removes the rogue empty element at the beginning of the array
if tokens[0]&.empty? tokens.shift if tokens[0]&.empty?
@offset += 1
end
tokens tokens
end end
-8
View File
@@ -81,13 +81,5 @@ module Liquid
rescue ::ArgumentError rescue ::ArgumentError
nil nil
end 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
end end
+4 -4
View File
@@ -47,7 +47,7 @@ module Liquid
name_markup = Regexp.last_match(1) name_markup = Regexp.last_match(1)
filter_markup = Regexp.last_match(2) filter_markup = Regexp.last_match(2)
@name = parse_context.parse_expression(name_markup) @name = Expression.parse(name_markup)
if filter_markup =~ FilterMarkupRegex if filter_markup =~ FilterMarkupRegex
filters = Regexp.last_match(1).scan(FilterParser) filters = Regexp.last_match(1).scan(FilterParser)
filters.each do |f| filters.each do |f|
@@ -65,7 +65,7 @@ module Liquid
return if p.look(:end_of_string) return if p.look(:end_of_string)
@name = parse_context.parse_expression(p.expression) @name = Expression.parse(p.expression)
while p.consume?(:pipe) while p.consume?(:pipe)
filtername = p.consume(:id) filtername = p.consume(:id)
filterargs = p.consume?(:colon) ? parse_filterargs(p) : [] filterargs = p.consume?(:colon) ? parse_filterargs(p) : []
@@ -122,9 +122,9 @@ module Liquid
unparsed_args.each do |a| unparsed_args.each do |a|
if (matches = a.match(JustTagAttributes)) if (matches = a.match(JustTagAttributes))
keyword_args ||= {} keyword_args ||= {}
keyword_args[matches[1]] = parse_context.parse_expression(matches[2]) keyword_args[matches[1]] = Expression.parse(matches[2])
else else
filter_args << parse_context.parse_expression(a) filter_args << Expression.parse(a)
end end
end end
result = [filter_name, filter_args] result = [filter_name, filter_args]
+7 -13
View File
@@ -2,7 +2,8 @@
module Liquid module Liquid
class VariableLookup class VariableLookup
COMMAND_METHODS = ['size', 'first', 'last'].freeze SQUARE_BRACKETED = /\A\[(.*)\]\z/m
COMMAND_METHODS = ['size', 'first', 'last'].freeze
attr_reader :name, :lookups attr_reader :name, :lookups
@@ -14,8 +15,8 @@ module Liquid
lookups = markup.scan(VariableParser) lookups = markup.scan(VariableParser)
name = lookups.shift name = lookups.shift
if name&.start_with?('[') && name&.end_with?(']') if name =~ SQUARE_BRACKETED
name = Expression.parse(name[1..-2]) name = Expression.parse(Regexp.last_match(1))
end end
@name = name @name = name
@@ -24,18 +25,14 @@ module Liquid
@lookups.each_index do |i| @lookups.each_index do |i|
lookup = lookups[i] lookup = lookups[i]
if lookup&.start_with?('[') && lookup&.end_with?(']') if lookup =~ SQUARE_BRACKETED
lookups[i] = Expression.parse(lookup[1..-2]) lookups[i] = Expression.parse(Regexp.last_match(1))
elsif COMMAND_METHODS.include?(lookup) elsif COMMAND_METHODS.include?(lookup)
@command_flags |= 1 << i @command_flags |= 1 << i
end end
end end
end end
def lookup_command?(lookup_index)
@command_flags & (1 << lookup_index) != 0
end
def evaluate(context) def evaluate(context)
name = context.evaluate(@name) name = context.evaluate(@name)
object = context.find_variable(name) object = context.find_variable(name)
@@ -43,9 +40,6 @@ module Liquid
@lookups.each_index do |i| @lookups.each_index do |i|
key = context.evaluate(@lookups[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 # If object is a hash- or array-like object we look for the
# presence of the key and if its available we return it # presence of the key and if its available we return it
if object.respond_to?(:[]) && if object.respond_to?(:[]) &&
@@ -59,7 +53,7 @@ module Liquid
# Some special cases. If the part wasn't in square brackets and # Some special cases. If the part wasn't in square brackets and
# no key with the same name was found we interpret following calls # no key with the same name was found we interpret following calls
# as commands and call them on the current object # as commands and call them on the current object
elsif lookup_command?(i) && object.respond_to?(key) elsif @command_flags & (1 << i) != 0 && object.respond_to?(key)
object = object.send(key).to_liquid object = object.send(key).to_liquid
# No key was present with the desired value and it wasn't one of the directly supported # No key was present with the desired value and it wasn't one of the directly supported
+1 -1
View File
@@ -2,5 +2,5 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
VERSION = "5.4.0" VERSION = "5.0.0"
end end
+3 -2
View File
@@ -17,12 +17,13 @@ Gem::Specification.new do |s|
s.license = "MIT" s.license = "MIT"
# s.description = "A secure, non-evaling end user template engine with aesthetic markup." # s.description = "A secure, non-evaling end user template engine with aesthetic markup."
s.required_ruby_version = ">= 2.7.0" s.required_ruby_version = ">= 2.5.0"
s.required_rubygems_version = ">= 1.3.7" s.required_rubygems_version = ">= 1.3.7"
s.metadata['allowed_push_host'] = 'https://rubygems.org' s.metadata['allowed_push_host'] = 'https://rubygems.org'
s.files = Dir.glob("{lib}/**/*") + %w(LICENSE README.md) s.test_files = Dir.glob("{test}/**/*")
s.files = Dir.glob("{lib}/**/*") + %w(LICENSE README.md)
s.extra_rdoc_files = ["History.md", "README.md"] s.extra_rdoc_files = ["History.md", "README.md"]
+6 -9
View File
@@ -3,19 +3,11 @@
require 'yaml' require 'yaml'
module Database module Database
DATABASE_FILE_PATH = "#{__dir__}/vision.database.yml"
# Load the standard vision toolkit database and re-arrage it to be simply exportable # Load the standard vision toolkit database and re-arrage it to be simply exportable
# to liquid as assigns. All this is based on Shopify # to liquid as assigns. All this is based on Shopify
def self.tables def self.tables
@tables ||= begin @tables ||= begin
db = db = YAML.load_file("#{__dir__}/vision.database.yml")
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 # From vision source
db['products'].each do |product| db['products'].each do |product|
@@ -49,3 +41,8 @@ module Database
end end
end end
end end
if __FILE__ == $PROGRAM_NAME
p(Database.tables['collections']['frontpage'].keys)
# p Database.tables['blog']['articles']
end
+2 -2
View File
@@ -45,8 +45,8 @@ class Paginate < Liquid::Block
pagination['items'] = collection_size pagination['items'] = collection_size
pagination['pages'] = page_count - 1 pagination['pages'] = page_count - 1
pagination['previous'] = link('&laquo; Previous', current_page - 1) if 1 < current_page pagination['previous'] = link('&laquo; Previous', current_page - 1) unless 1 >= current_page
pagination['next'] = link('Next &raquo;', current_page + 1) if page_count > current_page + 1 pagination['next'] = link('Next &raquo;', current_page + 1) unless page_count <= current_page + 1
pagination['parts'] = [] pagination['parts'] = []
hellip_break = false hellip_break = false
+1 -1
View File
@@ -3,7 +3,7 @@
# This profiler run simulates Shopify. # This profiler run simulates Shopify.
# We are looking in the tests directory for liquid files and render them within the designated layout file. # We are looking in the tests directory for liquid files and render them within the designated layout file.
# We will also export a substantial database to liquid which the templates can render values of. # We will also export a substantial database to liquid which the templates can render values of.
# All this is to make the benchmark as non synthetic as possible. All templates and tests are lifted from # All this is to make the benchmark as non syntetic as possible. All templates and tests are lifted from
# direct real-world usage and the profiler measures code that looks very similar to the way it looks in # direct real-world usage and the profiler measures code that looks very similar to the way it looks in
# Shopify which is likely the biggest user of liquid in the world which something to the tune of several # Shopify which is likely the biggest user of liquid in the world which something to the tune of several
# million Template#render calls a day. # million Template#render calls a day.
+24 -24
View File
@@ -6,51 +6,51 @@ class AssignTest < Minitest::Test
include Liquid include Liquid
def test_assign_with_hyphen_in_variable_name def test_assign_with_hyphen_in_variable_name
template_source = <<~END_TEMPLATE template_source = <<-END_TEMPLATE
{% assign this-thing = 'Print this-thing' -%} {% assign this-thing = 'Print this-thing' %}
{{ this-thing -}} {{ this-thing }}
END_TEMPLATE END_TEMPLATE
assert_template_result("Print this-thing", template_source) template = Template.parse(template_source)
rendered = template.render!
assert_equal("Print this-thing", rendered.strip)
end end
def test_assigned_variable def test_assigned_variable
assert_template_result( assert_template_result('.foo.',
'.foo.',
'{% assign foo = values %}.{{ foo[0] }}.', '{% assign foo = values %}.{{ foo[0] }}.',
{ 'values' => %w(foo bar baz) }, 'values' => %w(foo bar baz))
)
assert_template_result( assert_template_result('.bar.',
'.bar.',
'{% assign foo = values %}.{{ foo[1] }}.', '{% assign foo = values %}.{{ foo[1] }}.',
{ 'values' => %w(foo bar baz) }, 'values' => %w(foo bar baz))
)
end end
def test_assign_with_filter def test_assign_with_filter
assert_template_result( assert_template_result('.bar.',
'.bar.',
'{% assign foo = values | split: "," %}.{{ foo[1] }}.', '{% assign foo = values | split: "," %}.{{ foo[1] }}.',
{ 'values' => "foo,bar,baz" }, 'values' => "foo,bar,baz")
)
end end
def test_assign_syntax_error def test_assign_syntax_error
assert_match_syntax_error(/assign/, '{% assign foo not values %}.') assert_match_syntax_error(/assign/,
'{% assign foo not values %}.',
'values' => "foo,bar,baz")
end end
def test_assign_uses_error_mode def test_assign_uses_error_mode
assert_match_syntax_error( with_error_mode(:strict) do
"Expected dotdot but found pipe in ", assert_raises(SyntaxError) do
"{% assign foo = ('X' | downcase) %}", Template.parse("{% assign foo = ('X' | downcase) %}")
error_mode: :strict, end
) end
assert_template_result("", "{% assign foo = ('X' | downcase) %}", error_mode: :lax) with_error_mode(:lax) do
assert(Template.parse("{% assign foo = ('X' | downcase) %}"))
end
end end
def test_expression_with_whitespace_in_square_brackets def test_expression_with_whitespace_in_square_brackets
source = "{% assign r = a[ 'b' ] %}{{ r }}" source = "{% assign r = a[ 'b' ] %}{{ r }}"
assert_template_result('result', source, { 'a' => { 'b' => 'result' } }) assert_template_result('result', source, 'a' => { 'b' => 'result' })
end end
def test_assign_score_exceeding_resource_limit def test_assign_score_exceeding_resource_limit
+13 -20
View File
@@ -9,6 +9,12 @@ class FoobarTag < Liquid::Tag
end end
end end
class BlankTestFileSystem
def read_template_file(template_path)
template_path
end
end
class BlankTest < Minitest::Test class BlankTest < Minitest::Test
include Liquid include Liquid
N = 10 N = 10
@@ -27,7 +33,7 @@ class BlankTest < Minitest::Test
def test_new_tags_are_not_blank_by_default def test_new_tags_are_not_blank_by_default
with_custom_tag('foobar', FoobarTag) do with_custom_tag('foobar', FoobarTag) do
assert_equal(" " * N, Liquid::Template.parse(wrap_in_for("{% foobar %}")).render!) assert_template_result(" " * N, wrap_in_for("{% foobar %}"))
end end
end end
@@ -57,11 +63,9 @@ class BlankTest < Minitest::Test
def test_nested_blocks_are_blank_but_only_if_all_children_are def test_nested_blocks_are_blank_but_only_if_all_children_are
assert_template_result("", wrap(wrap(" "))) assert_template_result("", wrap(wrap(" ")))
assert_template_result( assert_template_result("\n but this is not " * (N + 1),
"\n but this is not " * (N + 1),
wrap('{% if true %} {% comment %} this is blank {% endcomment %} {% endif %} wrap('{% if true %} {% comment %} this is blank {% endcomment %} {% endif %}
{% if true %} but this is not {% endif %}'), {% if true %} but this is not {% endif %}'))
)
end end
def test_assigns_are_blank def test_assigns_are_blank
@@ -91,21 +95,10 @@ class BlankTest < Minitest::Test
end end
def test_include_is_blank def test_include_is_blank
assert_template_result( Liquid::Template.file_system = BlankTestFileSystem.new
"foobar" * (N + 1), assert_template_result("foobar" * (N + 1), wrap("{% include 'foobar' %}"))
wrap("{% include 'foobar' %}"), assert_template_result(" foobar " * (N + 1), wrap("{% include ' foobar ' %}"))
partials: { 'foobar' => 'foobar' }, assert_template_result(" " * (N + 1), wrap(" {% include ' ' %} "))
)
assert_template_result(
" foobar " * (N + 1),
wrap("{% include ' foobar ' %}"),
partials: { ' foobar ' => ' foobar ' },
)
assert_template_result(
" " * (N + 1),
wrap(" {% include ' ' %} "),
partials: { ' ' => ' ' },
)
end end
def test_case_is_blank def test_case_is_blank
+4 -2
View File
@@ -6,8 +6,10 @@ class BlockTest < Minitest::Test
include Liquid include Liquid
def test_unexpected_end_tag def test_unexpected_end_tag
source = '{% if true %}{% endunless %}' exc = assert_raises(SyntaxError) do
assert_match_syntax_error("Liquid syntax error (line 1): 'endunless' is not a valid delimiter for if tags. use endif", source) Template.parse("{% if true %}{% endunless %}")
end
assert_equal(exc.message, "Liquid syntax error: 'endunless' is not a valid delimiter for if tags. use endif")
end end
def test_with_custom_tag def test_with_custom_tag
+29 -23
View File
@@ -10,38 +10,44 @@ class CaptureTest < Minitest::Test
end end
def test_capture_with_hyphen_in_variable_name def test_capture_with_hyphen_in_variable_name
template_source = <<~END_TEMPLATE template_source = <<-END_TEMPLATE
{% capture this-thing %}Print this-thing{% endcapture -%} {% capture this-thing %}Print this-thing{% endcapture %}
{{ this-thing -}} {{ this-thing }}
END_TEMPLATE END_TEMPLATE
assert_template_result("Print this-thing", template_source) template = Template.parse(template_source)
rendered = template.render!
assert_equal("Print this-thing", rendered.strip)
end end
def test_capture_to_variable_from_outer_scope_if_existing def test_capture_to_variable_from_outer_scope_if_existing
template_source = <<~END_TEMPLATE template_source = <<-END_TEMPLATE
{% assign var = '' -%} {% assign var = '' %}
{% if true -%} {% if true %}
{% capture var %}first-block-string{% endcapture -%} {% capture var %}first-block-string{% endcapture %}
{% endif -%} {% endif %}
{% if true -%} {% if true %}
{% capture var %}test-string{% endcapture -%} {% capture var %}test-string{% endcapture %}
{% endif -%} {% endif %}
{{var-}} {{var}}
END_TEMPLATE END_TEMPLATE
assert_template_result("test-string", template_source) template = Template.parse(template_source)
rendered = template.render!
assert_equal("test-string", rendered.gsub(/\s/, ''))
end end
def test_assigning_from_capture def test_assigning_from_capture
template_source = <<~END_TEMPLATE template_source = <<-END_TEMPLATE
{% assign first = '' -%} {% assign first = '' %}
{% assign second = '' -%} {% assign second = '' %}
{% for number in (1..3) -%} {% for number in (1..3) %}
{% capture first %}{{number}}{% endcapture -%} {% capture first %}{{number}}{% endcapture %}
{% assign second = first -%} {% assign second = first %}
{% endfor -%} {% endfor %}
{{ first }}-{{ second -}} {{ first }}-{{ second }}
END_TEMPLATE END_TEMPLATE
assert_template_result("3-3", template_source) template = Template.parse(template_source)
rendered = template.render!
assert_equal("3-3", rendered.gsub(/\s/, ''))
end end
def test_increment_assign_score_by_bytes_not_characters def test_increment_assign_score_by_bytes_not_characters
+107 -122
View File
@@ -24,7 +24,7 @@ class ContextSensitiveDrop < Liquid::Drop
end end
end end
class Category class Category < Liquid::Drop
attr_accessor :name attr_accessor :name
def initialize(name) def initialize(name)
@@ -36,9 +36,8 @@ class Category
end end
end end
class CategoryDrop < Liquid::Drop class CategoryDrop
attr_accessor :category, :context attr_accessor :category, :context
def initialize(category) def initialize(category)
@category = category @category = category
end end
@@ -102,7 +101,7 @@ class ContextTest < Minitest::Test
end end
def test_variables_not_existing def test_variables_not_existing
assert_template_result("true", "{% if does_not_exist == nil %}true{% endif %}") assert_nil(@context['does_not_exist'])
end end
def test_scoping def test_scoping
@@ -121,27 +120,22 @@ class ContextTest < Minitest::Test
end end
def test_length_query def test_length_query
assert_template_result( @context['numbers'] = [1, 2, 3, 4]
"true",
"{% if numbers.size == 4 %}true{% endif %}",
{ "numbers" => [1, 2, 3, 4] },
)
assert_template_result( assert_equal(4, @context['numbers.size'])
"true",
"{% if numbers.size == 4 %}true{% endif %}",
{ "numbers" => { 1 => 1, 2 => 2, 3 => 3, 4 => 4 } },
)
assert_template_result( @context['numbers'] = { 1 => 1, 2 => 2, 3 => 3, 4 => 4 }
"true",
"{% if numbers.size == 1000 %}true{% endif %}", assert_equal(4, @context['numbers.size'])
{ "numbers" => { 1 => 1, 2 => 2, 3 => 3, 4 => 4, 'size' => 1000 } },
) @context['numbers'] = { 1 => 1, 2 => 2, 3 => 3, 4 => 4, 'size' => 1000 }
assert_equal(1000, @context['numbers.size'])
end end
def test_hyphenated_variable def test_hyphenated_variable
assert_template_result("godz", "{{ oh-my }}", { "oh-my" => 'godz' }) @context['oh-my'] = 'godz'
assert_equal('godz', @context['oh-my'])
end end
def test_add_filter def test_add_filter
@@ -193,24 +187,24 @@ class ContextTest < Minitest::Test
end end
def test_hierachical_data def test_hierachical_data
assigns = { 'hash' => { "name" => 'tobi' } } @context['hash'] = { "name" => 'tobi' }
assert_template_result("tobi", "{{ hash.name }}", assigns) assert_equal('tobi', @context['hash.name'])
assert_template_result("tobi", '{{ hash["name"] }}', assigns) assert_equal('tobi', @context['hash["name"]'])
end end
def test_keywords def test_keywords
assert_template_result("pass", "{% if true == expect %}pass{% endif %}", { "expect" => true }) assert_equal(true, @context['true'])
assert_template_result("pass", "{% if false == expect %}pass{% endif %}", { "expect" => false }) assert_equal(false, @context['false'])
end end
def test_digits def test_digits
assert_template_result("pass", "{% if 100 == expect %}pass{% endif %}", { "expect" => 100 }) assert_equal(100, @context['100'])
assert_template_result("pass", "{% if 100.00 == expect %}pass{% endif %}", { "expect" => 100.00 }) assert_equal(100.00, @context['100.00'])
end end
def test_strings def test_strings
assert_template_result("hello!", '{{ "hello!" }}') assert_equal("hello!", @context['"hello!"'])
assert_template_result("hello!", "{{ 'hello!' }}") assert_equal("hello!", @context["'hello!'"])
end end
def test_merge def test_merge
@@ -222,92 +216,99 @@ class ContextTest < Minitest::Test
end end
def test_array_notation def test_array_notation
assigns = { "test" => ["a", "b"] } @context['test'] = [1, 2, 3, 4, 5]
assert_template_result("a", "{{ test[0] }}", assigns)
assert_template_result("b", "{{ test[1] }}", assigns) assert_equal(1, @context['test[0]'])
assert_template_result("pass", "{% if test[2] == nil %}pass{% endif %}", assigns) assert_equal(2, @context['test[1]'])
assert_equal(3, @context['test[2]'])
assert_equal(4, @context['test[3]'])
assert_equal(5, @context['test[4]'])
end end
def test_recoursive_array_notation def test_recoursive_array_notation
assigns = { "test" => { 'test' => [1, 2, 3, 4, 5] } } @context['test'] = { 'test' => [1, 2, 3, 4, 5] }
assert_template_result("1", "{{ test.test[0] }}", assigns)
assigns = { "test" => [{ 'test' => 'worked' }] } assert_equal(1, @context['test.test[0]'])
assert_template_result("worked", "{{ test[0].test }}", assigns)
@context['test'] = [{ 'test' => 'worked' }]
assert_equal('worked', @context['test[0].test'])
end end
def test_hash_to_array_transition def test_hash_to_array_transition
assigns = { @context['colors'] = {
'colors' => { 'Blue' => ['003366', '336699', '6699CC', '99CCFF'],
'Blue' => ['003366', '336699', '6699CC', '99CCFF'], 'Green' => ['003300', '336633', '669966', '99CC99'],
'Green' => ['003300', '336633', '669966', '99CC99'], 'Yellow' => ['CC9900', 'FFCC00', 'FFFF99', 'FFFFCC'],
'Yellow' => ['CC9900', 'FFCC00', 'FFFF99', 'FFFFCC'], 'Red' => ['660000', '993333', 'CC6666', 'FF9999'],
'Red' => ['660000', '993333', 'CC6666', 'FF9999'],
},
} }
assert_template_result("003366", "{{ colors.Blue[0] }}", assigns) assert_equal('003366', @context['colors.Blue[0]'])
assert_template_result("FF9999", "{{ colors.Red[3] }}", assigns) assert_equal('FF9999', @context['colors.Red[3]'])
end end
def test_try_first def test_try_first
assigns = { 'test' => [1, 2, 3, 4, 5] } @context['test'] = [1, 2, 3, 4, 5]
assert_template_result("1", "{{ test.first }}", assigns)
assert_template_result("pass", "{% if test.last == 5 %}pass{% endif %}", assigns)
assigns = { "test" => { "test" => [1, 2, 3, 4, 5] } } assert_equal(1, @context['test.first'])
assert_template_result("1", "{{ test.test.first }}", assigns) assert_equal(5, @context['test.last'])
assert_template_result("5", "{{ test.test.last }}", assigns)
assigns = { "test" => [1] } @context['test'] = { 'test' => [1, 2, 3, 4, 5] }
assert_template_result("1", "{{ test.first }}", assigns)
assert_template_result("1", "{{ test.last }}", assigns) assert_equal(1, @context['test.test.first'])
assert_equal(5, @context['test.test.last'])
@context['test'] = [1]
assert_equal(1, @context['test.first'])
assert_equal(1, @context['test.last'])
end end
def test_access_hashes_with_hash_notation def test_access_hashes_with_hash_notation
assigns = { 'products' => { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] } } @context['products'] = { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] }
assert_template_result("5", '{{ products["count"] }}', assigns) @context['product'] = { 'variants' => [{ 'title' => 'draft151cm' }, { 'title' => 'element151cm' }] }
assert_template_result("deepsnow", '{{ products["tags"][0] }}', assigns)
assert_template_result("deepsnow", '{{ products["tags"].first }}', assigns)
assigns = { 'product' => { 'variants' => [{ 'title' => 'draft151cm' }, { 'title' => 'element151cm' }] } } assert_equal(5, @context['products["count"]'])
assert_template_result("draft151cm", '{{ product["variants"][0]["title"] }}', assigns) assert_equal('deepsnow', @context['products["tags"][0]'])
assert_template_result("element151cm", '{{ product["variants"][1]["title"] }}', assigns) assert_equal('deepsnow', @context['products["tags"].first'])
assert_template_result("draft151cm", '{{ product["variants"].first["title"] }}', assigns) assert_equal('draft151cm', @context['product["variants"][0]["title"]'])
assert_template_result("element151cm", '{{ product["variants"].last["title"] }}', assigns) assert_equal('element151cm', @context['product["variants"][1]["title"]'])
assert_equal('draft151cm', @context['product["variants"][0]["title"]'])
assert_equal('element151cm', @context['product["variants"].last["title"]'])
end end
def test_access_variable_with_hash_notation def test_access_variable_with_hash_notation
assert_template_result('baz', '{{ ["foo"] }}', { "foo" => "baz" }) @context['foo'] = 'baz'
assert_template_result('baz', '{{ [bar] }}', { 'foo' => 'baz', 'bar' => 'foo' }) @context['bar'] = 'foo'
assert_equal('baz', @context['["foo"]'])
assert_equal('baz', @context['[bar]'])
end end
def test_access_hashes_with_hash_access_variables def test_access_hashes_with_hash_access_variables
assigns = { @context['var'] = 'tags'
'var' => 'tags', @context['nested'] = { 'var' => 'tags' }
'nested' => { 'var' => 'tags' }, @context['products'] = { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] }
'products' => { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] },
}
assert_template_result('deepsnow', '{{ products[var].first }}', assigns) assert_equal('deepsnow', @context['products[var].first'])
assert_template_result('freestyle', '{{ products[nested.var].last }}', assigns) assert_equal('freestyle', @context['products[nested.var].last'])
end end
def test_hash_notation_only_for_hash_access def test_hash_notation_only_for_hash_access
assigns = { "array" => [1, 2, 3, 4, 5] } @context['array'] = [1, 2, 3, 4, 5]
assert_template_result("1", "{{ array.first }}", assigns) @context['hash'] = { 'first' => 'Hello' }
assert_template_result("pass", '{% if array["first"] == nil %}pass{% endif %}', assigns)
assert_template_result("Hello", '{{ hash["first"] }}', { "hash" => { "first" => "Hello" } }) assert_equal(1, @context['array.first'])
assert_nil(@context['array["first"]'])
assert_equal('Hello', @context['hash["first"]'])
end end
def test_first_can_appear_in_middle_of_callchain def test_first_can_appear_in_middle_of_callchain
assigns = { "product" => { 'variants' => [{ 'title' => 'draft151cm' }, { 'title' => 'element151cm' }] } } @context['product'] = { 'variants' => [{ 'title' => 'draft151cm' }, { 'title' => 'element151cm' }] }
assert_template_result('draft151cm', '{{ product.variants[0].title }}', assigns) assert_equal('draft151cm', @context['product.variants[0].title'])
assert_template_result('element151cm', '{{ product.variants[1].title }}', assigns) assert_equal('element151cm', @context['product.variants[1].title'])
assert_template_result('draft151cm', '{{ product.variants.first.title }}', assigns) assert_equal('draft151cm', @context['product.variants.first.title'])
assert_template_result('element151cm', '{{ product.variants.last.title }}', assigns) assert_equal('element151cm', @context['product.variants.last.title'])
end end
def test_cents def test_cents
@@ -349,12 +350,10 @@ class ContextTest < Minitest::Test
end end
def test_ranges def test_ranges
assert_template_result("1..5", '{{ (1..5) }}') @context.merge("test" => '5')
assert_template_result("pass", '{% if (1..5) == expect %}pass{% endif %}', { "expect" => (1..5) }) assert_equal((1..5), @context['(1..5)'])
assert_equal((1..5), @context['(1..test)'])
assigns = { "test" => '5' } assert_equal((5..5), @context['(test..test)'])
assert_template_result("1..5", "{{ (1..test) }}", assigns)
assert_template_result("5..5", "{{ (test..test) }}", assigns)
end end
def test_cents_through_drop_nestedly def test_cents_through_drop_nestedly
@@ -406,45 +405,45 @@ class ContextTest < Minitest::Test
end end
def test_lambda_is_called_once def test_lambda_is_called_once
@global = 0
@context['callcount'] = proc { @context['callcount'] = proc {
@global += 1 @global ||= 0
@global += 1
@global.to_s @global.to_s
} }
assert_equal('1', @context['callcount']) assert_equal('1', @context['callcount'])
assert_equal('1', @context['callcount']) assert_equal('1', @context['callcount'])
assert_equal('1', @context['callcount']) assert_equal('1', @context['callcount'])
@global = nil
end end
def test_nested_lambda_is_called_once def test_nested_lambda_is_called_once
@global = 0 @context['callcount'] = { "lambda" => proc {
@global ||= 0
@context['callcount'] = { @global += 1
"lambda" => proc { @global.to_s
@global += 1 } }
@global.to_s
},
}
assert_equal('1', @context['callcount.lambda']) assert_equal('1', @context['callcount.lambda'])
assert_equal('1', @context['callcount.lambda']) assert_equal('1', @context['callcount.lambda'])
assert_equal('1', @context['callcount.lambda']) assert_equal('1', @context['callcount.lambda'])
@global = nil
end end
def test_lambda_in_array_is_called_once def test_lambda_in_array_is_called_once
@global = 0 @context['callcount'] = [1, 2, proc {
@global ||= 0
p = proc { @global += 1
@global += 1 @global.to_s
@global.to_s }, 4, 5]
}
@context['callcount'] = [1, 2, p, 4, 5]
assert_equal('1', @context['callcount[2]']) assert_equal('1', @context['callcount[2]'])
assert_equal('1', @context['callcount[2]']) assert_equal('1', @context['callcount[2]'])
assert_equal('1', @context['callcount[2]']) assert_equal('1', @context['callcount[2]'])
@global = nil
end end
def test_access_to_context_from_proc def test_access_to_context_from_proc
@@ -487,7 +486,7 @@ class ContextTest < Minitest::Test
def test_static_environments_are_read_with_lower_priority_than_environments def test_static_environments_are_read_with_lower_priority_than_environments
context = Context.build( context = Context.build(
static_environments: { 'shadowed' => 'static', 'unshadowed' => 'static' }, static_environments: { 'shadowed' => 'static', 'unshadowed' => 'static' },
environments: { 'shadowed' => 'dynamic' }, environments: { 'shadowed' => 'dynamic' }
) )
assert_equal('dynamic', context['shadowed']) assert_equal('dynamic', context['shadowed'])
@@ -532,7 +531,7 @@ class ContextTest < Minitest::Test
registers = { registers = {
my_register: :my_value, my_register: :my_value,
} }
super_context = Context.new({}, {}, Registers.new(registers)) super_context = Context.new({}, {}, StaticRegisters.new(registers))
super_context.registers[:my_register] = :my_alt_value super_context.registers[:my_register] = :my_alt_value
subcontext = super_context.new_isolated_subcontext subcontext = super_context.new_isolated_subcontext
assert_equal(:my_value, subcontext.registers[:my_register]) assert_equal(:my_value, subcontext.registers[:my_register])
@@ -621,20 +620,6 @@ class ContextTest < Minitest::Test
end end
end end
def test_context_always_uses_static_registers
registers = {
my_register: :my_value,
}
c = Context.new({}, {}, registers)
assert_instance_of(Registers, c.registers)
assert_equal(:my_value, c.registers[:my_register])
r = Registers.new(registers)
c = Context.new({}, {}, r)
assert_instance_of(Registers, c.registers)
assert_equal(:my_value, c.registers[:my_register])
end
private private
def assert_no_object_allocations def assert_no_object_allocations
+8 -4
View File
@@ -6,12 +6,16 @@ class DocumentTest < Minitest::Test
include Liquid include Liquid
def test_unexpected_outer_tag def test_unexpected_outer_tag
source = "{% else %}" exc = assert_raises(SyntaxError) do
assert_match_syntax_error("Liquid syntax error (line 1): Unexpected outer 'else' tag", source) Template.parse("{% else %}")
end
assert_equal(exc.message, "Liquid syntax error: Unexpected outer 'else' tag")
end end
def test_unknown_tag def test_unknown_tag
source = "{% foo %}" exc = assert_raises(SyntaxError) do
assert_match_syntax_error("Liquid syntax error (line 1): Unknown tag 'foo'", source) Template.parse("{% foo %}")
end
assert_equal(exc.message, "Liquid syntax error: Unknown tag 'foo'")
end end
end end
+39 -110
View File
@@ -63,7 +63,9 @@ class ErrorHandlingTest < Minitest::Test
end end
def test_missing_endtag_parse_time_error def test_missing_endtag_parse_time_error
assert_match_syntax_error(/: 'for' tag was never closed\z/, ' {% for a in b %} ... ') assert_raises(Liquid::SyntaxError) do
Liquid::Template.parse(' {% for a in b %} ... ')
end
end end
def test_unrecognized_operator def test_unrecognized_operator
@@ -82,31 +84,37 @@ class ErrorHandlingTest < Minitest::Test
end end
def test_with_line_numbers_adds_numbers_to_parser_errors def test_with_line_numbers_adds_numbers_to_parser_errors
source = <<~LIQUID err = assert_raises(SyntaxError) do
foobar Liquid::Template.parse('
foobar
{% "cat" | foobar %} {% "cat" | foobar %}
bla bla
LIQUID ',
assert_match_syntax_error(/Liquid syntax error \(line 3\)/, source) line_numbers: true)
end
assert_match(/Liquid syntax error \(line 4\)/, err.message)
end end
def test_with_line_numbers_adds_numbers_to_parser_errors_with_whitespace_trim def test_with_line_numbers_adds_numbers_to_parser_errors_with_whitespace_trim
source = <<~LIQUID err = assert_raises(SyntaxError) do
foobar Liquid::Template.parse('
foobar
{%- "cat" | foobar -%} {%- "cat" | foobar -%}
bla bla
LIQUID ',
line_numbers: true)
end
assert_match_syntax_error(/Liquid syntax error \(line 3\)/, source) assert_match(/Liquid syntax error \(line 4\)/, err.message)
end end
def test_parsing_warn_with_line_numbers_adds_numbers_to_lexer_errors def test_parsing_warn_with_line_numbers_adds_numbers_to_lexer_errors
template = Liquid::Template.parse( template = Liquid::Template.parse('
'
foobar foobar
{% if 1 =! 2 %}ok{% endif %} {% if 1 =! 2 %}ok{% endif %}
@@ -114,19 +122,15 @@ class ErrorHandlingTest < Minitest::Test
bla bla
', ',
error_mode: :warn, error_mode: :warn,
line_numbers: true, line_numbers: true)
)
assert_equal( assert_equal(['Liquid syntax error (line 4): Unexpected character = in "1 =! 2"'],
['Liquid syntax error (line 4): Unexpected character = in "1 =! 2"'], template.warnings.map(&:message))
template.warnings.map(&:message),
)
end end
def test_parsing_strict_with_line_numbers_adds_numbers_to_lexer_errors def test_parsing_strict_with_line_numbers_adds_numbers_to_lexer_errors
err = assert_raises(SyntaxError) do err = assert_raises(SyntaxError) do
Liquid::Template.parse( Liquid::Template.parse('
'
foobar foobar
{% if 1 =! 2 %}ok{% endif %} {% if 1 =! 2 %}ok{% endif %}
@@ -134,25 +138,27 @@ class ErrorHandlingTest < Minitest::Test
bla bla
', ',
error_mode: :strict, error_mode: :strict,
line_numbers: true, line_numbers: true)
)
end end
assert_equal('Liquid syntax error (line 4): Unexpected character = in "1 =! 2"', err.message) assert_equal('Liquid syntax error (line 4): Unexpected character = in "1 =! 2"', err.message)
end end
def test_syntax_errors_in_nested_blocks_have_correct_line_number def test_syntax_errors_in_nested_blocks_have_correct_line_number
source = <<~LIQUID err = assert_raises(SyntaxError) do
foobar Liquid::Template.parse('
foobar
{% if 1 != 2 %} {% if 1 != 2 %}
{% foo %} {% foo %}
{% endif %} {% endif %}
bla bla
LIQUID ',
line_numbers: true)
end
assert_match_syntax_error("Liquid syntax error (line 4): Unknown tag 'foo'", source) assert_equal("Liquid syntax error (line 5): Unknown tag 'foo'", err.message)
end end
def test_strict_error_messages def test_strict_error_messages
@@ -263,81 +269,4 @@ class ErrorHandlingTest < Minitest::Test
output = Liquid::Template.parse("{% assign x = 0 %}{% if 1 < '2' %}{% assign x = 3 %}{% endif %}{{ x }}").render output = Liquid::Template.parse("{% assign x = 0 %}{% if 1 < '2' %}{% assign x = 3 %}{% endif %}{{ x }}").render
assert_equal("0", output) assert_equal("0", output)
end end
def test_syntax_error_is_raised_with_template_name
file_system = StubFileSystem.new("snippet" => "1\n2\n{{ 1")
context = Liquid::Context.build(
registers: { file_system: file_system },
)
template = Template.parse(
'{% render "snippet" %}',
line_numbers: true,
)
template.name = "template/index"
assert_equal(
"Liquid syntax error (snippet line 3): Variable '{{' was not properly terminated with regexp: /\\}\\}/",
template.render(context),
)
end
def test_syntax_error_is_raised_with_template_name_from_template_factory
file_system = StubFileSystem.new("snippet" => "1\n2\n{{ 1")
context = Liquid::Context.build(
registers: {
file_system: file_system,
template_factory: StubTemplateFactory.new,
},
)
template = Template.parse(
'{% render "snippet" %}',
line_numbers: true,
)
template.name = "template/index"
assert_equal(
"Liquid syntax error (some/path/snippet line 3): Variable '{{' was not properly terminated with regexp: /\\}\\}/",
template.render(context),
)
end
def test_error_is_raised_during_parse_with_template_name
depth = Liquid::Block::MAX_DEPTH + 1
code = "{% if true %}" * depth + "rendered" + "{% endif %}" * depth
template = Template.parse("{% render 'snippet' %}", line_numbers: true)
context = Liquid::Context.build(
registers: {
file_system: StubFileSystem.new("snippet" => code),
template_factory: StubTemplateFactory.new,
},
)
assert_equal("Liquid error (some/path/snippet line 1): Nesting too deep", template.render(context))
end
def test_internal_error_is_raised_with_template_name
template = Template.new
template.parse(
"{% render 'snippet' %}",
line_numbers: true,
)
template.name = "template/index"
context = Liquid::Context.build(
registers: {
file_system: StubFileSystem.new({}),
},
)
assert_equal(
"Liquid error (template/index line 1): internal",
template.render(context),
)
end
end end
+22 -25
View File
@@ -4,46 +4,43 @@ require 'test_helper'
class ExpressionTest < Minitest::Test class ExpressionTest < Minitest::Test
def test_keyword_literals def test_keyword_literals
assert_template_result("true", "{{ true }}") assert_equal(true, parse_and_eval("true"))
assert_expression_result(true, "true") assert_equal(true, parse_and_eval(" true "))
end end
def test_string def test_string
assert_template_result("single quoted", "{{'single quoted'}}") assert_equal("single quoted", parse_and_eval("'single quoted'"))
assert_template_result("double quoted", '{{"double quoted"}}') assert_equal("double quoted", parse_and_eval('"double quoted"'))
assert_template_result("spaced", "{{ 'spaced' }}") assert_equal("spaced", parse_and_eval(" 'spaced' "))
assert_template_result("spaced2", "{{ 'spaced2' }}") assert_equal("spaced2", parse_and_eval(' "spaced2" '))
end end
def test_int def test_int
assert_template_result("456", "{{ 456 }}") assert_equal(123, parse_and_eval("123"))
assert_expression_result(123, "123") assert_equal(456, parse_and_eval(" 456 "))
assert_expression_result(12, "012") assert_equal(12, parse_and_eval("012"))
end end
def test_float def test_float
assert_template_result("2.5", "{{ 2.5 }}") assert_equal(1.5, parse_and_eval("1.5"))
assert_expression_result(1.5, "1.5") assert_equal(2.5, parse_and_eval(" 2.5 "))
end end
def test_range def test_range
assert_template_result("3..4", "{{ ( 3 .. 4 ) }}") assert_equal(1..2, parse_and_eval("(1..2)"))
assert_expression_result(1..2, "(1..2)") assert_equal(3..4, parse_and_eval(" ( 3 .. 4 ) "))
assert_match_syntax_error(
"Liquid syntax error (line 1): Invalid expression type 'false' in range expression",
"{{ (false..true) }}",
)
assert_match_syntax_error(
"Liquid syntax error (line 1): Invalid expression type '(1..2)' in range expression",
"{{ ((1..2)..3) }}",
)
end end
private private
def assert_expression_result(expect, markup, **assigns) def parse_and_eval(markup, **assigns)
liquid = "{% if expect == #{markup} %}pass{% else %}got {{ #{markup} }}{% endif %}" if Liquid::Template.error_mode == :strict
assert_template_result("pass", liquid, { "expect" => expect, **assigns }) p = Liquid::Parser.new(markup)
markup = p.expression
p.consume(:end_of_string)
end
expression = Liquid::Expression.parse(markup)
context = Liquid::Context.new(assigns)
context.evaluate(expression)
end end
end end
-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
+42 -45
View File
@@ -59,87 +59,84 @@ class FiltersTest < Minitest::Test
end end
def test_size def test_size
assert_template_result("4", "{{var | size}}", { "var" => 'abcd' }) @context['var'] = 'abcd'
@context.add_filters(MoneyFilter)
assert_equal('4', Template.parse("{{var | size}}").render(@context))
end end
def test_join def test_join
assert_template_result("1 2 3 4", "{{var | join}}", { "var" => [1, 2, 3, 4] }) @context['var'] = [1, 2, 3, 4]
assert_equal("1 2 3 4", Template.parse("{{var | join}}").render(@context))
end end
def test_sort def test_sort
assert_template_result("1 2 3 4", "{{numbers | sort | join}}", { "numbers" => [2, 1, 4, 3] }) @context['value'] = 3
assert_template_result( @context['numbers'] = [2, 1, 4, 3]
"alphabetic as expected", @context['words'] = ['expected', 'as', 'alphabetic']
"{{words | sort | join}}", @context['arrays'] = ['flower', 'are']
{ "words" => ['expected', 'as', 'alphabetic'] }, @context['case_sensitive'] = ['sensitive', 'Expected', 'case']
)
assert_template_result("3", "{{value | sort}}", { "value" => 3 }) assert_equal('1 2 3 4', Template.parse("{{numbers | sort | join}}").render(@context))
assert_template_result('are flower', "{{arrays | sort | join}}", { 'arrays' => ['flower', 'are'] }) assert_equal('alphabetic as expected', Template.parse("{{words | sort | join}}").render(@context))
assert_template_result( assert_equal('3', Template.parse("{{value | sort}}").render(@context))
"Expected case sensitive", assert_equal('are flower', Template.parse("{{arrays | sort | join}}").render(@context))
"{{case_sensitive | sort | join}}", assert_equal('Expected case sensitive', Template.parse("{{case_sensitive | sort | join}}").render(@context))
{ "case_sensitive" => ["sensitive", "Expected", "case"] },
)
end end
def test_sort_natural def test_sort_natural
@context['words'] = ['case', 'Assert', 'Insensitive']
@context['hashes'] = [{ 'a' => 'A' }, { 'a' => 'b' }, { 'a' => 'C' }]
@context['objects'] = [TestObject.new('A'), TestObject.new('b'), TestObject.new('C')]
# Test strings # Test strings
assert_template_result( assert_equal('Assert case Insensitive', Template.parse("{{words | sort_natural | join}}").render(@context))
"Assert case Insensitive",
"{{words | sort_natural | join}}",
{ "words" => ["case", "Assert", "Insensitive"] },
)
# Test hashes # Test hashes
assert_template_result( assert_equal('A b C', Template.parse("{{hashes | sort_natural: 'a' | map: 'a' | join}}").render(@context))
"A b C",
"{{hashes | sort_natural: 'a' | map: 'a' | join}}",
{ "hashes" => [{ "a" => "A" }, { "a" => "b" }, { "a" => "C" }] },
)
# Test objects # Test objects
@context['objects'] = [TestObject.new('A'), TestObject.new('b'), TestObject.new('C')]
assert_equal('A b C', Template.parse("{{objects | sort_natural: 'a' | map: 'a' | join}}").render(@context)) assert_equal('A b C', Template.parse("{{objects | sort_natural: 'a' | map: 'a' | join}}").render(@context))
end end
def test_compact def test_compact
@context['words'] = ['a', nil, 'b', nil, 'c']
@context['hashes'] = [{ 'a' => 'A' }, { 'a' => nil }, { 'a' => 'C' }]
@context['objects'] = [TestObject.new('A'), TestObject.new(nil), TestObject.new('C')]
# Test strings # Test strings
assert_template_result( assert_equal('a b c', Template.parse("{{words | compact | join}}").render(@context))
"a b c",
"{{words | compact | join}}",
{ "words" => ['a', nil, 'b', nil, 'c'] },
)
# Test hashes # Test hashes
assert_template_result( assert_equal('A C', Template.parse("{{hashes | compact: 'a' | map: 'a' | join}}").render(@context))
"A C",
"{{hashes | compact: 'a' | map: 'a' | join}}",
{ "hashes" => [{ "a" => "A" }, { "a" => nil }, { "a" => "C" }] },
)
# Test objects # Test objects
@context['objects'] = [TestObject.new('A'), TestObject.new(nil), TestObject.new('C')]
assert_equal('A C', Template.parse("{{objects | compact: 'a' | map: 'a' | join}}").render(@context)) assert_equal('A C', Template.parse("{{objects | compact: 'a' | map: 'a' | join}}").render(@context))
end end
def test_strip_html def test_strip_html
assert_template_result("bla blub", "{{ var | strip_html }}", { "var" => "<b>bla blub</a>" }) @context['var'] = "<b>bla blub</a>"
assert_equal("bla blub", Template.parse("{{ var | strip_html }}").render(@context))
end end
def test_strip_html_ignore_comments_with_html def test_strip_html_ignore_comments_with_html
assert_template_result( @context['var'] = "<!-- split and some <ul> tag --><b>bla blub</a>"
"bla blub",
"{{ var | strip_html }}", assert_equal("bla blub", Template.parse("{{ var | strip_html }}").render(@context))
{ "var" => "<!-- split and some <ul> tag --><b>bla blub</a>" },
)
end end
def test_capitalize def test_capitalize
assert_template_result("Blub", "{{ var | capitalize }}", { "var" => "blub" }) @context['var'] = "blub"
assert_equal("Blub", Template.parse("{{ var | capitalize }}").render(@context))
end end
def test_nonexistent_filter_is_ignored def test_nonexistent_filter_is_ignored
assert_template_result("1000", "{{ var | xyzzy }}", { "var" => 1000 }) @context['var'] = 1000
assert_equal('1000', Template.parse("{{ var | xyzzy }}").render(@context))
end end
def test_filter_with_keyword_arguments def test_filter_with_keyword_arguments
+14 -9
View File
@@ -33,25 +33,31 @@ class OutputTest < Minitest::Test
def setup def setup
@assigns = { @assigns = {
'best_cars' => 'bmw',
'car' => { 'bmw' => 'good', 'gm' => 'bad' }, 'car' => { 'bmw' => 'good', 'gm' => 'bad' },
} }
end end
def test_variable def test_variable
assert_template_result(" bmw ", " {{best_cars}} ", { "best_cars" => "bmw" }) text = %( {{best_cars}} )
expected = %( bmw )
assert_equal(expected, Template.parse(text).render!(@assigns))
end end
def test_variable_traversing_with_two_brackets def test_variable_traversing_with_two_brackets
source = "{{ site.data.menu[include.menu][include.locale] }}" text = %({{ site.data.menu[include.menu][include.locale] }})
assert_template_result("it works!", source, { assert_equal("it works!", Template.parse(text).render!(
"site" => { "data" => { "menu" => { "foo" => { "bar" => "it works!" } } } }, "site" => { "data" => { "menu" => { "foo" => { "bar" => "it works!" } } } },
"include" => { "menu" => "foo", "locale" => "bar" }, "include" => { "menu" => "foo", "locale" => "bar" }
}) ))
end end
def test_variable_traversing def test_variable_traversing
source = " {{car.bmw}} {{car.gm}} {{car.bmw}} " text = %( {{car.bmw}} {{car.gm}} {{car.bmw}} )
assert_template_result(" good bad good ", source, @assigns)
expected = %( good bad good )
assert_equal(expected, Template.parse(text).render!(@assigns))
end end
def test_variable_piping def test_variable_piping
@@ -104,11 +110,10 @@ class OutputTest < Minitest::Test
end end
def test_multiple_pipings def test_multiple_pipings
assigns = { 'best_cars' => 'bmw' }
text = %( {{ best_cars | cite_funny | paragraph }} ) text = %( {{ best_cars | cite_funny | paragraph }} )
expected = %( <p>LOL: bmw</p> ) expected = %( <p>LOL: bmw</p> )
assert_equal(expected, Template.parse(text).render!(assigns, filters: [FunnyFilter])) assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
end end
def test_link_to def test_link_to
+1 -1
View File
@@ -129,6 +129,6 @@ class ParsingQuirksTest < Minitest::Test
end end
def test_contains_in_id def test_contains_in_id
assert_template_result(' YES ', '{% if containsallshipments == true %} YES {% endif %}', { 'containsallshipments' => true }) assert_template_result(' YES ', '{% if containsallshipments == true %} YES {% endif %}', 'containsallshipments' => true)
end end
end # ParsingQuirksTest end # ParsingQuirksTest
+6 -33
View File
@@ -3,27 +3,6 @@
require 'test_helper' require 'test_helper'
class ProfilerTest < Minitest::Test 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 include Liquid
class ProfilingFileSystem class ProfilingFileSystem
@@ -219,22 +198,16 @@ class ProfilerTest < Minitest::Test
def test_profiling_supports_self_time def test_profiling_supports_self_time
t = Template.parse("{% for item in collection %} {{ item }} {% endfor %}", profile: true) t = Template.parse("{% for item in collection %} {{ item }} {% endfor %}", profile: true)
collection = [ t.render!("collection" => ["one", "two"])
TestDrop.new("one"),
TestDrop.new("two"),
]
output = t.render!("collection" => collection)
assert_equal(" one two ", output)
leaf = t.profiler[0].children[0] leaf = t.profiler[0].children[0]
assert_operator(leaf.self_time, :>, 0.0)
assert_operator(leaf.self_time, :>, 0)
end end
def test_profiling_supports_total_time def test_profiling_supports_total_time
t = Template.parse("{% if true %} {{ test }} {% endif %}", profile: true) t = Template.parse("{% if true %} {% increment test %} {{ test }} {% endif %}", profile: true)
output = t.render!("test" => TestDrop.new("one")) t.render!
assert_equal(" one ", output)
assert_operator(t.profiler[0].total_time, :>, 0.0) assert_operator(t.profiler[0].total_time, :>, 0)
end end
end end
+77 -179
View File
@@ -3,6 +3,10 @@
require 'test_helper' require 'test_helper'
class Filters
include Liquid::StandardFilters
end
class TestThing class TestThing
attr_reader :foo attr_reader :foo
@@ -25,24 +29,8 @@ class TestThing
end end
class TestDrop < Liquid::Drop class TestDrop < Liquid::Drop
def initialize(value:) def test
@value = value "testfoo"
end
attr_reader :value
def registers
{ @value => @context.registers[@value] }
end
end
class TestModel
def initialize(value:)
@value = value
end
def to_liquid
TestDrop.new(value: @value)
end end
end end
@@ -65,13 +53,10 @@ class NumberLikeThing < Liquid::Drop
end end
class StandardFiltersTest < Minitest::Test class StandardFiltersTest < Minitest::Test
Filters = Class.new(Liquid::StrainerTemplate)
Filters.add_filter(Liquid::StandardFilters)
include Liquid include Liquid
def setup def setup
@filters = Filters.new(Context.new) @filters = Filters.new
end end
def test_size def test_size
@@ -109,10 +94,6 @@ class StandardFiltersTest < Minitest::Test
assert_raises(Liquid::ArgumentError) do assert_raises(Liquid::ArgumentError) do
@filters.slice('foobar', 0, "") @filters.slice('foobar', 0, "")
end end
assert_equal("", @filters.slice("foobar", 0, -(1 << 64)))
assert_equal("foobar", @filters.slice("foobar", 0, 1 << 63))
assert_equal("", @filters.slice("foobar", 1 << 63, 6))
assert_equal("", @filters.slice("foobar", -(1 << 63), 6))
end end
def test_slice_on_arrays def test_slice_on_arrays
@@ -127,10 +108,6 @@ class StandardFiltersTest < Minitest::Test
assert_equal(%w(r), @filters.slice(input, -1)) assert_equal(%w(r), @filters.slice(input, -1))
assert_equal(%w(), @filters.slice(input, 100, 10)) assert_equal(%w(), @filters.slice(input, 100, 10))
assert_equal(%w(), @filters.slice(input, -100, 10)) assert_equal(%w(), @filters.slice(input, -100, 10))
assert_equal([], @filters.slice(input, 0, -(1 << 64)))
assert_equal(input, @filters.slice(input, 0, 1 << 63))
assert_equal([], @filters.slice(input, 1 << 63, 6))
assert_equal([], @filters.slice(input, -(1 << 63), 6))
end end
def test_truncate def test_truncate
@@ -140,8 +117,6 @@ class StandardFiltersTest < Minitest::Test
assert_equal('1234567890', @filters.truncate('1234567890')) assert_equal('1234567890', @filters.truncate('1234567890'))
assert_equal("测试...", @filters.truncate("测试测试测试测试", 5)) assert_equal("测试...", @filters.truncate("测试测试测试测试", 5))
assert_equal('12341', @filters.truncate("1234567890", 5, 1)) assert_equal('12341', @filters.truncate("1234567890", 5, 1))
assert_equal("foobar", @filters.truncate("foobar", 1 << 63))
assert_equal("...", @filters.truncate("foobar", -(1 << 63)))
end end
def test_split def test_split
@@ -170,40 +145,6 @@ class StandardFiltersTest < Minitest::Test
assert_equal('&lt;strong&gt;Hulk&lt;/strong&gt;', @filters.escape_once('&lt;strong&gt;Hulk</strong>')) assert_equal('&lt;strong&gt;Hulk&lt;/strong&gt;', @filters.escape_once('&lt;strong&gt;Hulk</strong>'))
end 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 def test_url_encode
assert_equal('foo%2B1%40example.com', @filters.url_encode('[email protected]')) assert_equal('foo%2B1%40example.com', @filters.url_encode('[email protected]'))
assert_equal('1', @filters.url_encode(1)) assert_equal('1', @filters.url_encode(1))
@@ -230,15 +171,13 @@ class StandardFiltersTest < Minitest::Test
assert_equal('one two three', @filters.truncatewords('one two three')) assert_equal('one two three', @filters.truncatewords('one two three'))
assert_equal( assert_equal(
'Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221;...', '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("测试测试测试测试", @filters.truncatewords('测试测试测试测试', 5))
assert_equal('one two1', @filters.truncatewords("one two three", 2, 1)) 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 three...', @filters.truncatewords("one two\tthree\nfour", 3))
assert_equal('one two...', @filters.truncatewords("one two three four", 2)) assert_equal('one two...', @filters.truncatewords("one two three four", 2))
assert_equal('one...', @filters.truncatewords("one two three four", 0)) assert_equal('one...', @filters.truncatewords("one two three four", 0))
assert_equal('one two three four', @filters.truncatewords("one two three four", 1 << 31))
assert_equal('one...', @filters.truncatewords("one two three four", -(1 << 32)))
end end
def test_strip_html def test_strip_html
@@ -282,8 +221,8 @@ class StandardFiltersTest < Minitest::Test
{ "price" => 1, "handle" => "gamma" }, { "price" => 1, "handle" => "gamma" },
{ "price" => 2, "handle" => "epsilon" }, { "price" => 2, "handle" => "epsilon" },
{ "price" => 4, "handle" => "alpha" }, { "price" => 4, "handle" => "alpha" },
{ "handle" => "beta" },
{ "handle" => "delta" }, { "handle" => "delta" },
{ "handle" => "beta" },
] ]
assert_equal(expectation, @filters.sort(input, "price")) assert_equal(expectation, @filters.sort(input, "price"))
end end
@@ -386,9 +325,8 @@ class StandardFiltersTest < Minitest::Test
assert_equal(["foo"], @filters.uniq("foo")) 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([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")) 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") testdrop = TestDrop.new
test_drop_alternate = TestDrop.new(value: "test") assert_equal([testdrop], @filters.uniq([testdrop, TestDrop.new], 'test'))
assert_equal([test_drop], @filters.uniq([test_drop, test_drop_alternate], 'value'))
end end
def test_uniq_empty_array def test_uniq_empty_array
@@ -433,11 +371,8 @@ class StandardFiltersTest < Minitest::Test
def test_map def test_map
assert_equal([1, 2, 3, 4], @filters.map([{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }], 'a')) assert_equal([1, 2, 3, 4], @filters.map([{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }], 'a'))
assert_template_result( assert_template_result('abc', "{{ ary | map:'foo' | map:'bar' }}",
'abc', 'ary' => [{ 'foo' => { 'bar' => 'a' } }, { 'foo' => { 'bar' => 'b' } }, { 'foo' => { 'bar' => 'c' } }])
"{{ ary | map:'foo' | map:'bar' }}",
{ 'ary' => [{ 'foo' => { 'bar' => 'a' } }, { 'foo' => { 'bar' => 'b' } }, { 'foo' => { 'bar' => 'c' } }] },
)
end end
def test_map_doesnt_call_arbitrary_stuff def test_map_doesnt_call_arbitrary_stuff
@@ -447,31 +382,18 @@ class StandardFiltersTest < Minitest::Test
def test_map_calls_to_liquid def test_map_calls_to_liquid
t = TestThing.new t = TestThing.new
assert_template_result("woot: 1", '{{ foo | map: "whatever" }}', { "foo" => [t] }) 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 end
def test_map_on_hashes def test_map_on_hashes
assert_template_result( assert_template_result("4217", '{{ thing | map: "foo" | map: "bar" }}',
"4217", "thing" => { "foo" => [{ "bar" => 42 }, { "bar" => 17 }] })
'{{ thing | map: "foo" | map: "bar" }}',
{ "thing" => { "foo" => [{ "bar" => 42 }, { "bar" => 17 }] } },
)
end end
def test_legacy_map_on_hashes_with_dynamic_key def test_legacy_map_on_hashes_with_dynamic_key
template = "{% assign key = 'foo' %}{{ thing | map: key | map: 'bar' }}" template = "{% assign key = 'foo' %}{{ thing | map: key | map: 'bar' }}"
hash = { "foo" => { "bar" => 42 } } hash = { "foo" => { "bar" => 42 } }
assert_template_result("42", template, { "thing" => hash }) assert_template_result("42", template, "thing" => hash)
end end
def test_sort_calls_to_liquid def test_sort_calls_to_liquid
@@ -481,10 +403,10 @@ class StandardFiltersTest < Minitest::Test
end end
def test_map_over_proc def test_map_over_proc
drop = TestDrop.new(value: "testfoo") drop = TestDrop.new
p = proc { drop } p = proc { drop }
output = Liquid::Template.parse('{{ procs | map: "value" }}').render!({ "procs" => [p] }) templ = '{{ procs | map: "test" }}'
assert_equal("testfoo", output) assert_template_result("testfoo", templ, "procs" => [p])
end end
def test_map_over_drops_returning_procs def test_map_over_drops_returning_procs
@@ -496,13 +418,12 @@ class StandardFiltersTest < Minitest::Test
"proc" => -> { "bar" }, "proc" => -> { "bar" },
}, },
] ]
output = Liquid::Template.parse('{{ drops | map: "proc" }}').render!({ "drops" => drops }) templ = '{{ drops | map: "proc" }}'
assert_equal("foobar", output) assert_template_result("foobar", templ, "drops" => drops)
end end
def test_map_works_on_enumerables def test_map_works_on_enumerables
output = Liquid::Template.parse('{{ foo | map: "foo" }}').render!({ "foo" => TestEnumerable.new }) assert_template_result("123", '{{ foo | map: "foo" }}', "foo" => TestEnumerable.new)
assert_equal("123", output)
end end
def test_map_returns_empty_on_2d_input_array def test_map_returns_empty_on_2d_input_array
@@ -529,16 +450,16 @@ class StandardFiltersTest < Minitest::Test
end end
def test_sort_works_on_enumerables def test_sort_works_on_enumerables
assert_template_result("213", '{{ foo | sort: "bar" | map: "foo" }}', { "foo" => TestEnumerable.new }) assert_template_result("213", '{{ foo | sort: "bar" | map: "foo" }}', "foo" => TestEnumerable.new)
end end
def test_first_and_last_call_to_liquid def test_first_and_last_call_to_liquid
assert_template_result('foobar', '{{ foo | first }}', { 'foo' => [ThingWithToLiquid.new] }) assert_template_result('foobar', '{{ foo | first }}', 'foo' => [ThingWithToLiquid.new])
assert_template_result('foobar', '{{ foo | last }}', { 'foo' => [ThingWithToLiquid.new] }) assert_template_result('foobar', '{{ foo | last }}', 'foo' => [ThingWithToLiquid.new])
end end
def test_truncate_calls_to_liquid def test_truncate_calls_to_liquid
assert_template_result("wo...", '{{ foo | truncate: 5 }}', { "foo" => TestThing.new }) assert_template_result("wo...", '{{ foo | truncate: 5 }}', "foo" => TestThing.new)
end end
def test_date def test_date
@@ -580,31 +501,19 @@ class StandardFiltersTest < Minitest::Test
end end
def test_replace 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('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_equal('2 1 1 1', @filters.replace_first('1 1 1 1', '1', 2))
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_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 end
def test_remove def test_remove
assert_equal(' ', @filters.remove("a a a a", 'a')) assert_equal(' ', @filters.remove("a a a a", 'a'))
assert_template_result(' ', "{{ '1 1 1 1' | remove: 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('b a a', @filters.remove_first("a b a a", 'a ')) assert_equal(' 1 1 1', @filters.remove_first("1 1 1 1", 1))
assert_template_result(' 1 1 1', "{{ '1 1 1 1' | remove_first: 1 }}") assert_template_result('a a a', "{{ 'a a a a' | remove_first: 'a ' }}")
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 end
def test_pipes_in_string_arguments def test_pipes_in_string_arguments
@@ -612,42 +521,42 @@ class StandardFiltersTest < Minitest::Test
end end
def test_strip def test_strip
assert_template_result('ab c', "{{ source | strip }}", { 'source' => " ab c " }) assert_template_result('ab c', "{{ source | strip }}", 'source' => " ab c ")
assert_template_result('ab c', "{{ source | strip }}", { 'source' => " \tab c \n \t" }) assert_template_result('ab c', "{{ source | strip }}", 'source' => " \tab c \n \t")
end end
def test_lstrip def test_lstrip
assert_template_result('ab c ', "{{ source | lstrip }}", { 'source' => " ab c " }) assert_template_result('ab c ', "{{ source | lstrip }}", 'source' => " ab c ")
assert_template_result("ab c \n \t", "{{ source | lstrip }}", { 'source' => " \tab c \n \t" }) assert_template_result("ab c \n \t", "{{ source | lstrip }}", 'source' => " \tab c \n \t")
end end
def test_rstrip def test_rstrip
assert_template_result(" ab c", "{{ source | rstrip }}", { 'source' => " ab c " }) assert_template_result(" ab c", "{{ source | rstrip }}", 'source' => " ab c ")
assert_template_result(" \tab c", "{{ source | rstrip }}", { 'source' => " \tab c \n \t" }) assert_template_result(" \tab c", "{{ source | rstrip }}", 'source' => " \tab c \n \t")
end end
def test_strip_newlines def test_strip_newlines
assert_template_result('abc', "{{ source | strip_newlines }}", { 'source' => "a\nb\nc" }) assert_template_result('abc', "{{ source | strip_newlines }}", 'source' => "a\nb\nc")
assert_template_result('abc', "{{ source | strip_newlines }}", { 'source' => "a\r\nb\nc" }) assert_template_result('abc', "{{ source | strip_newlines }}", 'source' => "a\r\nb\nc")
end end
def test_newlines_to_br def test_newlines_to_br
assert_template_result("a<br />\nb<br />\nc", "{{ source | newline_to_br }}", { 'source' => "a\nb\nc" }) assert_template_result("a<br />\nb<br />\nc", "{{ source | newline_to_br }}", 'source' => "a\nb\nc")
assert_template_result("a<br />\nb<br />\nc", "{{ source | newline_to_br }}", { 'source' => "a\r\nb\nc" }) assert_template_result("a<br />\nb<br />\nc", "{{ source | newline_to_br }}", 'source' => "a\r\nb\nc")
end end
def test_plus def test_plus
assert_template_result("2", "{{ 1 | plus:1 }}") assert_template_result("2", "{{ 1 | plus:1 }}")
assert_template_result("2.0", "{{ '1' | plus:'1.0' }}") assert_template_result("2.0", "{{ '1' | plus:'1.0' }}")
assert_template_result("5", "{{ price | plus:'2' }}", { 'price' => NumberLikeThing.new(3) }) assert_template_result("5", "{{ price | plus:'2' }}", 'price' => NumberLikeThing.new(3))
end end
def test_minus def test_minus
assert_template_result("4", "{{ input | minus:operand }}", { 'input' => 5, 'operand' => 1 }) assert_template_result("4", "{{ input | minus:operand }}", 'input' => 5, 'operand' => 1)
assert_template_result("2.3", "{{ '4.3' | minus:'2' }}") assert_template_result("2.3", "{{ '4.3' | minus:'2' }}")
assert_template_result("5", "{{ price | minus:'2' }}", { 'price' => NumberLikeThing.new(7) }) assert_template_result("5", "{{ price | minus:'2' }}", 'price' => NumberLikeThing.new(7))
end end
def test_abs def test_abs
@@ -670,7 +579,7 @@ class StandardFiltersTest < Minitest::Test
assert_template_result("7.25", "{{ 0.0725 | times:100 }}") assert_template_result("7.25", "{{ 0.0725 | times:100 }}")
assert_template_result("-7.25", '{{ "-0.0725" | times:100 }}') assert_template_result("-7.25", '{{ "-0.0725" | times:100 }}')
assert_template_result("7.25", '{{ "-0.0725" | times: -100 }}') assert_template_result("7.25", '{{ "-0.0725" | times: -100 }}')
assert_template_result("4", "{{ price | times:2 }}", { 'price' => NumberLikeThing.new(2) }) assert_template_result("4", "{{ price | times:2 }}", 'price' => NumberLikeThing.new(2))
end end
def test_divided_by def test_divided_by
@@ -685,7 +594,7 @@ class StandardFiltersTest < Minitest::Test
assert_template_result("4", "{{ 1 | modulo: 0 }}") assert_template_result("4", "{{ 1 | modulo: 0 }}")
end end
assert_template_result("5", "{{ price | divided_by:2 }}", { 'price' => NumberLikeThing.new(10) }) assert_template_result("5", "{{ price | divided_by:2 }}", 'price' => NumberLikeThing.new(10))
end end
def test_modulo def test_modulo
@@ -694,39 +603,39 @@ class StandardFiltersTest < Minitest::Test
assert_template_result("4", "{{ 1 | modulo: 0 }}") assert_template_result("4", "{{ 1 | modulo: 0 }}")
end end
assert_template_result("1", "{{ price | modulo:2 }}", { 'price' => NumberLikeThing.new(3) }) assert_template_result("1", "{{ price | modulo:2 }}", 'price' => NumberLikeThing.new(3))
end end
def test_round def test_round
assert_template_result("5", "{{ input | round }}", { 'input' => 4.6 }) assert_template_result("5", "{{ input | round }}", 'input' => 4.6)
assert_template_result("4", "{{ '4.3' | round }}") assert_template_result("4", "{{ '4.3' | round }}")
assert_template_result("4.56", "{{ input | round: 2 }}", { 'input' => 4.5612 }) assert_template_result("4.56", "{{ input | round: 2 }}", 'input' => 4.5612)
assert_raises(Liquid::FloatDomainError) do assert_raises(Liquid::FloatDomainError) do
assert_template_result("4", "{{ 1.0 | divided_by: 0.0 | round }}") assert_template_result("4", "{{ 1.0 | divided_by: 0.0 | round }}")
end end
assert_template_result("5", "{{ price | round }}", { 'price' => NumberLikeThing.new(4.6) }) assert_template_result("5", "{{ price | round }}", 'price' => NumberLikeThing.new(4.6))
assert_template_result("4", "{{ price | round }}", { 'price' => NumberLikeThing.new(4.3) }) assert_template_result("4", "{{ price | round }}", 'price' => NumberLikeThing.new(4.3))
end end
def test_ceil def test_ceil
assert_template_result("5", "{{ input | ceil }}", { 'input' => 4.6 }) assert_template_result("5", "{{ input | ceil }}", 'input' => 4.6)
assert_template_result("5", "{{ '4.3' | ceil }}") assert_template_result("5", "{{ '4.3' | ceil }}")
assert_raises(Liquid::FloatDomainError) do assert_raises(Liquid::FloatDomainError) do
assert_template_result("4", "{{ 1.0 | divided_by: 0.0 | ceil }}") assert_template_result("4", "{{ 1.0 | divided_by: 0.0 | ceil }}")
end end
assert_template_result("5", "{{ price | ceil }}", { 'price' => NumberLikeThing.new(4.6) }) assert_template_result("5", "{{ price | ceil }}", 'price' => NumberLikeThing.new(4.6))
end end
def test_floor def test_floor
assert_template_result("4", "{{ input | floor }}", { 'input' => 4.6 }) assert_template_result("4", "{{ input | floor }}", 'input' => 4.6)
assert_template_result("4", "{{ '4.3' | floor }}") assert_template_result("4", "{{ '4.3' | floor }}")
assert_raises(Liquid::FloatDomainError) do assert_raises(Liquid::FloatDomainError) do
assert_template_result("4", "{{ 1.0 | divided_by: 0.0 | floor }}") assert_template_result("4", "{{ 1.0 | divided_by: 0.0 | floor }}")
end end
assert_template_result("5", "{{ price | floor }}", { 'price' => NumberLikeThing.new(5.4) }) assert_template_result("5", "{{ price | floor }}", 'price' => NumberLikeThing.new(5.4))
end end
def test_at_most def test_at_most
@@ -735,9 +644,9 @@ class StandardFiltersTest < Minitest::Test
assert_template_result("5", "{{ 5 | at_most:6 }}") assert_template_result("5", "{{ 5 | at_most:6 }}")
assert_template_result("4.5", "{{ 4.5 | at_most:5 }}") assert_template_result("4.5", "{{ 4.5 | at_most:5 }}")
assert_template_result("5", "{{ width | at_most:5 }}", { 'width' => NumberLikeThing.new(6) }) assert_template_result("5", "{{ width | at_most:5 }}", 'width' => NumberLikeThing.new(6))
assert_template_result("4", "{{ width | at_most:5 }}", { 'width' => NumberLikeThing.new(4) }) assert_template_result("4", "{{ width | at_most:5 }}", 'width' => NumberLikeThing.new(4))
assert_template_result("4", "{{ 5 | at_most: width }}", { 'width' => NumberLikeThing.new(4) }) assert_template_result("4", "{{ 5 | at_most: width }}", 'width' => NumberLikeThing.new(4))
end end
def test_at_least def test_at_least
@@ -746,9 +655,9 @@ class StandardFiltersTest < Minitest::Test
assert_template_result("6", "{{ 5 | at_least:6 }}") assert_template_result("6", "{{ 5 | at_least:6 }}")
assert_template_result("5", "{{ 4.5 | at_least:5 }}") assert_template_result("5", "{{ 4.5 | at_least:5 }}")
assert_template_result("6", "{{ width | at_least:5 }}", { 'width' => NumberLikeThing.new(6) }) assert_template_result("6", "{{ width | at_least:5 }}", 'width' => NumberLikeThing.new(6))
assert_template_result("5", "{{ width | at_least:5 }}", { 'width' => NumberLikeThing.new(4) }) assert_template_result("5", "{{ width | at_least:5 }}", 'width' => NumberLikeThing.new(4))
assert_template_result("6", "{{ 5 | at_least: width }}", { 'width' => NumberLikeThing.new(6) }) assert_template_result("6", "{{ 5 | at_least: width }}", 'width' => NumberLikeThing.new(6))
end end
def test_append def test_append
@@ -781,8 +690,6 @@ class StandardFiltersTest < Minitest::Test
assert_equal("bar", @filters.default([], "bar")) assert_equal("bar", @filters.default([], "bar"))
assert_equal("bar", @filters.default({}, "bar")) assert_equal("bar", @filters.default({}, "bar"))
assert_template_result('bar', "{{ false | 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 end
def test_default_handle_false def test_default_handle_false
@@ -793,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_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('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 end
def test_cannot_access_private_methods def test_cannot_access_private_methods
@@ -823,18 +728,6 @@ class StandardFiltersTest < Minitest::Test
assert_equal(expectation, @filters.where(input, "ok")) assert_equal(expectation, @filters.where(input, "ok"))
end 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 def test_where_no_key_set
input = [ input = [
{ "handle" => "alpha", "ok" => true }, { "handle" => "alpha", "ok" => true },
@@ -880,7 +773,7 @@ class StandardFiltersTest < Minitest::Test
end end
def test_all_filters_never_raise_non_liquid_exception 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_drop.context = Context.new
test_enum = TestEnumerable.new test_enum = TestEnumerable.new
test_enum.context = Context.new test_enum.context = Context.new
@@ -905,14 +798,19 @@ class StandardFiltersTest < Minitest::Test
{ 1 => "bar" }, { 1 => "bar" },
["foo", 123, nil, true, false, Drop, ["foo"], { foo: "bar" }], ["foo", 123, nil, true, false, Drop, ["foo"], { foo: "bar" }],
] ]
StandardFilters.public_instance_methods(false).each do |method| test_types.each do |first|
arg_count = @filters.method(method).arity test_types.each do |other|
arg_count *= -1 if arg_count < 0 (@filters.methods - Object.methods).each do |method|
arg_count = @filters.method(method).arity
test_types.repeated_permutation(arg_count) do |args| arg_count *= -1 if arg_count < 0
@filters.send(method, *args) inputs = [first]
rescue Liquid::Error inputs << ([other] * (arg_count - 1)) if arg_count > 1
nil begin
@filters.send(method, *inputs)
rescue Liquid::ArgumentError, Liquid::ZeroDivisionError
nil
end
end
end end
end end
end end
+2 -2
View File
@@ -9,8 +9,8 @@ class BreakTagTest < Minitest::Test
# block # block
def test_break_with_no_block def test_break_with_no_block
assigns = { 'i' => 1 } assigns = { 'i' => 1 }
markup = 'before{% break %}after' markup = '{% break %}'
expected = 'before' expected = ''
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
end end
+1 -1
View File
@@ -6,7 +6,7 @@ class EchoTest < Minitest::Test
include Liquid include Liquid
def test_echo_outputs_its_input def test_echo_outputs_its_input
assert_template_result('BAR', <<~LIQUID, { 'variable-name' => 'bar' }) assert_template_result('BAR', <<~LIQUID, 'variable-name' => 'bar')
{%- echo variable-name | upcase -%} {%- echo variable-name | upcase -%}
LIQUID LIQUID
end end
+59 -61
View File
@@ -12,10 +12,10 @@ class ForTagTest < Minitest::Test
include Liquid include Liquid
def test_for def test_for
assert_template_result(' yo yo yo yo ', '{%for item in array%} yo {%endfor%}', { 'array' => [1, 2, 3, 4] }) assert_template_result(' yo yo yo yo ', '{%for item in array%} yo {%endfor%}', 'array' => [1, 2, 3, 4])
assert_template_result('yoyo', '{%for item in array%}yo{%endfor%}', { 'array' => [1, 2] }) assert_template_result('yoyo', '{%for item in array%}yo{%endfor%}', 'array' => [1, 2])
assert_template_result(' yo ', '{%for item in array%} yo {%endfor%}', { 'array' => [1] }) assert_template_result(' yo ', '{%for item in array%} yo {%endfor%}', 'array' => [1])
assert_template_result('', '{%for item in array%}{%endfor%}', { 'array' => [1, 2] }) assert_template_result('', '{%for item in array%}{%endfor%}', 'array' => [1, 2])
expected = <<HERE expected = <<HERE
yo yo
@@ -30,7 +30,7 @@ HERE
yo yo
{%endfor%} {%endfor%}
HERE HERE
assert_template_result(expected, template, { 'array' => [1, 2, 3] }) assert_template_result(expected, template, 'array' => [1, 2, 3])
end end
def test_for_reversed def test_for_reversed
@@ -45,39 +45,37 @@ HERE
Template.parse('{% for i in (a..2) %}{% endfor %}').render!("a" => [1, 2]) Template.parse('{% for i in (a..2) %}{% endfor %}').render!("a" => [1, 2])
end end
assert_template_result(' 0 1 2 3 ', '{% for item in (a..3) %} {{item}} {% endfor %}', { "a" => "invalid integer" }) assert_template_result(' 0 1 2 3 ', '{% for item in (a..3) %} {{item}} {% endfor %}', "a" => "invalid integer")
end end
def test_for_with_variable_range def test_for_with_variable_range
assert_template_result(' 1 2 3 ', '{%for item in (1..foobar) %} {{item}} {%endfor%}', { "foobar" => 3 }) assert_template_result(' 1 2 3 ', '{%for item in (1..foobar) %} {{item}} {%endfor%}', "foobar" => 3)
end end
def test_for_with_hash_value_range def test_for_with_hash_value_range
foobar = { "value" => 3 } foobar = { "value" => 3 }
assert_template_result(' 1 2 3 ', '{%for item in (1..foobar.value) %} {{item}} {%endfor%}', { "foobar" => foobar }) assert_template_result(' 1 2 3 ', '{%for item in (1..foobar.value) %} {{item}} {%endfor%}', "foobar" => foobar)
end end
def test_for_with_drop_value_range def test_for_with_drop_value_range
foobar = ThingWithValue.new foobar = ThingWithValue.new
assert_template_result(' 1 2 3 ', '{%for item in (1..foobar.value) %} {{item}} {%endfor%}', { "foobar" => foobar }) assert_template_result(' 1 2 3 ', '{%for item in (1..foobar.value) %} {{item}} {%endfor%}', "foobar" => foobar)
end end
def test_for_with_variable def test_for_with_variable
assert_template_result(' 1 2 3 ', '{%for item in array%} {{item}} {%endfor%}', { 'array' => [1, 2, 3] }) assert_template_result(' 1 2 3 ', '{%for item in array%} {{item}} {%endfor%}', 'array' => [1, 2, 3])
assert_template_result('123', '{%for item in array%}{{item}}{%endfor%}', { 'array' => [1, 2, 3] }) assert_template_result('123', '{%for item in array%}{{item}}{%endfor%}', 'array' => [1, 2, 3])
assert_template_result('123', '{% for item in array %}{{item}}{% endfor %}', { 'array' => [1, 2, 3] }) assert_template_result('123', '{% for item in array %}{{item}}{% endfor %}', 'array' => [1, 2, 3])
assert_template_result('abcd', '{%for item in array%}{{item}}{%endfor%}', { 'array' => ['a', 'b', 'c', 'd'] }) assert_template_result('abcd', '{%for item in array%}{{item}}{%endfor%}', 'array' => ['a', 'b', 'c', 'd'])
assert_template_result('a b c', '{%for item in array%}{{item}}{%endfor%}', { 'array' => ['a', ' ', 'b', ' ', 'c'] }) assert_template_result('a b c', '{%for item in array%}{{item}}{%endfor%}', 'array' => ['a', ' ', 'b', ' ', 'c'])
assert_template_result('abc', '{%for item in array%}{{item}}{%endfor%}', { 'array' => ['a', '', 'b', '', 'c'] }) assert_template_result('abc', '{%for item in array%}{{item}}{%endfor%}', 'array' => ['a', '', 'b', '', 'c'])
end end
def test_for_helpers def test_for_helpers
assigns = { 'array' => [1, 2, 3] } assigns = { 'array' => [1, 2, 3] }
assert_template_result( assert_template_result(' 1/3 2/3 3/3 ',
' 1/3 2/3 3/3 ',
'{%for item in array%} {{forloop.index}}/{{forloop.length}} {%endfor%}', '{%for item in array%} {{forloop.index}}/{{forloop.length}} {%endfor%}',
assigns, assigns)
)
assert_template_result(' 1 2 3 ', '{%for item in array%} {{forloop.index}} {%endfor%}', assigns) assert_template_result(' 1 2 3 ', '{%for item in array%} {{forloop.index}} {%endfor%}', assigns)
assert_template_result(' 0 1 2 ', '{%for item in array%} {{forloop.index0}} {%endfor%}', assigns) assert_template_result(' 0 1 2 ', '{%for item in array%} {{forloop.index0}} {%endfor%}', assigns)
assert_template_result(' 2 1 0 ', '{%for item in array%} {{forloop.rindex0}} {%endfor%}', assigns) assert_template_result(' 2 1 0 ', '{%for item in array%} {{forloop.rindex0}} {%endfor%}', assigns)
@@ -88,17 +86,15 @@ HERE
def test_for_and_if def test_for_and_if
assigns = { 'array' => [1, 2, 3] } assigns = { 'array' => [1, 2, 3] }
assert_template_result( assert_template_result('+--',
'+--',
'{%for item in array%}{% if forloop.first %}+{% else %}-{% endif %}{%endfor%}', '{%for item in array%}{% if forloop.first %}+{% else %}-{% endif %}{%endfor%}',
assigns, assigns)
)
end end
def test_for_else def test_for_else
assert_template_result('+++', '{%for item in array%}+{%else%}-{%endfor%}', { 'array' => [1, 2, 3] }) assert_template_result('+++', '{%for item in array%}+{%else%}-{%endfor%}', 'array' => [1, 2, 3])
assert_template_result('-', '{%for item in array%}+{%else%}-{%endfor%}', { 'array' => [] }) assert_template_result('-', '{%for item in array%}+{%else%}-{%endfor%}', 'array' => [])
assert_template_result('-', '{%for item in array%}+{%else%}-{%endfor%}', { 'array' => nil }) assert_template_result('-', '{%for item in array%}+{%else%}-{%endfor%}', 'array' => nil)
end end
def test_limiting def test_limiting
@@ -107,7 +103,6 @@ HERE
assert_template_result('1234', '{%for i in array limit:4 %}{{ i }}{%endfor%}', assigns) assert_template_result('1234', '{%for i in array limit:4 %}{{ i }}{%endfor%}', assigns)
assert_template_result('3456', '{%for i in array limit:4 offset:2 %}{{ i }}{%endfor%}', assigns) assert_template_result('3456', '{%for i in array limit:4 offset:2 %}{{ i }}{%endfor%}', assigns)
assert_template_result('3456', '{%for i in array limit: 4 offset: 2 %}{{ i }}{%endfor%}', assigns) assert_template_result('3456', '{%for i in array limit: 4 offset: 2 %}{{ i }}{%endfor%}', assigns)
assert_template_result('3456', '{%for i in array, limit: 4, offset: 2 %}{{ i }}{%endfor%}', assigns)
end end
def test_limiting_with_invalid_limit def test_limiting_with_invalid_limit
@@ -268,19 +263,6 @@ HERE
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
end end
def test_for_with_break_after_nested_loop
source = <<~LIQUID.chomp
{% for i in (1..2) -%}
{% for j in (1..2) -%}
{{ i }}-{{ j }},
{%- endfor -%}
{% break -%}
{% endfor -%}
after
LIQUID
assert_template_result("1-1,1-2,after", source)
end
def test_for_with_continue def test_for_with_continue
assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5] } } assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5] } }
@@ -329,20 +311,15 @@ HERE
# ruby 1.9.3 no longer supports .each on String though we mimic # ruby 1.9.3 no longer supports .each on String though we mimic
# the functionality for backwards compatibility # the functionality for backwards compatibility
assert_template_result( assert_template_result('test string',
'test string',
'{%for val in string%}{{val}}{%endfor%}', '{%for val in string%}{{val}}{%endfor%}',
{ 'string' => "test string" }, 'string' => "test string")
)
assert_template_result( assert_template_result('test string',
'test string',
'{%for val in string limit:1%}{{val}}{%endfor%}', '{%for val in string limit:1%}{{val}}{%endfor%}',
{ 'string' => "test string" }, 'string' => "test string")
)
assert_template_result( assert_template_result('val-string-1-1-0-1-0-true-true-test string',
'val-string-1-1-0-1-0-true-true-test string',
'{%for val in string%}' \ '{%for val in string%}' \
'{{forloop.name}}-' \ '{{forloop.name}}-' \
'{{forloop.index}}-' \ '{{forloop.index}}-' \
@@ -353,28 +330,23 @@ HERE
'{{forloop.first}}-' \ '{{forloop.first}}-' \
'{{forloop.last}}-' \ '{{forloop.last}}-' \
'{{val}}{%endfor%}', '{{val}}{%endfor%}',
{ 'string' => "test string" }, 'string' => "test string")
)
end end
def test_for_parentloop_references_parent_loop def test_for_parentloop_references_parent_loop
assert_template_result( assert_template_result('1.1 1.2 1.3 2.1 2.2 2.3 ',
'1.1 1.2 1.3 2.1 2.2 2.3 ',
'{% for inner in outer %}{% for k in inner %}' \ '{% for inner in outer %}{% for k in inner %}' \
'{{ forloop.parentloop.index }}.{{ forloop.index }} ' \ '{{ forloop.parentloop.index }}.{{ forloop.index }} ' \
'{% endfor %}{% endfor %}', '{% endfor %}{% endfor %}',
{ 'outer' => [[1, 1, 1], [1, 1, 1]] }, 'outer' => [[1, 1, 1], [1, 1, 1]])
)
end end
def test_for_parentloop_nil_when_not_present def test_for_parentloop_nil_when_not_present
assert_template_result( assert_template_result('.1 .2 ',
'.1 .2 ',
'{% for inner in outer %}' \ '{% for inner in outer %}' \
'{{ forloop.parentloop.index }}.{{ forloop.index }} ' \ '{{ forloop.parentloop.index }}.{{ forloop.index }} ' \
'{% endfor %}', '{% endfor %}',
{ 'outer' => [[1, 1, 1], [1, 1, 1]] }, 'outer' => [[1, 1, 1], [1, 1, 1]])
)
end end
def test_inner_for_over_empty_input def test_inner_for_over_empty_input
@@ -382,7 +354,7 @@ HERE
end end
def test_blank_string_not_iterable def test_blank_string_not_iterable
assert_template_result('', "{% for char in characters %}I WILL NOT BE OUTPUT{% endfor %}", { 'characters' => '' }) assert_template_result('', "{% for char in characters %}I WILL NOT BE OUTPUT{% endfor %}", 'characters' => '')
end end
def test_bad_variable_naming_in_for_loop def test_bad_variable_naming_in_for_loop
@@ -465,4 +437,30 @@ HERE
assert(context.registers[:for_stack].empty?) assert(context.registers[:for_stack].empty?)
end end
def test_instrument_for_offset_continue
assert_usage_increment('for_offset_continue') do
Template.parse('{% for item in items offset:continue %}{{item}}{% endfor %}')
end
assert_usage_increment('for_offset_continue', times: 0) do
Template.parse('{% for item in items offset:2 %}{{item}}{% endfor %}')
end
end
def test_instrument_forloop_drop_name
assigns = { 'items' => [1, 2, 3, 4, 5] }
assert_usage_increment('forloop_drop_name', times: 5) do
Template.parse('{% for item in items %}{{forloop.name}}{% endfor %}').render!(assigns)
end
assert_usage_increment('forloop_drop_name', times: 0) do
Template.parse('{% for item in items %}{{forloop.index}}{% endfor %}').render!(assigns)
end
assert_usage_increment('forloop_drop_name', times: 0) do
Template.parse('{% for item in items %}{{item}}{% endfor %}').render!(assigns)
end
end
end end
+47 -55
View File
@@ -7,10 +7,8 @@ class IfElseTagTest < Minitest::Test
def test_if def test_if
assert_template_result(' ', ' {% if false %} this text should not go into the output {% endif %} ') assert_template_result(' ', ' {% if false %} this text should not go into the output {% endif %} ')
assert_template_result( assert_template_result(' this text should go into the output ',
' this text should go into the output ', ' {% if true %} this text should go into the output {% endif %} ')
' {% if true %} this text should go into the output {% endif %} ',
)
assert_template_result(' you rock ?', '{% if false %} you suck {% endif %} {% if true %} you rock {% endif %}?') assert_template_result(' you rock ?', '{% if false %} you suck {% endif %} {% if true %} you rock {% endif %}?')
end end
@@ -26,23 +24,23 @@ class IfElseTagTest < Minitest::Test
end end
def test_if_boolean def test_if_boolean
assert_template_result(' YES ', '{% if var %} YES {% endif %}', { 'var' => true }) assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => true)
end end
def test_if_or def test_if_or
assert_template_result(' YES ', '{% if a or b %} YES {% endif %}', { 'a' => true, 'b' => true }) assert_template_result(' YES ', '{% if a or b %} YES {% endif %}', 'a' => true, 'b' => true)
assert_template_result(' YES ', '{% if a or b %} YES {% endif %}', { 'a' => true, 'b' => false }) assert_template_result(' YES ', '{% if a or b %} YES {% endif %}', 'a' => true, 'b' => false)
assert_template_result(' YES ', '{% if a or b %} YES {% endif %}', { 'a' => false, 'b' => true }) assert_template_result(' YES ', '{% if a or b %} YES {% endif %}', 'a' => false, 'b' => true)
assert_template_result('', '{% if a or b %} YES {% endif %}', { 'a' => false, 'b' => false }) assert_template_result('', '{% if a or b %} YES {% endif %}', 'a' => false, 'b' => false)
assert_template_result(' YES ', '{% if a or b or c %} YES {% endif %}', { 'a' => false, 'b' => false, 'c' => true }) assert_template_result(' YES ', '{% if a or b or c %} YES {% endif %}', 'a' => false, 'b' => false, 'c' => true)
assert_template_result('', '{% if a or b or c %} YES {% endif %}', { 'a' => false, 'b' => false, 'c' => false }) assert_template_result('', '{% if a or b or c %} YES {% endif %}', 'a' => false, 'b' => false, 'c' => false)
end end
def test_if_or_with_operators def test_if_or_with_operators
assert_template_result(' YES ', '{% if a == true or b == true %} YES {% endif %}', { 'a' => true, 'b' => true }) assert_template_result(' YES ', '{% if a == true or b == true %} YES {% endif %}', 'a' => true, 'b' => true)
assert_template_result(' YES ', '{% if a == true or b == false %} YES {% endif %}', { 'a' => true, 'b' => true }) assert_template_result(' YES ', '{% if a == true or b == false %} YES {% endif %}', 'a' => true, 'b' => true)
assert_template_result('', '{% if a == false or b == false %} YES {% endif %}', { 'a' => true, 'b' => true }) assert_template_result('', '{% if a == false or b == false %} YES {% endif %}', 'a' => true, 'b' => true)
end end
def test_comparison_of_strings_containing_and_or_or def test_comparison_of_strings_containing_and_or_or
@@ -53,59 +51,55 @@ class IfElseTagTest < Minitest::Test
def test_comparison_of_expressions_starting_with_and_or_or def test_comparison_of_expressions_starting_with_and_or_or
assigns = { 'order' => { 'items_count' => 0 }, 'android' => { 'name' => 'Roy' } } assigns = { 'order' => { 'items_count' => 0 }, 'android' => { 'name' => 'Roy' } }
assert_template_result( assert_template_result("YES",
"YES",
"{% if android.name == 'Roy' %}YES{% endif %}", "{% if android.name == 'Roy' %}YES{% endif %}",
assigns, assigns)
) assert_template_result("YES",
assert_template_result(
"YES",
"{% if order.items_count == 0 %}YES{% endif %}", "{% if order.items_count == 0 %}YES{% endif %}",
assigns, assigns)
)
end end
def test_if_and def test_if_and
assert_template_result(' YES ', '{% if true and true %} YES {% endif %}') assert_template_result(' YES ', '{% if true and true %} YES {% endif %}')
assert_template_result('', '{% if false and true %} YES {% endif %}') assert_template_result('', '{% if false and true %} YES {% endif %}')
assert_template_result('', '{% if true and false %} YES {% endif %}') assert_template_result('', '{% if false and true %} YES {% endif %}')
end end
def test_hash_miss_generates_false def test_hash_miss_generates_false
assert_template_result('', '{% if foo.bar %} NO {% endif %}', { 'foo' => {} }) assert_template_result('', '{% if foo.bar %} NO {% endif %}', 'foo' => {})
end end
def test_if_from_variable def test_if_from_variable
assert_template_result('', '{% if var %} NO {% endif %}', { 'var' => false }) assert_template_result('', '{% if var %} NO {% endif %}', 'var' => false)
assert_template_result('', '{% if var %} NO {% endif %}', { 'var' => nil }) assert_template_result('', '{% if var %} NO {% endif %}', 'var' => nil)
assert_template_result('', '{% if foo.bar %} NO {% endif %}', { 'foo' => { 'bar' => false } }) assert_template_result('', '{% if foo.bar %} NO {% endif %}', 'foo' => { 'bar' => false })
assert_template_result('', '{% if foo.bar %} NO {% endif %}', { 'foo' => {} }) assert_template_result('', '{% if foo.bar %} NO {% endif %}', 'foo' => {})
assert_template_result('', '{% if foo.bar %} NO {% endif %}', { 'foo' => nil }) assert_template_result('', '{% if foo.bar %} NO {% endif %}', 'foo' => nil)
assert_template_result('', '{% if foo.bar %} NO {% endif %}', { 'foo' => true }) assert_template_result('', '{% if foo.bar %} NO {% endif %}', 'foo' => true)
assert_template_result(' YES ', '{% if var %} YES {% endif %}', { 'var' => "text" }) assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => "text")
assert_template_result(' YES ', '{% if var %} YES {% endif %}', { 'var' => true }) assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => true)
assert_template_result(' YES ', '{% if var %} YES {% endif %}', { 'var' => 1 }) assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => 1)
assert_template_result(' YES ', '{% if var %} YES {% endif %}', { 'var' => {} }) assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => {})
assert_template_result(' YES ', '{% if var %} YES {% endif %}', { 'var' => [] }) assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => [])
assert_template_result(' YES ', '{% if "foo" %} YES {% endif %}') assert_template_result(' YES ', '{% if "foo" %} YES {% endif %}')
assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', { 'foo' => { 'bar' => true } }) assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => true })
assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', { 'foo' => { 'bar' => "text" } }) assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => "text" })
assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', { 'foo' => { 'bar' => 1 } }) assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => 1 })
assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', { 'foo' => { 'bar' => {} } }) assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => {} })
assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', { 'foo' => { 'bar' => [] } }) assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => [] })
assert_template_result(' YES ', '{% if var %} NO {% else %} YES {% endif %}', { 'var' => false }) assert_template_result(' YES ', '{% if var %} NO {% else %} YES {% endif %}', 'var' => false)
assert_template_result(' YES ', '{% if var %} NO {% else %} YES {% endif %}', { 'var' => nil }) assert_template_result(' YES ', '{% if var %} NO {% else %} YES {% endif %}', 'var' => nil)
assert_template_result(' YES ', '{% if var %} YES {% else %} NO {% endif %}', { 'var' => true }) assert_template_result(' YES ', '{% if var %} YES {% else %} NO {% endif %}', 'var' => true)
assert_template_result(' YES ', '{% if "foo" %} YES {% else %} NO {% endif %}', { 'var' => "text" }) assert_template_result(' YES ', '{% if "foo" %} YES {% else %} NO {% endif %}', 'var' => "text")
assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', { 'foo' => { 'bar' => false } }) assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => { 'bar' => false })
assert_template_result(' YES ', '{% if foo.bar %} YES {% else %} NO {% endif %}', { 'foo' => { 'bar' => true } }) assert_template_result(' YES ', '{% if foo.bar %} YES {% else %} NO {% endif %}', 'foo' => { 'bar' => true })
assert_template_result(' YES ', '{% if foo.bar %} YES {% else %} NO {% endif %}', { 'foo' => { 'bar' => "text" } }) assert_template_result(' YES ', '{% if foo.bar %} YES {% else %} NO {% endif %}', 'foo' => { 'bar' => "text" })
assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', { 'foo' => { 'notbar' => true } }) assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => { 'notbar' => true })
assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', { 'foo' => {} }) assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => {})
assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', { 'notfoo' => { 'bar' => true } }) assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', 'notfoo' => { 'bar' => true })
end end
def test_nested_if def test_nested_if
@@ -161,10 +155,8 @@ class IfElseTagTest < Minitest::Test
original_op = Condition.operators['contains'] original_op = Condition.operators['contains']
Condition.operators['contains'] = :[] Condition.operators['contains'] = :[]
assert_template_result( assert_template_result('yes',
'yes', %({% if 'gnomeslab-and-or-liquid' contains 'gnomeslab-and-or-liquid' %}yes{% endif %}))
%({% if 'gnomeslab-and-or-liquid' contains 'gnomeslab-and-or-liquid' %}yes{% endif %}),
)
ensure ensure
Condition.operators['contains'] = original_op Condition.operators['contains'] = original_op
end end
@@ -192,7 +184,7 @@ class IfElseTagTest < Minitest::Test
tests.each do |vals, expected| tests.each do |vals, expected|
a, b, c = vals a, b, c = vals
assigns = { 'a' => a, 'b' => b, 'c' => c } assigns = { 'a' => a, 'b' => b, 'c' => c }
assert_template_result(expected.to_s, tpl, assigns, message: assigns.to_s) assert_template_result(expected.to_s, tpl, assigns, assigns.to_s)
end end
end end
end end
+89 -200
View File
@@ -3,13 +3,44 @@
require 'test_helper' require 'test_helper'
class TestFileSystem class TestFileSystem
PARTIALS = {
"nested_template" => "{% include 'header' %} {% include 'body' %} {% include 'footer' %}",
"body" => "body {% include 'body_detail' %}",
}
def read_template_file(template_path) def read_template_file(template_path)
PARTIALS[template_path] || template_path case template_path
when "product"
"Product: {{ product.title }} "
when "product_alias"
"Product: {{ product.title }} "
when "locale_variables"
"Locale: {{echo1}} {{echo2}}"
when "variant"
"Variant: {{ variant.title }}"
when "nested_template"
"{% include 'header' %} {% include 'body' %} {% include 'footer' %}"
when "body"
"body {% include 'body_detail' %}"
when "nested_product_template"
"Product: {{ nested_product_template.title }} {%include 'details'%} "
when "recursively_nested_template"
"-{% include 'recursively_nested_template' %}"
when "pick_a_source"
"from TestFileSystem"
when 'assignments'
"{% assign foo = 'bar' %}"
when 'break'
"{% break %}"
else
template_path
end
end end
end end
@@ -50,129 +81,70 @@ class IncludeTagTest < Minitest::Test
include Liquid include Liquid
def setup def setup
@default_file_system = Liquid::Template.file_system Liquid::Template.file_system = TestFileSystem.new
end
def teardown
Liquid::Template.file_system = @default_file_system
end end
def test_include_tag_looks_for_file_system_in_registers_first def test_include_tag_looks_for_file_system_in_registers_first
assert_equal( assert_equal('from OtherFileSystem',
'from OtherFileSystem', Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: OtherFileSystem.new }))
Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: OtherFileSystem.new }),
)
end end
def test_include_tag_with def test_include_tag_with
assert_template_result( assert_template_result("Product: Draft 151cm ",
"Product: Draft 151cm ", "{% include 'product' with products[0] %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
"{% include 'product' with products[0] %}",
{ "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] },
partials: { "product" => "Product: {{ product.title }} " },
)
end end
def test_include_tag_with_alias def test_include_tag_with_alias
assert_template_result( assert_template_result("Product: Draft 151cm ",
"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' }] },
partials: { "product_alias" => "Product: {{ product.title }} " },
)
end end
def test_include_tag_for_alias def test_include_tag_for_alias
assert_template_result( assert_template_result("Product: Draft 151cm Product: Element 155cm ",
"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' }] },
partials: { "product_alias" => "Product: {{ product.title }} " },
)
end end
def test_include_tag_with_default_name def test_include_tag_with_default_name
assert_template_result( assert_template_result("Product: Draft 151cm ",
"Product: Draft 151cm ", "{% include 'product' %}", "product" => { 'title' => 'Draft 151cm' })
"{% include 'product' %}",
{ "product" => { 'title' => 'Draft 151cm' } },
partials: { "product" => "Product: {{ product.title }} " },
)
end end
def test_include_tag_for def test_include_tag_for
assert_template_result( assert_template_result("Product: Draft 151cm Product: Element 155cm ",
"Product: Draft 151cm Product: Element 155cm ", "{% include 'product' for products %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
"{% include 'product' for products %}",
{ "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] },
partials: { "product" => "Product: {{ product.title }} " },
)
end end
def test_include_tag_with_local_variables def test_include_tag_with_local_variables
assert_template_result( assert_template_result("Locale: test123 ", "{% include 'locale_variables' echo1: 'test123' %}")
"Locale: test123 ",
"{% include 'locale_variables' echo1: 'test123' %}",
partials: { "locale_variables" => "Locale: {{echo1}} {{echo2}}" },
)
end end
def test_include_tag_with_multiple_local_variables def test_include_tag_with_multiple_local_variables
assert_template_result( assert_template_result("Locale: test123 test321",
"Locale: test123 test321", "{% include 'locale_variables' echo1: 'test123', echo2: 'test321' %}")
"{% include 'locale_variables' echo1: 'test123', echo2: 'test321' %}",
partials: { "locale_variables" => "Locale: {{echo1}} {{echo2}}" },
)
end end
def test_include_tag_with_multiple_local_variables_from_context def test_include_tag_with_multiple_local_variables_from_context
assert_template_result( assert_template_result("Locale: test123 test321",
"Locale: test123 test321",
"{% include 'locale_variables' echo1: echo1, echo2: more_echos.echo2 %}", "{% include 'locale_variables' echo1: echo1, echo2: more_echos.echo2 %}",
{ 'echo1' => 'test123', 'more_echos' => { "echo2" => 'test321' } }, 'echo1' => 'test123', 'more_echos' => { "echo2" => 'test321' })
partials: { "locale_variables" => "Locale: {{echo1}} {{echo2}}" },
)
end end
def test_included_templates_assigns_variables def test_included_templates_assigns_variables
assert_template_result( assert_template_result("bar", "{% include 'assignments' %}{{ foo }}")
"bar",
"{% include 'assignments' %}{{ foo }}",
partials: { 'assignments' => "{% assign foo = 'bar' %}" },
)
end end
def test_nested_include_tag def test_nested_include_tag
partials = { "body" => "body {% include 'body_detail' %}", "body_detail" => "body_detail" } assert_template_result("body body_detail", "{% include 'body' %}")
assert_template_result("body body_detail", "{% include 'body' %}", partials: partials)
partials = partials.merge({ assert_template_result("header body body_detail footer", "{% include 'nested_template' %}")
"nested_template" => "{% include 'header' %} {% include 'body' %} {% include 'footer' %}",
"header" => "header",
"footer" => "footer",
})
assert_template_result("header body body_detail footer", "{% include 'nested_template' %}", partials: partials)
end end
def test_nested_include_with_variable def test_nested_include_with_variable
partials = { assert_template_result("Product: Draft 151cm details ",
"nested_product_template" => "Product: {{ nested_product_template.title }} {%include 'details'%} ", "{% include 'nested_product_template' with product %}", "product" => { "title" => 'Draft 151cm' })
"details" => "details",
}
assert_template_result( assert_template_result("Product: Draft 151cm details Product: Element 155cm details ",
"Product: Draft 151cm details ", "{% include 'nested_product_template' for products %}", "products" => [{ "title" => 'Draft 151cm' }, { "title" => 'Element 155cm' }])
"{% include 'nested_product_template' with product %}",
{ "product" => { "title" => 'Draft 151cm' } },
partials: partials,
)
assert_template_result(
"Product: Draft 151cm details Product: Element 155cm details ",
"{% include 'nested_product_template' for products %}",
{ "products" => [{ "title" => 'Draft 151cm' }, { "title" => 'Element 155cm' }] },
partials: partials,
)
end end
def test_recursively_included_template_does_not_produce_endless_loop def test_recursively_included_template_does_not_produce_endless_loop
@@ -190,68 +162,41 @@ class IncludeTagTest < Minitest::Test
end end
def test_dynamically_choosen_template def test_dynamically_choosen_template
assert_template_result( assert_template_result("Test123", "{% include template %}", "template" => 'Test123')
"Test123", assert_template_result("Test321", "{% include template %}", "template" => 'Test321')
"{% include template %}",
{ "template" => 'Test123' },
partials: { "Test123" => "Test123" },
)
assert_template_result( assert_template_result("Product: Draft 151cm ", "{% include template for product %}",
"Test321", "template" => 'product', 'product' => { 'title' => 'Draft 151cm' })
"{% include template %}",
{ "template" => 'Test321' },
partials: { "Test321" => "Test321" },
)
assert_template_result(
"Product: Draft 151cm ",
"{% include template for product %}",
{ "template" => 'product', 'product' => { 'title' => 'Draft 151cm' } },
partials: { "product" => "Product: {{ product.title }} " },
)
end end
def test_include_tag_caches_second_read_of_same_partial def test_include_tag_caches_second_read_of_same_partial
file_system = CountingFileSystem.new file_system = CountingFileSystem.new
assert_equal( assert_equal('from CountingFileSystemfrom CountingFileSystem',
'from CountingFileSystemfrom CountingFileSystem', Template.parse("{% include 'pick_a_source' %}{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system }))
Template.parse("{% include 'pick_a_source' %}{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system }),
)
assert_equal(1, file_system.count) assert_equal(1, file_system.count)
end end
def test_include_tag_doesnt_cache_partials_across_renders def test_include_tag_doesnt_cache_partials_across_renders
file_system = CountingFileSystem.new file_system = CountingFileSystem.new
assert_equal( assert_equal('from CountingFileSystem',
'from CountingFileSystem', Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system }))
Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system }),
)
assert_equal(1, file_system.count) assert_equal(1, file_system.count)
assert_equal( assert_equal('from CountingFileSystem',
'from CountingFileSystem', Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system }))
Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system }),
)
assert_equal(2, file_system.count) assert_equal(2, file_system.count)
end end
def test_include_tag_within_if_statement def test_include_tag_within_if_statement
assert_template_result( assert_template_result("foo_if_true", "{% if true %}{% include 'foo_if_true' %}{% endif %}")
"foo_if_true",
"{% if true %}{% include 'foo_if_true' %}{% endif %}",
partials: { "foo_if_true" => "foo_if_true" },
)
end end
def test_custom_include_tag def test_custom_include_tag
original_tag = Liquid::Template.tags['include'] original_tag = Liquid::Template.tags['include']
Liquid::Template.tags['include'] = CustomInclude Liquid::Template.tags['include'] = CustomInclude
begin begin
assert_equal( assert_equal("custom_foo",
"custom_foo", Template.parse("{% include 'custom_foo' %}").render!)
Template.parse("{% include 'custom_foo' %}").render!,
)
ensure ensure
Liquid::Template.tags['include'] = original_tag Liquid::Template.tags['include'] = original_tag
end end
@@ -261,10 +206,8 @@ class IncludeTagTest < Minitest::Test
original_tag = Liquid::Template.tags['include'] original_tag = Liquid::Template.tags['include']
Liquid::Template.tags['include'] = CustomInclude Liquid::Template.tags['include'] = CustomInclude
begin begin
assert_equal( assert_equal("custom_foo_if_true",
"custom_foo_if_true", Template.parse("{% if true %}{% include 'custom_foo_if_true' %}{% endif %}").render!)
Template.parse("{% if true %}{% include 'custom_foo_if_true' %}{% endif %}").render!,
)
ensure ensure
Liquid::Template.tags['include'] = original_tag Liquid::Template.tags['include'] = original_tag
end end
@@ -279,7 +222,6 @@ class IncludeTagTest < Minitest::Test
end end
def test_passing_options_to_included_templates def test_passing_options_to_included_templates
Liquid::Template.file_system = TestFileSystem.new
assert_raises(Liquid::SyntaxError) do assert_raises(Liquid::SyntaxError) do
Template.parse("{% include template %}", error_mode: :strict).render!("template" => '{{ "X" || downcase }}') Template.parse("{% include template %}", error_mode: :strict).render!("template" => '{{ "X" || downcase }}')
end end
@@ -295,53 +237,25 @@ class IncludeTagTest < Minitest::Test
end end
def test_render_raise_argument_error_when_template_is_undefined def test_render_raise_argument_error_when_template_is_undefined
assert_template_result( assert_raises(Liquid::ArgumentError) do
"Liquid error (line 1): Argument error in tag 'include' - Illegal template name", template = Liquid::Template.parse('{% include undefined_variable %}')
"{% include undefined_variable %}", template.render!
render_errors: true, end
) assert_raises(Liquid::ArgumentError) do
template = Liquid::Template.parse('{% include nil %}')
assert_template_result( template.render!
"Liquid error (line 1): Argument error in tag 'include' - Illegal template name", end
"{% include nil %}",
render_errors: true,
)
end
def test_render_raise_argument_error_when_template_is_not_a_string
assert_template_result(
"Liquid error (line 1): Argument error in tag 'include' - Illegal template name",
"{% include 123 %}",
render_errors: true,
)
end end
def test_including_via_variable_value def test_including_via_variable_value
assert_template_result( assert_template_result("from TestFileSystem", "{% assign page = 'pick_a_source' %}{% include page %}")
"from TestFileSystem",
"{% assign page = 'pick_a_source' %}{% include page %}",
partials: { "pick_a_source" => "from TestFileSystem" },
)
partials = { "product" => "Product: {{ product.title }} " } assert_template_result("Product: Draft 151cm ", "{% assign page = 'product' %}{% include page %}", "product" => { 'title' => 'Draft 151cm' })
assert_template_result( assert_template_result("Product: Draft 151cm ", "{% assign page = 'product' %}{% include page for foo %}", "foo" => { 'title' => 'Draft 151cm' })
"Product: Draft 151cm ",
"{% assign page = 'product' %}{% include page %}",
{ "product" => { 'title' => 'Draft 151cm' } },
partials: partials,
)
assert_template_result(
"Product: Draft 151cm ",
"{% assign page = 'product' %}{% include page for foo %}",
{ "foo" => { 'title' => 'Draft 151cm' } },
partials: partials,
)
end end
def test_including_with_strict_variables def test_including_with_strict_variables
Liquid::Template.file_system = StubFileSystem.new({ "simple" => "simple" })
template = Liquid::Template.parse("{% include 'simple' %}", error_mode: :warn) template = Liquid::Template.parse("{% include 'simple' %}", error_mode: :warn)
template.render(nil, strict_variables: true) template.render(nil, strict_variables: true)
@@ -350,31 +264,6 @@ class IncludeTagTest < Minitest::Test
def test_break_through_include def test_break_through_include
assert_template_result("1", "{% for i in (1..3) %}{{ i }}{% break %}{{ i }}{% endfor %}") assert_template_result("1", "{% for i in (1..3) %}{{ i }}{% break %}{{ i }}{% endfor %}")
assert_template_result( assert_template_result("1", "{% for i in (1..3) %}{{ i }}{% include 'break' %}{{ i }}{% endfor %}")
"1",
"{% for i in (1..3) %}{{ i }}{% include 'break' %}{{ i }}{% endfor %}",
partials: { 'break' => "{% break %}" },
)
end
def test_render_tag_renders_error_with_template_name
assert_template_result(
'Liquid error (foo line 1): standard error',
"{% include 'foo' with errors %}",
{ 'errors' => ErrorDrop.new },
partials: { 'foo' => '{{ foo.standard_error }}' },
render_errors: true,
)
end
def test_render_tag_renders_error_with_template_name_from_template_factory
assert_template_result(
'Liquid error (some/path/foo line 1): standard error',
"{% include 'foo' with errors %}",
{ 'errors' => ErrorDrop.new },
partials: { 'foo' => '{{ foo.standard_error }}' },
template_factory: StubTemplateFactory.new,
render_errors: true,
)
end end
end # IncludeTagTest end # IncludeTagTest
+8 -13
View File
@@ -6,25 +6,20 @@ class IncrementTagTest < Minitest::Test
include Liquid include Liquid
def test_inc def test_inc
assert_template_result('0 1', '{%increment port %} {{ port }}') assert_template_result('0', '{%increment port %}', {})
assert_template_result(' 0 1 2', '{{port}} {%increment port %} {%increment port%} {{port}}') assert_template_result('0 1', '{%increment port %} {%increment port%}', {})
assert_template_result( assert_template_result('0 0 1 2 1',
'0 0 1 2 1',
'{%increment port %} {%increment starboard%} ' \ '{%increment port %} {%increment starboard%} ' \
'{%increment port %} {%increment port%} ' \ '{%increment port %} {%increment port%} ' \
'{%increment starboard %}', '{%increment starboard %}', {})
)
end end
def test_dec def test_dec
assert_template_result('-1 -1', '{%decrement port %} {{ port }}', { 'port' => 10 }) assert_template_result('9', '{%decrement port %}', 'port' => 10)
assert_template_result(' -1 -2 -2', '{{port}} {%decrement port %} {%decrement port%} {{port}}') assert_template_result('-1 -2', '{%decrement port %} {%decrement port%}', {})
assert_template_result( assert_template_result('1 5 2 2 5',
'0 1 2 0 3 1 1 3',
'{%increment starboard %} {%increment starboard%} {%increment starboard%} ' \
'{%increment port %} {%increment starboard%} ' \ '{%increment port %} {%increment starboard%} ' \
'{%increment port %} {%decrement port%} ' \ '{%increment port %} {%decrement port%} ' \
'{%decrement starboard %}', '{%decrement starboard %}', 'port' => 1, 'starboard' => 5)
)
end end
end end
+39 -49
View File
@@ -5,65 +5,55 @@ require 'test_helper'
class InlineCommentTest < Minitest::Test class InlineCommentTest < Minitest::Test
include Liquid include Liquid
def test_inline_comment_returns_nothing def test_tag_in_different_styles
assert_template_result('', '{%- # this is an inline comment -%}') assert_template_result('', '{% # This text gets ignored %}')
assert_template_result('', '{%-# this is an inline comment -%}') assert_template_result('', '{%# This text gets ignored #%}')
assert_template_result('', '{% # this is an inline comment %}') assert_template_result('', '{%# This text gets ignored %}')
assert_template_result('', '{%# this is an inline comment %}') assert_template_result('', '{%#- This text gets ignored -#%}')
end end
def test_inline_comment_does_not_require_a_space_after_the_pound_sign def test_test_syntax_error
assert_template_result('', '{%#this is an inline comment%}') assert_template_result('fail', '{% #This doesnt work %}')
assert false
rescue
# ok good
end end
def test_liquid_inline_comment_returns_nothing def test_tag_ws_stripping
assert_template_result('Hey there, how are you doing today?', <<~LIQUID) 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 {%- liquid
# This is how you'd write a block comment in a liquid tag. echo "before("
# It looks a lot like what you'd have in ruby. # This text gets ignored
echo ")after"
# You can use it as inline documentation in your
# liquid blocks to explain why you're doing something.
echo "Hey there, "
# It won't affect the output.
echo "how are you doing today?"
-%} -%}
LIQUID LIQUID
assert_template_result('before()after', source)
end end
def test_inline_comment_can_be_written_on_multiple_lines def test_multiline
assert_template_result('', <<~LIQUID) assert_template_result('', '{% # this sort of comment also
{%- # will just work, because it parses
# That kind of block comment is also allowed. # as a single call to the "#" tag %}')
# It would only be a stylistic difference.
# Much like JavaScript's /* */ comments and their
# leading * on new lines.
-%}
LIQUID
end end
def test_inline_comment_multiple_pound_signs
assert_template_result('', <<~LIQUID)
{%- liquid
######################################
# We support comments like this too. #
######################################
-%}
LIQUID
end
def test_inline_comments_require_the_pound_sign_on_every_new_line
assert_match_syntax_error("Each line of comments must be prefixed by the '#' character", <<~LIQUID)
{%-
# some comment
echo 'hello world'
-%}
LIQUID
end
def test_inline_comment_does_not_support_nested_tags
assert_template_result(' -%}', "{%- # {% echo 'hello world' %} -%}")
end
end end
+3 -3
View File
@@ -6,13 +6,13 @@ class LiquidTagTest < Minitest::Test
include Liquid include Liquid
def test_liquid_tag def test_liquid_tag
assert_template_result('1 2 3', <<~LIQUID, { 'array' => [1, 2, 3] }) assert_template_result('1 2 3', <<~LIQUID, 'array' => [1, 2, 3])
{%- liquid {%- liquid
echo array | join: " " echo array | join: " "
-%} -%}
LIQUID LIQUID
assert_template_result('1 2 3', <<~LIQUID, { 'array' => [1, 2, 3] }) assert_template_result('1 2 3', <<~LIQUID, 'array' => [1, 2, 3])
{%- liquid {%- liquid
for value in array for value in array
echo value echo value
@@ -23,7 +23,7 @@ class LiquidTagTest < Minitest::Test
-%} -%}
LIQUID LIQUID
assert_template_result('4 8 12 6', <<~LIQUID, { 'array' => [1, 2, 3] }) assert_template_result('4 8 12 6', <<~LIQUID, 'array' => [1, 2, 3])
{%- liquid {%- liquid
for value in array for value in array
assign double_value = value | times: 2 assign double_value = value | times: 2
+3 -6
View File
@@ -6,15 +6,12 @@ class RawTagTest < Minitest::Test
include Liquid include Liquid
def test_tag_in_raw def test_tag_in_raw
assert_template_result( assert_template_result('{% comment %} test {% endcomment %}',
'{% comment %} test {% endcomment %}', '{% raw %}{% comment %} test {% endcomment %}{% endraw %}')
'{% raw %}{% comment %} test {% endcomment %}{% endraw %}',
)
end end
def test_output_in_raw def test_output_in_raw
# assert_template_result('{{ test }}', '{% raw %}{{ test }}{% endraw %}') assert_template_result('{{ test }}', '{% raw %}{{ test }}{% endraw %}')
assert_template_result('>{{ test }}<', '> {%- raw -%}{{ test }}{%- endraw -%} <')
end end
def test_open_tag_in_raw def test_open_tag_in_raw
+95 -170
View File
@@ -6,79 +6,53 @@ class RenderTagTest < Minitest::Test
include Liquid include Liquid
def test_render_with_no_arguments def test_render_with_no_arguments
assert_template_result( Liquid::Template.file_system = StubFileSystem.new('source' => 'rendered content')
'rendered content', assert_template_result('rendered content', '{% render "source" %}')
'{% render "source" %}',
partials: { 'source' => 'rendered content' },
)
end end
def test_render_tag_looks_for_file_system_in_registers_first def test_render_tag_looks_for_file_system_in_registers_first
assert_template_result( file_system = StubFileSystem.new('pick_a_source' => 'from register file system')
'from register file system', assert_equal('from register file system',
'{% render "pick_a_source" %}', Template.parse('{% render "pick_a_source" %}').render!({}, registers: { file_system: file_system }))
partials: { 'pick_a_source' => 'from register file system' },
)
end end
def test_render_passes_named_arguments_into_inner_scope def test_render_passes_named_arguments_into_inner_scope
assert_template_result( Liquid::Template.file_system = StubFileSystem.new('product' => '{{ inner_product.title }}')
'My Product', assert_template_result('My Product', '{% render "product", inner_product: outer_product %}',
'{% render "product", inner_product: outer_product %}', 'outer_product' => { 'title' => 'My Product' })
{ 'outer_product' => { 'title' => 'My Product' } },
partials: { 'product' => '{{ inner_product.title }}' },
)
end end
def test_render_accepts_literals_as_arguments def test_render_accepts_literals_as_arguments
assert_template_result( Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ price }}')
'123', assert_template_result('123', '{% render "snippet", price: 123 %}')
'{% render "snippet", price: 123 %}',
partials: { 'snippet' => '{{ price }}' },
)
end end
def test_render_accepts_multiple_named_arguments def test_render_accepts_multiple_named_arguments
assert_template_result( Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ one }} {{ two }}')
'1 2', assert_template_result('1 2', '{% render "snippet", one: 1, two: 2 %}')
'{% render "snippet", one: 1, two: 2 %}',
partials: { 'snippet' => '{{ one }} {{ two }}' },
)
end end
def test_render_does_not_inherit_parent_scope_variables def test_render_does_not_inherit_parent_scope_variables
assert_template_result( Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ outer_variable }}')
'', assert_template_result('', '{% assign outer_variable = "should not be visible" %}{% render "snippet" %}')
'{% assign outer_variable = "should not be visible" %}{% render "snippet" %}',
partials: { 'snippet' => '{{ outer_variable }}' },
)
end end
def test_render_does_not_inherit_variable_with_same_name_as_snippet def test_render_does_not_inherit_variable_with_same_name_as_snippet
assert_template_result( Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ snippet }}')
'', assert_template_result('', "{% assign snippet = 'should not be visible' %}{% render 'snippet' %}")
"{% assign snippet = 'should not be visible' %}{% render 'snippet' %}",
partials: { 'snippet' => '{{ snippet }}' },
)
end end
def test_render_does_not_mutate_parent_scope def test_render_does_not_mutate_parent_scope
assert_template_result( Liquid::Template.file_system = StubFileSystem.new('snippet' => '{% assign inner = 1 %}')
'', assert_template_result('', "{% render 'snippet' %}{{ inner }}")
"{% render 'snippet' %}{{ inner }}",
partials: { 'snippet' => '{% assign inner = 1 %}' },
)
end end
def test_nested_render_tag def test_nested_render_tag
assert_template_result( Liquid::Template.file_system = StubFileSystem.new(
'one two', 'one' => "one {% render 'two' %}",
"{% render 'one' %}", 'two' => 'two'
partials: {
'one' => "one {% render 'two' %}",
'two' => 'two',
},
) )
assert_template_result('one two', "{% render 'one' %}")
end end
def test_recursively_rendered_template_does_not_produce_endless_loop def test_recursively_rendered_template_does_not_produce_endless_loop
@@ -99,190 +73,141 @@ class RenderTagTest < Minitest::Test
end end
def test_dynamically_choosen_templates_are_not_allowed def test_dynamically_choosen_templates_are_not_allowed
assert_syntax_error("{% assign name = 'snippet' %}{% render name %}") Liquid::Template.file_system = StubFileSystem.new('snippet' => 'should not be rendered')
assert_raises(Liquid::SyntaxError) do
Liquid::Template.parse("{% assign name = 'snippet' %}{% render name %}")
end
end end
def test_include_tag_caches_second_read_of_same_partial def test_include_tag_caches_second_read_of_same_partial
file_system = StubFileSystem.new('snippet' => 'echo') file_system = StubFileSystem.new('snippet' => 'echo')
assert_equal( assert_equal('echoecho',
'echoecho',
Template.parse('{% render "snippet" %}{% render "snippet" %}') Template.parse('{% render "snippet" %}{% render "snippet" %}')
.render!({}, registers: { file_system: file_system }), .render!({}, registers: { file_system: file_system }))
)
assert_equal(1, file_system.file_read_count) assert_equal(1, file_system.file_read_count)
end end
def test_render_tag_doesnt_cache_partials_across_renders def test_render_tag_doesnt_cache_partials_across_renders
file_system = StubFileSystem.new('snippet' => 'my message') file_system = StubFileSystem.new('snippet' => 'my message')
assert_equal( assert_equal('my message',
'my message', Template.parse('{% include "snippet" %}').render!({}, registers: { file_system: file_system }))
Template.parse('{% include "snippet" %}').render!({}, registers: { file_system: file_system }),
)
assert_equal(1, file_system.file_read_count) assert_equal(1, file_system.file_read_count)
assert_equal( assert_equal('my message',
'my message', Template.parse('{% include "snippet" %}').render!({}, registers: { file_system: file_system }))
Template.parse('{% include "snippet" %}').render!({}, registers: { file_system: file_system }),
)
assert_equal(2, file_system.file_read_count) assert_equal(2, file_system.file_read_count)
end end
def test_render_tag_within_if_statement def test_render_tag_within_if_statement
assert_template_result( Liquid::Template.file_system = StubFileSystem.new('snippet' => 'my message')
'my message', assert_template_result('my message', '{% if true %}{% render "snippet" %}{% endif %}')
'{% if true %}{% render "snippet" %}{% endif %}',
partials: { 'snippet' => 'my message' },
)
end end
def test_break_through_render def test_break_through_render
options = { partials: { 'break' => '{% break %}' } } Liquid::Template.file_system = StubFileSystem.new('break' => '{% break %}')
assert_template_result('1', '{% for i in (1..3) %}{{ i }}{% break %}{{ i }}{% endfor %}', **options) assert_template_result('1', '{% for i in (1..3) %}{{ i }}{% break %}{{ i }}{% endfor %}')
assert_template_result('112233', '{% for i in (1..3) %}{{ i }}{% render "break" %}{{ i }}{% endfor %}', **options) assert_template_result('112233', '{% for i in (1..3) %}{{ i }}{% render "break" %}{{ i }}{% endfor %}')
end end
def test_increment_is_isolated_between_renders def test_increment_is_isolated_between_renders
assert_template_result( Liquid::Template.file_system = StubFileSystem.new('incr' => '{% increment %}')
'010', assert_template_result('010', '{% increment %}{% increment %}{% render "incr" %}')
'{% increment %}{% increment %}{% render "incr" %}',
partials: { 'incr' => '{% increment %}' },
)
end end
def test_decrement_is_isolated_between_renders def test_decrement_is_isolated_between_renders
assert_template_result( Liquid::Template.file_system = StubFileSystem.new('decr' => '{% decrement %}')
'-1-2-1', assert_template_result('-1-2-1', '{% decrement %}{% decrement %}{% render "decr" %}')
'{% decrement %}{% decrement %}{% render "decr" %}',
partials: { 'decr' => '{% decrement %}' },
)
end end
def test_includes_will_not_render_inside_render_tag def test_includes_will_not_render_inside_render_tag
assert_template_result( Liquid::Template.file_system = StubFileSystem.new(
'Liquid error (test_include line 1): include usage is not allowed in this context', 'foo' => 'bar',
'{% render "test_include" %}', 'test_include' => '{% include "foo" %}'
render_errors: true,
partials: {
'foo' => 'bar',
'test_include' => '{% include "foo" %}',
},
) )
exc = assert_raises(Liquid::DisabledError) do
Liquid::Template.parse('{% render "test_include" %}').render!
end
assert_equal('Liquid error: include usage is not allowed in this context', exc.message)
end end
def test_includes_will_not_render_inside_nested_sibling_tags def test_includes_will_not_render_inside_nested_sibling_tags
assert_template_result( Liquid::Template.file_system = StubFileSystem.new(
"Liquid error (test_include line 1): include usage is not allowed in this context" \ 'foo' => 'bar',
"Liquid error (nested_render_with_sibling_include line 1): include usage is not allowed in this context", 'nested_render_with_sibling_include' => '{% render "test_include" %}{% include "foo" %}',
'{% render "nested_render_with_sibling_include" %}', 'test_include' => '{% include "foo" %}'
partials: {
'foo' => 'bar',
'nested_render_with_sibling_include' => '{% render "test_include" %}{% include "foo" %}',
'test_include' => '{% include "foo" %}',
},
render_errors: true,
) )
output = Liquid::Template.parse('{% render "nested_render_with_sibling_include" %}').render
assert_equal('Liquid error: include usage is not allowed in this contextLiquid error: include usage is not allowed in this context', output)
end end
def test_render_tag_with def test_render_tag_with
assert_template_result( Liquid::Template.file_system = StubFileSystem.new(
"Product: Draft 151cm ", 'product' => "Product: {{ product.title }} ",
"{% render 'product' with products[0] %}", 'product_alias' => "Product: {{ product.title }} ",
{ "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] },
partials: {
'product' => "Product: {{ product.title }} ",
'product_alias' => "Product: {{ product.title }} ",
},
) )
assert_template_result("Product: Draft 151cm ",
"{% render 'product' with products[0] %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end end
def test_render_tag_with_alias def test_render_tag_with_alias
assert_template_result( Liquid::Template.file_system = StubFileSystem.new(
"Product: Draft 151cm ", 'product' => "Product: {{ product.title }} ",
"{% render 'product_alias' with products[0] as product %}", 'product_alias' => "Product: {{ product.title }} ",
{ "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] },
partials: {
'product' => "Product: {{ product.title }} ",
'product_alias' => "Product: {{ product.title }} ",
},
) )
assert_template_result("Product: Draft 151cm ",
"{% render 'product_alias' with products[0] as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end end
def test_render_tag_for_alias def test_render_tag_for_alias
assert_template_result( Liquid::Template.file_system = StubFileSystem.new(
"Product: Draft 151cm Product: Element 155cm ", 'product' => "Product: {{ product.title }} ",
"{% render 'product_alias' for products as product %}", 'product_alias' => "Product: {{ product.title }} ",
{ "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] },
partials: {
'product' => "Product: {{ product.title }} ",
'product_alias' => "Product: {{ product.title }} ",
},
) )
assert_template_result("Product: Draft 151cm Product: Element 155cm ",
"{% render 'product_alias' for products as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end end
def test_render_tag_for def test_render_tag_for
assert_template_result( Liquid::Template.file_system = StubFileSystem.new(
"Product: Draft 151cm Product: Element 155cm ", 'product' => "Product: {{ product.title }} ",
"{% render 'product' for products %}", 'product_alias' => "Product: {{ product.title }} ",
{ "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] },
partials: {
'product' => "Product: {{ product.title }} ",
'product_alias' => "Product: {{ product.title }} ",
},
) )
assert_template_result("Product: Draft 151cm Product: Element 155cm ",
"{% render 'product' for products %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end end
def test_render_tag_forloop def test_render_tag_forloop
assert_template_result( Liquid::Template.file_system = StubFileSystem.new(
"Product: Draft 151cm first index:1 Product: Element 155cm last index:2 ", 'product' => "Product: {{ product.title }} {% if forloop.first %}first{% endif %} {% if forloop.last %}last{% endif %} index:{{ forloop.index }} ",
"{% render 'product' for products %}",
{ "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] },
partials: {
'product' => "Product: {{ product.title }} {% if forloop.first %}first{% endif %} {% if forloop.last %}last{% endif %} index:{{ forloop.index }} ",
},
) )
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' }])
end end
def test_render_tag_for_drop def test_render_tag_for_drop
assert_template_result( Liquid::Template.file_system = StubFileSystem.new(
"123", 'loop' => "{{ value.foo }}",
"{% render 'loop' for loop as value %}",
{ "loop" => TestEnumerable.new },
partials: {
'loop' => "{{ value.foo }}",
},
) )
assert_template_result("123",
"{% render 'loop' for loop as value %}", "loop" => TestEnumerable.new)
end end
def test_render_tag_with_drop def test_render_tag_with_drop
assert_template_result( Liquid::Template.file_system = StubFileSystem.new(
"TestEnumerable", 'loop' => "{{ value }}",
"{% render 'loop' with loop as value %}",
{ "loop" => TestEnumerable.new },
partials: {
'loop' => "{{ value }}",
},
) )
end
def test_render_tag_renders_error_with_template_name assert_template_result("TestEnumerable",
assert_template_result( "{% render 'loop' with loop as value %}", "loop" => TestEnumerable.new)
'Liquid error (foo line 1): standard error',
"{% render 'foo' with errors %}",
{ 'errors' => ErrorDrop.new },
partials: { 'foo' => '{{ foo.standard_error }}' },
render_errors: true,
)
end
def test_render_tag_renders_error_with_template_name_from_template_factory
assert_template_result(
'Liquid error (some/path/foo line 1): standard error',
"{% render 'foo' with errors %}",
{ 'errors' => ErrorDrop.new },
partials: { 'foo' => '{{ foo.standard_error }}' },
template_factory: StubTemplateFactory.new,
render_errors: true,
)
end end
end end
+84 -138
View File
@@ -6,10 +6,8 @@ class StandardTagTest < Minitest::Test
include Liquid include Liquid
def test_no_transform def test_no_transform
assert_template_result( assert_template_result('this text should come out of the template without change...',
'this text should come out of the template without change...', 'this text should come out of the template without change...')
'this text should come out of the template without change...',
)
assert_template_result('blah', 'blah') assert_template_result('blah', 'blah')
assert_template_result('<blah>', '<blah>') assert_template_result('<blah>', '<blah>')
@@ -22,10 +20,8 @@ class StandardTagTest < Minitest::Test
end end
def test_has_a_block_which_does_nothing def test_has_a_block_which_does_nothing
assert_template_result( assert_template_result(%(the comment block should be removed .. right?),
%(the comment block should be removed .. right?), %(the comment block should be removed {%comment%} be gone.. {%endcomment%} .. right?))
%(the comment block should be removed {%comment%} be gone.. {%endcomment%} .. right?),
)
assert_template_result('', '{%comment%}{%endcomment%}') assert_template_result('', '{%comment%}{%endcomment%}')
assert_template_result('', '{%comment%}{% endcomment %}') assert_template_result('', '{%comment%}{% endcomment %}')
@@ -40,8 +36,6 @@ class StandardTagTest < Minitest::Test
assert_template_result('', '{%comment%}{% endif %}{%endcomment%}') assert_template_result('', '{%comment%}{% endif %}{%endcomment%}')
assert_template_result('', '{% comment %}{% endwhatever %}{% endcomment %}') assert_template_result('', '{% comment %}{% endwhatever %}{% endcomment %}')
assert_template_result('', '{% comment %}{% raw %} {{%%%%}} }} { {% endcomment %} {% comment {% endraw %} {% endcomment %}') assert_template_result('', '{% comment %}{% raw %} {{%%%%}} }} { {% endcomment %} {% comment {% endraw %} {% endcomment %}')
assert_template_result('', '{% comment %}{% " %}{% endcomment %}')
assert_template_result('', '{% comment %}{%%}{% endcomment %}')
assert_template_result('foobar', 'foo{%comment%}comment{%endcomment%}bar') assert_template_result('foobar', 'foo{%comment%}comment{%endcomment%}bar')
assert_template_result('foobar', 'foo{% comment %}comment{% endcomment %}bar') assert_template_result('foobar', 'foo{% comment %}comment{% endcomment %}bar')
@@ -68,199 +62,160 @@ class StandardTagTest < Minitest::Test
def test_capture def test_capture
assigns = { 'var' => 'content' } assigns = { 'var' => 'content' }
assert_template_result( assert_template_result('content foo content foo ',
'content foo content foo ',
'{{ var2 }}{% capture var2 %}{{ var }} foo {% endcapture %}{{ var2 }}{{ var2 }}', '{{ var2 }}{% capture var2 %}{{ var }} foo {% endcapture %}{{ var2 }}{{ var2 }}',
assigns, assigns)
)
end end
def test_capture_detects_bad_syntax def test_capture_detects_bad_syntax
assert_raises(SyntaxError) do assert_raises(SyntaxError) do
assert_template_result( assert_template_result('content foo content foo ',
'content foo content foo ',
'{{ var2 }}{% capture %}{{ var }} foo {% endcapture %}{{ var2 }}{{ var2 }}', '{{ var2 }}{% capture %}{{ var }} foo {% endcapture %}{{ var2 }}{{ var2 }}',
{ 'var' => 'content' }, 'var' => 'content')
)
end end
end end
def test_case def test_case
assigns = { 'condition' => 2 } assigns = { 'condition' => 2 }
assert_template_result( assert_template_result(' its 2 ',
' its 2 ',
'{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}', '{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}',
assigns, assigns)
)
assigns = { 'condition' => 1 } assigns = { 'condition' => 1 }
assert_template_result( assert_template_result(' its 1 ',
' its 1 ',
'{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}', '{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}',
assigns, assigns)
)
assigns = { 'condition' => 3 } assigns = { 'condition' => 3 }
assert_template_result( assert_template_result('',
'',
'{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}', '{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}',
assigns, assigns)
)
assigns = { 'condition' => "string here" } assigns = { 'condition' => "string here" }
assert_template_result( assert_template_result(' hit ',
' hit ',
'{% case condition %}{% when "string here" %} hit {% endcase %}', '{% case condition %}{% when "string here" %} hit {% endcase %}',
assigns, assigns)
)
assigns = { 'condition' => "bad string here" } assigns = { 'condition' => "bad string here" }
assert_template_result( assert_template_result('',
'',
'{% case condition %}{% when "string here" %} hit {% endcase %}',\ '{% case condition %}{% when "string here" %} hit {% endcase %}',\
assigns, assigns)
)
end end
def test_case_with_else def test_case_with_else
assigns = { 'condition' => 5 } assigns = { 'condition' => 5 }
assert_template_result( assert_template_result(' hit ',
' hit ',
'{% case condition %}{% when 5 %} hit {% else %} else {% endcase %}', '{% case condition %}{% when 5 %} hit {% else %} else {% endcase %}',
assigns, assigns)
)
assigns = { 'condition' => 6 } assigns = { 'condition' => 6 }
assert_template_result( assert_template_result(' else ',
' else ',
'{% case condition %}{% when 5 %} hit {% else %} else {% endcase %}', '{% case condition %}{% when 5 %} hit {% else %} else {% endcase %}',
assigns, assigns)
)
assigns = { 'condition' => 6 } assigns = { 'condition' => 6 }
assert_template_result( assert_template_result(' else ',
' else ',
'{% case condition %} {% when 5 %} hit {% else %} else {% endcase %}', '{% case condition %} {% when 5 %} hit {% else %} else {% endcase %}',
assigns, assigns)
)
end end
def test_case_on_size def test_case_on_size
assert_template_result('', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', { 'a' => [] }) assert_template_result('', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [])
assert_template_result('1', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', { 'a' => [1] }) assert_template_result('1', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1])
assert_template_result('2', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', { 'a' => [1, 1] }) assert_template_result('2', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1, 1])
assert_template_result('', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', { 'a' => [1, 1, 1] }) assert_template_result('', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1, 1, 1])
assert_template_result('', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', { 'a' => [1, 1, 1, 1] }) assert_template_result('', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1, 1, 1, 1])
assert_template_result('', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', { 'a' => [1, 1, 1, 1, 1] }) assert_template_result('', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1, 1, 1, 1, 1])
end end
def test_case_on_size_with_else def test_case_on_size_with_else
assert_template_result( assert_template_result('else',
'else',
'{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}',
{ 'a' => [] }, 'a' => [])
)
assert_template_result( assert_template_result('1',
'1',
'{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}',
{ 'a' => [1] }, 'a' => [1])
)
assert_template_result( assert_template_result('2',
'2',
'{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}',
{ 'a' => [1, 1] }, 'a' => [1, 1])
)
assert_template_result( assert_template_result('else',
'else',
'{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}',
{ 'a' => [1, 1, 1] }, 'a' => [1, 1, 1])
)
assert_template_result( assert_template_result('else',
'else',
'{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}',
{ 'a' => [1, 1, 1, 1] }, 'a' => [1, 1, 1, 1])
)
assert_template_result( assert_template_result('else',
'else',
'{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}',
{ 'a' => [1, 1, 1, 1, 1] }, 'a' => [1, 1, 1, 1, 1])
)
end end
def test_case_on_length_with_else def test_case_on_length_with_else
assert_template_result( assert_template_result('else',
'else',
'{% case a.empty? %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', '{% case a.empty? %}{% when true %}true{% when false %}false{% else %}else{% endcase %}',
{}, {})
)
assert_template_result( assert_template_result('false',
'false',
'{% case false %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', '{% case false %}{% when true %}true{% when false %}false{% else %}else{% endcase %}',
{}, {})
)
assert_template_result( assert_template_result('true',
'true',
'{% case true %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', '{% case true %}{% when true %}true{% when false %}false{% else %}else{% endcase %}',
{}, {})
)
assert_template_result( assert_template_result('else',
'else',
'{% case NULL %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', '{% case NULL %}{% when true %}true{% when false %}false{% else %}else{% endcase %}',
{}, {})
)
end end
def test_assign_from_case def test_assign_from_case
# Example from the shopify forums # Example from the shopify forums
code = "{% case collection.handle %}{% when 'menswear-jackets' %}{% assign ptitle = 'menswear' %}{% when 'menswear-t-shirts' %}{% assign ptitle = 'menswear' %}{% else %}{% assign ptitle = 'womenswear' %}{% endcase %}{{ ptitle }}" code = "{% case collection.handle %}{% when 'menswear-jackets' %}{% assign ptitle = 'menswear' %}{% when 'menswear-t-shirts' %}{% assign ptitle = 'menswear' %}{% else %}{% assign ptitle = 'womenswear' %}{% endcase %}{{ ptitle }}"
assert_template_result("menswear", code, { "collection" => { 'handle' => 'menswear-jackets' } }) template = Liquid::Template.parse(code)
assert_template_result("menswear", code, { "collection" => { 'handle' => 'menswear-t-shirts' } }) assert_equal("menswear", template.render!("collection" => { 'handle' => 'menswear-jackets' }))
assert_template_result("womenswear", code, { "collection" => { 'handle' => 'x' } }) assert_equal("menswear", template.render!("collection" => { 'handle' => 'menswear-t-shirts' }))
assert_template_result("womenswear", code, { "collection" => { 'handle' => 'y' } }) assert_equal("womenswear", template.render!("collection" => { 'handle' => 'x' }))
assert_template_result("womenswear", code, { "collection" => { 'handle' => 'z' } }) assert_equal("womenswear", template.render!("collection" => { 'handle' => 'y' }))
assert_equal("womenswear", template.render!("collection" => { 'handle' => 'z' }))
end end
def test_case_when_or def test_case_when_or
code = '{% case condition %}{% when 1 or 2 or 3 %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' code = '{% case condition %}{% when 1 or 2 or 3 %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}'
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 1 }) assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 1)
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 2 }) assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 2)
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 3 }) assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 3)
assert_template_result(' its 4 ', code, { 'condition' => 4 }) assert_template_result(' its 4 ', code, 'condition' => 4)
assert_template_result('', code, { 'condition' => 5 }) assert_template_result('', code, 'condition' => 5)
code = '{% case condition %}{% when 1 or "string" or null %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' code = '{% case condition %}{% when 1 or "string" or null %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}'
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 1 }) assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 1)
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 'string' }) assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 'string')
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => nil }) assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => nil)
assert_template_result('', code, { 'condition' => 'something else' }) assert_template_result('', code, 'condition' => 'something else')
end end
def test_case_when_comma def test_case_when_comma
code = '{% case condition %}{% when 1, 2, 3 %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' code = '{% case condition %}{% when 1, 2, 3 %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}'
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 1 }) assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 1)
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 2 }) assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 2)
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 3 }) assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 3)
assert_template_result(' its 4 ', code, { 'condition' => 4 }) assert_template_result(' its 4 ', code, 'condition' => 4)
assert_template_result('', code, { 'condition' => 5 }) assert_template_result('', code, 'condition' => 5)
code = '{% case condition %}{% when 1, "string", null %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' code = '{% case condition %}{% when 1, "string", null %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}'
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 1 }) assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 1)
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 'string' }) assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 'string')
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => nil }) assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => nil)
assert_template_result('', code, { 'condition' => 'something else' }) assert_template_result('', code, 'condition' => 'something else')
end end
def test_case_when_comma_and_blank_body def test_case_when_comma_and_blank_body
code = '{% case condition %}{% when 1, 2 %} {% assign r = "result" %} {% endcase %}{{ r }}' code = '{% case condition %}{% when 1, 2 %} {% assign r = "result" %} {% endcase %}{{ r }}'
assert_template_result('result', code, { 'condition' => 2 }) assert_template_result('result', code, 'condition' => 2)
end end
def test_assign def test_assign
@@ -297,33 +252,24 @@ class StandardTagTest < Minitest::Test
assert_template_result('one two one', '{%cycle "one", "two"%} {%cycle "one", "two"%} {%cycle "one", "two"%}') assert_template_result('one two one', '{%cycle "one", "two"%} {%cycle "one", "two"%} {%cycle "one", "two"%}')
assert_template_result( assert_template_result('text-align: left text-align: right',
'text-align: left text-align: right', '{%cycle "text-align: left", "text-align: right" %} {%cycle "text-align: left", "text-align: right"%}')
'{%cycle "text-align: left", "text-align: right" %} {%cycle "text-align: left", "text-align: right"%}',
)
end end
def test_multiple_cycles def test_multiple_cycles
assert_template_result( assert_template_result('1 2 1 1 2 3 1',
'1 2 1 1 2 3 1', '{%cycle 1,2%} {%cycle 1,2%} {%cycle 1,2%} {%cycle 1,2,3%} {%cycle 1,2,3%} {%cycle 1,2,3%} {%cycle 1,2,3%}')
'{%cycle 1,2%} {%cycle 1,2%} {%cycle 1,2%} {%cycle 1,2,3%} {%cycle 1,2,3%} {%cycle 1,2,3%} {%cycle 1,2,3%}',
)
end end
def test_multiple_named_cycles def test_multiple_named_cycles
assert_template_result( assert_template_result('one one two two one one',
'one one two two one one', '{%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %} {%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %} {%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %}')
'{%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %} {%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %} {%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %}',
)
end end
def test_multiple_named_cycles_with_names_from_context def test_multiple_named_cycles_with_names_from_context
assigns = { "var1" => 1, "var2" => 2 } assigns = { "var1" => 1, "var2" => 2 }
assert_template_result( assert_template_result('one one two two one one',
'one one two two one one', '{%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %} {%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %} {%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %}', assigns)
'{%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %} {%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %} {%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %}',
assigns,
)
end end
def test_size_of_array def test_size_of_array
+10 -10
View File
@@ -60,22 +60,22 @@ class StatementsTest < Minitest::Test
def test_var_strings_equal def test_var_strings_equal
text = ' {% if var == "hello there!" %} true {% else %} false {% endif %} ' text = ' {% if var == "hello there!" %} true {% else %} false {% endif %} '
assert_template_result(' true ', text, { 'var' => 'hello there!' }) assert_template_result(' true ', text, 'var' => 'hello there!')
end end
def test_var_strings_are_not_equal def test_var_strings_are_not_equal
text = ' {% if "hello there!" == var %} true {% else %} false {% endif %} ' text = ' {% if "hello there!" == var %} true {% else %} false {% endif %} '
assert_template_result(' true ', text, { 'var' => 'hello there!' }) assert_template_result(' true ', text, 'var' => 'hello there!')
end end
def test_var_and_long_string_are_equal def test_var_and_long_string_are_equal
text = " {% if var == 'hello there!' %} true {% else %} false {% endif %} " text = " {% if var == 'hello there!' %} true {% else %} false {% endif %} "
assert_template_result(' true ', text, { 'var' => 'hello there!' }) assert_template_result(' true ', text, 'var' => 'hello there!')
end end
def test_var_and_long_string_are_equal_backwards def test_var_and_long_string_are_equal_backwards
text = " {% if 'hello there!' == var %} true {% else %} false {% endif %} " text = " {% if 'hello there!' == var %} true {% else %} false {% endif %} "
assert_template_result(' true ', text, { 'var' => 'hello there!' }) assert_template_result(' true ', text, 'var' => 'hello there!')
end end
# def test_is_nil # def test_is_nil
@@ -87,27 +87,27 @@ class StatementsTest < Minitest::Test
def test_is_collection_empty def test_is_collection_empty
text = ' {% if array == empty %} true {% else %} false {% endif %} ' text = ' {% if array == empty %} true {% else %} false {% endif %} '
assert_template_result(' true ', text, { 'array' => [] }) assert_template_result(' true ', text, 'array' => [])
end end
def test_is_not_collection_empty def test_is_not_collection_empty
text = ' {% if array == empty %} true {% else %} false {% endif %} ' text = ' {% if array == empty %} true {% else %} false {% endif %} '
assert_template_result(' false ', text, { 'array' => [1, 2, 3] }) assert_template_result(' false ', text, 'array' => [1, 2, 3])
end end
def test_nil def test_nil
text = ' {% if var == nil %} true {% else %} false {% endif %} ' text = ' {% if var == nil %} true {% else %} false {% endif %} '
assert_template_result(' true ', text, { 'var' => nil }) assert_template_result(' true ', text, 'var' => nil)
text = ' {% if var == null %} true {% else %} false {% endif %} ' text = ' {% if var == null %} true {% else %} false {% endif %} '
assert_template_result(' true ', text, { 'var' => nil }) assert_template_result(' true ', text, 'var' => nil)
end end
def test_not_nil def test_not_nil
text = ' {% if var != nil %} true {% else %} false {% endif %} ' text = ' {% if var != nil %} true {% else %} false {% endif %} '
assert_template_result(' true ', text, { 'var' => 1 }) assert_template_result(' true ', text, 'var' => 1)
text = ' {% if var != null %} true {% else %} false {% endif %} ' text = ' {% if var != null %} true {% else %} false {% endif %} '
assert_template_result(' true ', text, { 'var' => 1 }) assert_template_result(' true ', text, 'var' => 1)
end end
end # StatementsTest end # StatementsTest
+17 -161
View File
@@ -18,193 +18,49 @@ class TableRowTest < Minitest::Test
end end
def test_table_row def test_table_row
assert_template_result( assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
"<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
'{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}', '{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}',
{ 'numbers' => [1, 2, 3, 4, 5, 6] }, 'numbers' => [1, 2, 3, 4, 5, 6])
)
assert_template_result( assert_template_result("<tr class=\"row1\">\n</tr>\n",
"<tr class=\"row1\">\n</tr>\n",
'{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}', '{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}',
{ 'numbers' => [] }, 'numbers' => [])
)
end end
def test_table_row_with_different_cols def test_table_row_with_different_cols
assert_template_result( assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td><td class=\"col4\"> 4 </td><td class=\"col5\"> 5 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 6 </td></tr>\n",
"<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td><td class=\"col4\"> 4 </td><td class=\"col5\"> 5 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 6 </td></tr>\n",
'{% tablerow n in numbers cols:5%} {{n}} {% endtablerow %}', '{% tablerow n in numbers cols:5%} {{n}} {% endtablerow %}',
{ 'numbers' => [1, 2, 3, 4, 5, 6] }, 'numbers' => [1, 2, 3, 4, 5, 6])
)
end end
def test_table_col_counter def test_table_col_counter
assert_template_result( assert_template_result("<tr class=\"row1\">\n<td class=\"col1\">1</td><td class=\"col2\">2</td></tr>\n<tr class=\"row2\"><td class=\"col1\">1</td><td class=\"col2\">2</td></tr>\n<tr class=\"row3\"><td class=\"col1\">1</td><td class=\"col2\">2</td></tr>\n",
"<tr class=\"row1\">\n<td class=\"col1\">1</td><td class=\"col2\">2</td></tr>\n<tr class=\"row2\"><td class=\"col1\">1</td><td class=\"col2\">2</td></tr>\n<tr class=\"row3\"><td class=\"col1\">1</td><td class=\"col2\">2</td></tr>\n",
'{% tablerow n in numbers cols:2%}{{tablerowloop.col}}{% endtablerow %}', '{% tablerow n in numbers cols:2%}{{tablerowloop.col}}{% endtablerow %}',
{ 'numbers' => [1, 2, 3, 4, 5, 6] }, 'numbers' => [1, 2, 3, 4, 5, 6])
)
end end
def test_quoted_fragment def test_quoted_fragment
assert_template_result( assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
"<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
"{% tablerow n in collections.frontpage cols:3%} {{n}} {% endtablerow %}", "{% tablerow n in collections.frontpage cols:3%} {{n}} {% endtablerow %}",
{ 'collections' => { 'frontpage' => [1, 2, 3, 4, 5, 6] } }, 'collections' => { 'frontpage' => [1, 2, 3, 4, 5, 6] })
) assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
assert_template_result(
"<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
"{% tablerow n in collections['frontpage'] cols:3%} {{n}} {% endtablerow %}", "{% tablerow n in collections['frontpage'] cols:3%} {{n}} {% endtablerow %}",
{ 'collections' => { 'frontpage' => [1, 2, 3, 4, 5, 6] } }, 'collections' => { 'frontpage' => [1, 2, 3, 4, 5, 6] })
)
end end
def test_enumerable_drop def test_enumerable_drop
assert_template_result( assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
"<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
'{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}', '{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}',
{ 'numbers' => ArrayDrop.new([1, 2, 3, 4, 5, 6]) }, 'numbers' => ArrayDrop.new([1, 2, 3, 4, 5, 6]))
)
end end
def test_offset_and_limit def test_offset_and_limit
assert_template_result( assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
"<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
'{% tablerow n in numbers cols:3 offset:1 limit:6%} {{n}} {% endtablerow %}', '{% tablerow n in numbers cols:3 offset:1 limit:6%} {{n}} {% endtablerow %}',
{ 'numbers' => [0, 1, 2, 3, 4, 5, 6, 7] }, 'numbers' => [0, 1, 2, 3, 4, 5, 6, 7])
)
end end
def test_blank_string_not_iterable def test_blank_string_not_iterable
assert_template_result( assert_template_result("<tr class=\"row1\">\n</tr>\n", "{% tablerow char in characters cols:3 %}I WILL NOT BE OUTPUT{% endtablerow %}", 'characters' => '')
"<tr class=\"row1\">\n</tr>\n",
"{% tablerow char in characters cols:3 %}I WILL NOT BE OUTPUT{% endtablerow %}",
{ 'characters' => '' },
)
end
def test_cols_nil_constant_same_as_evaluated_nil_expression
expect = "<tr class=\"row1\">\n" \
"<td class=\"col1\">false</td>" \
"<td class=\"col2\">false</td>" \
"</tr>\n"
assert_template_result(
expect,
"{% tablerow i in (1..2) cols:nil %}{{ tablerowloop.col_last }}{% endtablerow %}",
)
assert_template_result(
expect,
"{% tablerow i in (1..2) cols:var %}{{ tablerowloop.col_last }}{% endtablerow %}",
{ "var" => nil },
)
end
def test_nil_limit_is_treated_as_zero
expect = "<tr class=\"row1\">\n" \
"</tr>\n"
assert_template_result(
expect,
"{% tablerow i in (1..2) limit:nil %}{{ i }}{% endtablerow %}",
)
assert_template_result(
expect,
"{% tablerow i in (1..2) limit:var %}{{ i }}{% endtablerow %}",
{ "var" => nil },
)
end
def test_nil_offset_is_treated_as_zero
expect = "<tr class=\"row1\">\n" \
"<td class=\"col1\">1:false</td>" \
"<td class=\"col2\">2:true</td>" \
"</tr>\n"
assert_template_result(
expect,
"{% tablerow i in (1..2) offset:nil %}{{ i }}:{{ tablerowloop.col_last }}{% endtablerow %}",
)
assert_template_result(
expect,
"{% tablerow i in (1..2) offset:var %}{{ i }}:{{ tablerowloop.col_last }}{% endtablerow %}",
{ "var" => nil },
)
end
def test_tablerow_loop_drop_attributes
template = <<~LIQUID.chomp
{% tablerow i in (1...2) %}
col: {{ tablerowloop.col }}
col0: {{ tablerowloop.col0 }}
col_first: {{ tablerowloop.col_first }}
col_last: {{ tablerowloop.col_last }}
first: {{ tablerowloop.first }}
index: {{ tablerowloop.index }}
index0: {{ tablerowloop.index0 }}
last: {{ tablerowloop.last }}
length: {{ tablerowloop.length }}
rindex: {{ tablerowloop.rindex }}
rindex0: {{ tablerowloop.rindex0 }}
row: {{ tablerowloop.row }}
{% endtablerow %}
LIQUID
expected_output = <<~OUTPUT
<tr class="row1">
<td class="col1">
col: 1
col0: 0
col_first: true
col_last: false
first: true
index: 1
index0: 0
last: false
length: 2
rindex: 2
rindex0: 1
row: 1
</td><td class="col2">
col: 2
col0: 1
col_first: false
col_last: true
first: false
index: 2
index0: 1
last: true
length: 2
rindex: 1
rindex0: 0
row: 1
</td></tr>
OUTPUT
assert_template_result(expected_output, template)
end
def test_table_row_renders_correct_error_message_for_invalid_parameters
assert_template_result(
"Liquid error (line 1): invalid integer",
'{% tablerow n in (1...10) limit:true %} {{n}} {% endtablerow %}',
render_errors: true,
)
assert_template_result(
"Liquid error (line 1): invalid integer",
'{% tablerow n in (1...10) offset:true %} {{n}} {% endtablerow %}',
render_errors: true,
)
assert_template_result(
"Liquid error (line 1): invalid integer",
'{% tablerow n in (1...10) cols:true %} {{n}} {% endtablerow %}',
render_errors: true,
)
end end
end end
@@ -7,10 +7,8 @@ class UnlessElseTagTest < Minitest::Test
def test_unless def test_unless
assert_template_result(' ', ' {% unless true %} this text should not go into the output {% endunless %} ') assert_template_result(' ', ' {% unless true %} this text should not go into the output {% endunless %} ')
assert_template_result( assert_template_result(' this text should go into the output ',
' this text should go into the output ', ' {% unless false %} this text should go into the output {% endunless %} ')
' {% unless false %} this text should go into the output {% endunless %} ',
)
assert_template_result(' you rock ?', '{% unless true %} you suck {% endunless %} {% unless false %} you rock {% endunless %}?') assert_template_result(' you rock ?', '{% unless true %} you suck {% endunless %} {% unless false %} you rock {% endunless %}?')
end end
@@ -21,10 +19,10 @@ class UnlessElseTagTest < Minitest::Test
end end
def test_unless_in_loop def test_unless_in_loop
assert_template_result('23', '{% for i in choices %}{% unless i %}{{ forloop.index }}{% endunless %}{% endfor %}', { 'choices' => [1, nil, false] }) assert_template_result('23', '{% for i in choices %}{% unless i %}{{ forloop.index }}{% endunless %}{% endfor %}', 'choices' => [1, nil, false])
end end
def test_unless_else_in_loop def test_unless_else_in_loop
assert_template_result(' TRUE 2 3 ', '{% for i in choices %}{% unless i %} {{ forloop.index }} {% else %} TRUE {% endunless %}{% endfor %}', { 'choices' => [1, nil, false] }) assert_template_result(' TRUE 2 3 ', '{% for i in choices %}{% unless i %} {{ forloop.index }} {% else %} TRUE {% endunless %}{% endfor %}', 'choices' => [1, nil, false])
end end
end # UnlessElseTest end # UnlessElseTest
+10 -24
View File
@@ -93,12 +93,10 @@ class TemplateTest < Minitest::Test
def test_lambda_is_called_once_from_custom_assigns_over_multiple_parses_and_renders def test_lambda_is_called_once_from_custom_assigns_over_multiple_parses_and_renders
t = Template.new t = Template.new
assigns = { assigns = { 'number' => -> {
'number' => -> { @global ||= 0
@global ||= 0 @global += 1
@global += 1 } }
},
}
assert_equal('1', t.parse("{{number}}").render!(assigns)) assert_equal('1', t.parse("{{number}}").render!(assigns))
assert_equal('1', t.parse("{{number}}").render!(assigns)) assert_equal('1', t.parse("{{number}}").render!(assigns))
assert_equal('1', t.render!(assigns)) assert_equal('1', t.render!(assigns))
@@ -317,24 +315,12 @@ class TemplateTest < Minitest::Test
end end
def test_using_range_literal_works_as_expected def test_using_range_literal_works_as_expected
source = "{% assign foo = (x..y) %}{{ foo }}" t = Template.parse("{% assign foo = (x..y) %}{{ foo }}")
assert_template_result("1..5", source, { "x" => 1, "y" => 5 }) result = t.render('x' => 1, 'y' => 5)
assert_equal('1..5', result)
source = "{% assign nums = (x..y) %}{% for num in nums %}{{ num }}{% endfor %}" t = Template.parse("{% assign nums = (x..y) %}{% for num in nums %}{{ num }}{% endfor %}")
assert_template_result("12345", source, { "x" => 1, "y" => 5 }) result = t.render('x' => 1, 'y' => 5)
end assert_equal('12345', result)
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
end end
+8 -3
View File
@@ -530,9 +530,14 @@ class TrimModeTest < Minitest::Test
end end
def test_pre_trim_blank_preceding_text def test_pre_trim_blank_preceding_text
assert_template_result("", "\n{%- raw %}{% endraw %}") template = Liquid::Template.parse("\n{%- raw %}{% endraw %}")
assert_template_result("", "\n{%- if true %}{% endif %}") assert_equal("", template.render)
assert_template_result("BC", "{{ 'B' }} \n{%- if true %}C{% endif %}")
template = Liquid::Template.parse("\n{%- if true %}{% endif %}")
assert_equal("", template.render)
template = Liquid::Template.parse("{{ 'B' }} \n{%- if true %}C{% endif %}")
assert_equal("BC", template.render)
end end
def test_bug_compatible_pre_trim def test_bug_compatible_pre_trim
+24 -124
View File
@@ -1,87 +1,59 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'test_helper' require 'test_helper'
require 'timeout'
class VariableTest < Minitest::Test class VariableTest < Minitest::Test
include Liquid include Liquid
def test_simple_variable def test_simple_variable
assert_template_result('worked', "{{test}}", { 'test' => 'worked' }) template = Template.parse(%({{test}}))
assert_template_result('worked wonderfully', "{{test}}", { 'test' => 'worked wonderfully' }) assert_equal('worked', template.render!('test' => 'worked'))
assert_equal('worked wonderfully', template.render!('test' => 'worked wonderfully'))
end end
def test_variable_render_calls_to_liquid def test_variable_render_calls_to_liquid
assert_template_result('foobar', '{{ foo }}', { 'foo' => ThingWithToLiquid.new }) 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 foo == eqv %}one{% endif %}', { 'foo' => IntegerDrop.new(1), 'eqv' => 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('one', '{% if b > a %}one{% endif %}', { 'b' => IntegerDrop.new(1), 'a' => IntegerDrop.new(0) })
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) })
assert_template_result('one', '{% if a contains x %}one{% endif %}', { 'a' => [1], 'x' => IntegerDrop.new(1) })
end
def test_unless_tag_calls_to_liquid_value
assert_template_result('', '{% unless foo %}true{% endunless %}', { 'foo' => BooleanDrop.new(true) })
assert_template_result('true', '{% unless foo %}true{% endunless %}', { 'foo' => BooleanDrop.new(false) })
end
def test_case_tag_calls_to_liquid_value
assert_template_result('One', '{% case foo %}{% when 1 %}One{% endcase %}', { 'foo' => IntegerDrop.new('1') })
end end
def test_simple_with_whitespaces def test_simple_with_whitespaces
assert_template_result(" worked ", " {{ test }} ", { "test" => "worked" }) template = Template.parse(%( {{ test }} ))
assert_template_result(" worked wonderfully ", " {{ test }} ", { "test" => "worked wonderfully" }) assert_equal(' worked ', template.render!('test' => 'worked'))
assert_equal(' worked wonderfully ', template.render!('test' => 'worked wonderfully'))
end end
def test_expression_with_whitespace_in_square_brackets def test_expression_with_whitespace_in_square_brackets
assert_template_result('result', "{{ a[ 'b' ] }}", { 'a' => { 'b' => 'result' } }) assert_template_result('result', "{{ a[ 'b' ] }}", 'a' => { 'b' => 'result' })
assert_template_result('result', "{{ a[ [ 'b' ] ] }}", { 'b' => 'c', 'a' => { 'c' => 'result' } }) assert_template_result('result', "{{ a[ [ 'b' ] ] }}", 'b' => 'c', 'a' => { 'c' => 'result' })
end end
def test_ignore_unknown def test_ignore_unknown
assert_template_result("", "{{ test }}") template = Template.parse(%({{ test }}))
assert_equal('', template.render!)
end end
def test_using_blank_as_variable_name def test_using_blank_as_variable_name
assert_template_result("", "{% assign foo = blank %}{{ foo }}") template = Template.parse("{% assign foo = blank %}{{ foo }}")
assert_equal('', template.render!)
end end
def test_using_empty_as_variable_name def test_using_empty_as_variable_name
assert_template_result("", "{% assign foo = empty %}{{ foo }}") template = Template.parse("{% assign foo = empty %}{{ foo }}")
assert_equal('', template.render!)
end end
def test_hash_scoping def test_hash_scoping
assert_template_result('worked', "{{ test.test }}", { 'test' => { 'test' => 'worked' } }) assert_template_result('worked', "{{ test.test }}", 'test' => { 'test' => 'worked' })
assert_template_result('worked', "{{ test . test }}", { 'test' => { 'test' => 'worked' } }) assert_template_result('worked', "{{ test . test }}", 'test' => { 'test' => 'worked' })
end end
def test_false_renders_as_false def test_false_renders_as_false
assert_template_result("false", "{{ foo }}", { 'foo' => false }) assert_equal('false', Template.parse("{{ foo }}").render!('foo' => false))
assert_template_result("false", "{{ false }}") assert_equal('false', Template.parse("{{ false }}").render!)
end end
def test_nil_renders_as_empty_string def test_nil_renders_as_empty_string
assert_template_result("", "{{ nil }}") assert_equal('', Template.parse("{{ nil }}").render!)
assert_template_result("cat", "{{ nil | append: 'cat' }}") assert_equal('cat', Template.parse("{{ nil | append: 'cat' }}").render!)
end end
def test_preset_assigns def test_preset_assigns
@@ -122,86 +94,14 @@ class VariableTest < Minitest::Test
end end
def test_multiline_variable def test_multiline_variable
assert_template_result("worked", "{{\ntest\n}}", { "test" => "worked" }) assert_equal('worked', Template.parse("{{\ntest\n}}").render!('test' => 'worked'))
end end
def test_render_symbol def test_render_symbol
assert_template_result('bar', '{{ foo }}', { 'foo' => :bar }) assert_template_result('bar', '{{ foo }}', 'foo' => :bar)
end end
def test_dynamic_find_var def test_dynamic_find_var
assert_template_result('bar', '{{ [key] }}', { 'key' => 'foo', 'foo' => 'bar' }) 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
def test_dynamic_find_var_with_drop
assert_template_result(
'bar',
'{{ [list[settings.zero]] }}',
{
'list' => ['foo'],
'settings' => SettingsDrop.new("zero" => 0),
'foo' => 'bar',
},
)
assert_template_result(
'foo',
'{{ [list[settings.zero]["foo"]] }}',
{
'list' => [{ 'foo' => 'bar' }],
'settings' => SettingsDrop.new("zero" => 0),
'bar' => 'foo',
},
)
end
def test_double_nested_variable_lookup
assert_template_result(
'bar',
'{{ list[list[settings.zero]]["foo"] }}',
{
'list' => [1, { 'foo' => 'bar' }],
'settings' => SettingsDrop.new("zero" => 0),
'bar' => 'foo',
},
)
end
def test_variable_lookup_should_not_hang_with_invalid_syntax
Timeout.timeout(1) do
assert_template_result(
'bar',
"{{['foo'}}",
{
'foo' => 'bar',
},
error_mode: :lax,
)
end
very_long_key = "1234567890" * 100
template_list = [
"{{['#{very_long_key}']}}", # valid
"{{['#{very_long_key}'}}", # missing closing bracket
"{{[['#{very_long_key}']}}", # extra open bracket
]
template_list.each do |template|
Timeout.timeout(1) do
assert_template_result(
'bar',
template,
{
very_long_key => 'bar',
},
error_mode: :lax,
)
end
end
end end
end end
+22 -72
View File
@@ -37,30 +37,23 @@ module Minitest
module Assertions module Assertions
include Liquid include Liquid
def assert_template_result( def assert_template_result(expected, template, assigns = {}, message = nil)
expected, template, assigns = {}, assert_equal(expected, Template.parse(template, line_numbers: true).render!(assigns), message)
message: nil, partials: nil, error_mode: nil, render_errors: false,
template_factory: nil
)
template = Liquid::Template.parse(template, line_numbers: true, error_mode: error_mode&.to_sym)
file_system = StubFileSystem.new(partials || {})
registers = Liquid::Registers.new(file_system: file_system, template_factory: template_factory)
context = Liquid::Context.build(static_environments: assigns, rethrow_errors: !render_errors, registers: registers)
output = template.render(context)
assert_equal(expected, output, message)
end end
def assert_match_syntax_error(match, template, error_mode: nil) def assert_template_result_matches(expected, template, assigns = {}, message = nil)
return assert_template_result(expected, template, assigns, message) unless expected.is_a?(Regexp)
assert_match(expected, Template.parse(template, line_numbers: true).render!(assigns), message)
end
def assert_match_syntax_error(match, template, assigns = {})
exception = assert_raises(Liquid::SyntaxError) do exception = assert_raises(Liquid::SyntaxError) do
Template.parse(template, line_numbers: true, error_mode: error_mode&.to_sym).render Template.parse(template, line_numbers: true).render(assigns)
end end
assert_match(match, exception.message) assert_match(match, exception.message)
end end
def assert_syntax_error(template, error_mode: nil)
assert_match_syntax_error("", template, error_mode: error_mode)
end
def assert_usage_increment(name, times: 1) def assert_usage_increment(name, times: 1)
old_method = Liquid::Usage.method(:increment) old_method = Liquid::Usage.method(:increment)
calls = 0 calls = 0
@@ -79,21 +72,21 @@ module Minitest
end end
def with_global_filter(*globals) def with_global_filter(*globals)
original_global_cache = Liquid::StrainerFactory::GlobalCache original_global_filters = Liquid::StrainerFactory.instance_variable_get(:@global_filters)
Liquid::StrainerFactory.send(:remove_const, :GlobalCache) Liquid::StrainerFactory.instance_variable_set(:@global_filters, [])
Liquid::StrainerFactory.const_set(:GlobalCache, Class.new(Liquid::StrainerTemplate)) globals.each do |global|
Liquid::StrainerFactory.add_global_filter(global)
end
Liquid::StrainerFactory.send(:strainer_class_cache).clear
globals.each do |global| globals.each do |global|
Liquid::Template.register_filter(global) Liquid::Template.register_filter(global)
end end
yield
ensure
Liquid::StrainerFactory.send(:strainer_class_cache).clear Liquid::StrainerFactory.send(:strainer_class_cache).clear
begin Liquid::StrainerFactory.instance_variable_set(:@global_filters, original_global_filters)
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 end
def with_error_mode(mode) def with_error_mode(mode)
@@ -126,47 +119,6 @@ class ThingWithToLiquid
end end
end end
class SettingsDrop < Liquid::Drop
def initialize(settings)
super()
@settings = settings
end
def liquid_method_missing(key)
@settings[key]
end
end
class IntegerDrop < Liquid::Drop
def initialize(value)
super()
@value = value.to_i
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 to_liquid_value
@value
end
def to_s
@value ? "Yay" : "Nay"
end
end
class ErrorDrop < Liquid::Drop class ErrorDrop < Liquid::Drop
def standard_error def standard_error
raise Liquid::StandardError, 'standard error' raise Liquid::StandardError, 'standard error'
@@ -210,10 +162,8 @@ class StubTemplateFactory
@count = 0 @count = 0
end end
def for(template_name) def for(_template_name)
@count += 1 @count += 1
template = Liquid::Template.new Liquid::Template.new
template.name = "some/path/" + template_name
template
end end
end end
+2 -4
View File
@@ -35,10 +35,8 @@ class BlockUnitTest < Minitest::Test
def test_variable_many_embedded_fragments def test_variable_many_embedded_fragments
template = Liquid::Template.parse(" {{funk}} {{so}} {{brother}} ") template = Liquid::Template.parse(" {{funk}} {{so}} {{brother}} ")
assert_equal(7, template.root.nodelist.size) assert_equal(7, template.root.nodelist.size)
assert_equal( assert_equal([String, Variable, String, Variable, String, Variable, String],
[String, Variable, String, Variable, String, Variable, String], block_types(template.root.nodelist))
block_types(template.root.nodelist),
)
end end
def test_with_block def test_with_block
+18 -35
View File
@@ -10,8 +10,8 @@ class ConditionUnitTest < Minitest::Test
end end
def test_basic_condition def test_basic_condition
assert_equal(false, Condition.new(1, '==', 2).evaluate(Context.new)) assert_equal(false, Condition.new(1, '==', 2).evaluate)
assert_equal(true, Condition.new(1, '==', 1).evaluate(Context.new)) assert_equal(true, Condition.new(1, '==', 1).evaluate)
end end
def test_default_operators_evalute_true def test_default_operators_evalute_true
@@ -67,11 +67,11 @@ class ConditionUnitTest < Minitest::Test
end end
def test_hash_compare_backwards_compatibility def test_hash_compare_backwards_compatibility
assert_nil(Condition.new({}, '>', 2).evaluate(Context.new)) assert_nil(Condition.new({}, '>', 2).evaluate)
assert_nil(Condition.new(2, '>', {}).evaluate(Context.new)) assert_nil(Condition.new(2, '>', {}).evaluate)
assert_equal(false, Condition.new({}, '==', 2).evaluate(Context.new)) assert_equal(false, Condition.new({}, '==', 2).evaluate)
assert_equal(true, Condition.new({ 'a' => 1 }, '==', 'a' => 1).evaluate(Context.new)) assert_equal(true, Condition.new({ 'a' => 1 }, '==', 'a' => 1).evaluate)
assert_equal(true, Condition.new({ 'a' => 2 }, 'contains', 'a').evaluate(Context.new)) assert_equal(true, Condition.new({ 'a' => 2 }, 'contains', 'a').evaluate)
end end
def test_contains_works_on_arrays def test_contains_works_on_arrays
@@ -106,29 +106,30 @@ class ConditionUnitTest < Minitest::Test
def test_or_condition def test_or_condition
condition = Condition.new(1, '==', 2) condition = Condition.new(1, '==', 2)
assert_equal(false, condition.evaluate(Context.new))
assert_equal(false, condition.evaluate)
condition.or(Condition.new(2, '==', 1)) condition.or(Condition.new(2, '==', 1))
assert_equal(false, condition.evaluate(Context.new)) assert_equal(false, condition.evaluate)
condition.or(Condition.new(1, '==', 1)) condition.or(Condition.new(1, '==', 1))
assert_equal(true, condition.evaluate(Context.new)) assert_equal(true, condition.evaluate)
end end
def test_and_condition def test_and_condition
condition = Condition.new(1, '==', 1) condition = Condition.new(1, '==', 1)
assert_equal(true, condition.evaluate(Context.new)) assert_equal(true, condition.evaluate)
condition.and(Condition.new(2, '==', 2)) condition.and(Condition.new(2, '==', 2))
assert_equal(true, condition.evaluate(Context.new)) assert_equal(true, condition.evaluate)
condition.and(Condition.new(2, '==', 1)) condition.and(Condition.new(2, '==', 1))
assert_equal(false, condition.evaluate(Context.new)) assert_equal(false, condition.evaluate)
end end
def test_should_allow_custom_proc_operator def test_should_allow_custom_proc_operator
@@ -147,34 +148,16 @@ class ConditionUnitTest < Minitest::Test
assert_evaluates_true(VariableLookup.new("one"), '==', VariableLookup.new("another")) assert_evaluates_true(VariableLookup.new("one"), '==', VariableLookup.new("another"))
end 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 private
def assert_evaluates_true(left, op, right) def assert_evaluates_true(left, op, right)
assert( assert(Condition.new(left, op, right).evaluate(@context),
Condition.new(left, op, right).evaluate(@context), "Evaluated false: #{left} #{op} #{right}")
"Evaluated false: #{left} #{op} #{right}",
)
end end
def assert_evaluates_false(left, op, right) def assert_evaluates_false(left, op, right)
assert( assert(!Condition.new(left, op, right).evaluate(@context),
!Condition.new(left, op, right).evaluate(@context), "Evaluated true: #{left} #{op} #{right}")
"Evaluated true: #{left} #{op} #{right}",
)
end end
def assert_evaluates_argument_error(left, op, right) def assert_evaluates_argument_error(left, op, right)
+32 -60
View File
@@ -8,245 +8,217 @@ class ParseTreeVisitorTest < Minitest::Test
def test_variable def test_variable
assert_equal( assert_equal(
["test"], ["test"],
visit(%({{ test }})), visit(%({{ test }}))
) )
end end
def test_varible_with_filter def test_varible_with_filter
assert_equal( assert_equal(
["test", "infilter"], ["test", "infilter"],
visit(%({{ test | split: infilter }})), visit(%({{ test | split: infilter }}))
) )
end end
def test_dynamic_variable def test_dynamic_variable
assert_equal( assert_equal(
["test", "inlookup"], ["test", "inlookup"],
visit(%({{ test[inlookup] }})), visit(%({{ test[inlookup] }}))
)
end
def test_echo
assert_equal(
["test"],
visit(%({% echo test %})),
) )
end end
def test_if_condition def test_if_condition
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% if test %}{% endif %})), visit(%({% if test %}{% endif %}))
) )
end end
def test_complex_if_condition def test_complex_if_condition
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% if 1 == 1 and 2 == test %}{% endif %})), visit(%({% if 1 == 1 and 2 == test %}{% endif %}))
) )
end end
def test_if_body def test_if_body
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% if 1 == 1 %}{{ test }}{% endif %})), visit(%({% if 1 == 1 %}{{ test }}{% endif %}))
) )
end end
def test_unless_condition def test_unless_condition
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% unless test %}{% endunless %})), visit(%({% unless test %}{% endunless %}))
) )
end end
def test_complex_unless_condition def test_complex_unless_condition
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% unless 1 == 1 and 2 == test %}{% endunless %})), visit(%({% unless 1 == 1 and 2 == test %}{% endunless %}))
) )
end end
def test_unless_body def test_unless_body
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% unless 1 == 1 %}{{ test }}{% endunless %})), visit(%({% unless 1 == 1 %}{{ test }}{% endunless %}))
) )
end end
def test_elsif_condition def test_elsif_condition
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% if 1 == 1 %}{% elsif test %}{% endif %})), visit(%({% if 1 == 1 %}{% elsif test %}{% endif %}))
) )
end end
def test_complex_elsif_condition def test_complex_elsif_condition
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% if 1 == 1 %}{% elsif 1 == 1 and 2 == test %}{% endif %})), visit(%({% if 1 == 1 %}{% elsif 1 == 1 and 2 == test %}{% endif %}))
) )
end end
def test_elsif_body def test_elsif_body
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% if 1 == 1 %}{% elsif 2 == 2 %}{{ test }}{% endif %})), visit(%({% if 1 == 1 %}{% elsif 2 == 2 %}{{ test }}{% endif %}))
) )
end end
def test_else_body def test_else_body
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% if 1 == 1 %}{% else %}{{ test }}{% endif %})), visit(%({% if 1 == 1 %}{% else %}{{ test }}{% endif %}))
) )
end end
def test_case_left def test_case_left
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% case test %}{% endcase %})), visit(%({% case test %}{% endcase %}))
) )
end end
def test_case_condition def test_case_condition
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% case 1 %}{% when test %}{% endcase %})), visit(%({% case 1 %}{% when test %}{% endcase %}))
) )
end end
def test_case_when_body def test_case_when_body
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% case 1 %}{% when 2 %}{{ test }}{% endcase %})), visit(%({% case 1 %}{% when 2 %}{{ test }}{% endcase %}))
) )
end end
def test_case_else_body def test_case_else_body
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% case 1 %}{% else %}{{ test }}{% endcase %})), visit(%({% case 1 %}{% else %}{{ test }}{% endcase %}))
) )
end end
def test_for_in def test_for_in
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% for x in test %}{% endfor %})), visit(%({% for x in test %}{% endfor %}))
) )
end end
def test_for_limit def test_for_limit
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% for x in (1..5) limit: test %}{% endfor %})), visit(%({% for x in (1..5) limit: test %}{% endfor %}))
) )
end end
def test_for_offset def test_for_offset
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% for x in (1..5) offset: test %}{% endfor %})), visit(%({% for x in (1..5) offset: test %}{% endfor %}))
) )
end end
def test_for_body def test_for_body
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% for x in (1..5) %}{{ test }}{% endfor %})), visit(%({% for x in (1..5) %}{{ test }}{% endfor %}))
)
end
def test_for_range
assert_equal(
["test"],
visit(%({% for x in (1..test) %}{% endfor %})),
) )
end end
def test_tablerow_in def test_tablerow_in
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% tablerow x in test %}{% endtablerow %})), visit(%({% tablerow x in test %}{% endtablerow %}))
) )
end end
def test_tablerow_limit def test_tablerow_limit
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% tablerow x in (1..5) limit: test %}{% endtablerow %})), visit(%({% tablerow x in (1..5) limit: test %}{% endtablerow %}))
) )
end end
def test_tablerow_offset def test_tablerow_offset
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% tablerow x in (1..5) offset: test %}{% endtablerow %})), visit(%({% tablerow x in (1..5) offset: test %}{% endtablerow %}))
) )
end end
def test_tablerow_body def test_tablerow_body
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% tablerow x in (1..5) %}{{ test }}{% endtablerow %})), visit(%({% tablerow x in (1..5) %}{{ test }}{% endtablerow %}))
) )
end end
def test_cycle def test_cycle
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% cycle test %})), visit(%({% cycle test %}))
) )
end end
def test_assign def test_assign
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% assign x = test %})), visit(%({% assign x = test %}))
) )
end end
def test_capture def test_capture
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% capture x %}{{ test }}{% endcapture %})), visit(%({% capture x %}{{ test }}{% endcapture %}))
) )
end end
def test_include def test_include
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% include test %})), visit(%({% include test %}))
) )
end end
def test_include_with def test_include_with
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% include "hai" with test %})), visit(%({% include "hai" with test %}))
) )
end end
def test_include_for def test_include_for
assert_equal( assert_equal(
["test"], ["test"],
visit(%({% include "hai" for test %})), visit(%({% include "hai" for test %}))
)
end
def test_render_with
assert_equal(
["test"],
visit(%({% render "hai" with test %})),
)
end
def test_render_for
assert_equal(
["test"],
visit(%({% render "hai" for test %})),
) )
end end
@@ -257,7 +229,7 @@ class ParseTreeVisitorTest < Minitest::Test
["test", []], ["test", []],
["xs", []], ["xs", []],
]]], ]]],
traversal(%({% for x in xs offset: test %}{{ other }}{% endfor %})).visit, traversal(%({% for x in xs offset: test %}{{ other }}{% endfor %})).visit
) )
end end
+16 -65
View File
@@ -7,13 +7,13 @@ class PartialCacheUnitTest < Minitest::Test
context = Liquid::Context.build( context = Liquid::Context.build(
registers: { registers: {
file_system: StubFileSystem.new('my_partial' => 'my partial body'), file_system: StubFileSystem.new('my_partial' => 'my partial body'),
}, }
) )
partial = Liquid::PartialCache.load( partial = Liquid::PartialCache.load(
'my_partial', 'my_partial',
context: context, context: context,
parse_context: Liquid::ParseContext.new, parse_context: Liquid::ParseContext.new
) )
assert_equal('my partial body', partial.render) assert_equal('my partial body', partial.render)
@@ -22,14 +22,14 @@ class PartialCacheUnitTest < Minitest::Test
def test_reads_from_the_file_system_only_once_per_file def test_reads_from_the_file_system_only_once_per_file
file_system = StubFileSystem.new('my_partial' => 'some partial body') file_system = StubFileSystem.new('my_partial' => 'some partial body')
context = Liquid::Context.build( context = Liquid::Context.build(
registers: { file_system: file_system }, registers: { file_system: file_system }
) )
2.times do 2.times do
Liquid::PartialCache.load( Liquid::PartialCache.load(
'my_partial', 'my_partial',
context: context, context: context,
parse_context: Liquid::ParseContext.new, parse_context: Liquid::ParseContext.new
) )
end end
@@ -39,31 +39,31 @@ class PartialCacheUnitTest < Minitest::Test
def test_cache_state_is_stored_per_context def test_cache_state_is_stored_per_context
parse_context = Liquid::ParseContext.new parse_context = Liquid::ParseContext.new
shared_file_system = StubFileSystem.new( shared_file_system = StubFileSystem.new(
'my_partial' => 'my shared value', 'my_partial' => 'my shared value'
) )
context_one = Liquid::Context.build( context_one = Liquid::Context.build(
registers: { registers: {
file_system: shared_file_system, file_system: shared_file_system,
}, }
) )
context_two = Liquid::Context.build( context_two = Liquid::Context.build(
registers: { registers: {
file_system: shared_file_system, file_system: shared_file_system,
}, }
) )
2.times do 2.times do
Liquid::PartialCache.load( Liquid::PartialCache.load(
'my_partial', 'my_partial',
context: context_one, context: context_one,
parse_context: parse_context, parse_context: parse_context
) )
end end
Liquid::PartialCache.load( Liquid::PartialCache.load(
'my_partial', 'my_partial',
context: context_two, context: context_two,
parse_context: parse_context, parse_context: parse_context
) )
assert_equal(2, shared_file_system.file_read_count) assert_equal(2, shared_file_system.file_read_count)
@@ -72,18 +72,18 @@ class PartialCacheUnitTest < Minitest::Test
def test_cache_is_not_broken_when_a_different_parse_context_is_used def test_cache_is_not_broken_when_a_different_parse_context_is_used
file_system = StubFileSystem.new('my_partial' => 'some partial body') file_system = StubFileSystem.new('my_partial' => 'some partial body')
context = Liquid::Context.build( context = Liquid::Context.build(
registers: { file_system: file_system }, registers: { file_system: file_system }
) )
Liquid::PartialCache.load( Liquid::PartialCache.load(
'my_partial', 'my_partial',
context: context, context: context,
parse_context: Liquid::ParseContext.new(my_key: 'value one'), parse_context: Liquid::ParseContext.new(my_key: 'value one')
) )
Liquid::PartialCache.load( Liquid::PartialCache.load(
'my_partial', 'my_partial',
context: context, context: context,
parse_context: Liquid::ParseContext.new(my_key: 'value two'), parse_context: Liquid::ParseContext.new(my_key: 'value two')
) )
# Technically what we care about is that the file was parsed twice, # Technically what we care about is that the file was parsed twice,
@@ -95,13 +95,13 @@ class PartialCacheUnitTest < Minitest::Test
context = Liquid::Context.build( context = Liquid::Context.build(
registers: { registers: {
file_system: StubFileSystem.new('my_partial' => 'my partial body'), file_system: StubFileSystem.new('my_partial' => 'my partial body'),
}, }
) )
partial = Liquid::PartialCache.load( partial = Liquid::PartialCache.load(
'my_partial', 'my_partial',
context: context, context: context,
parse_context: Liquid::ParseContext.new, parse_context: Liquid::ParseContext.new
) )
assert_equal('my partial body', partial.render) assert_equal('my partial body', partial.render)
@@ -113,65 +113,16 @@ class PartialCacheUnitTest < Minitest::Test
registers: { registers: {
file_system: StubFileSystem.new('my_partial' => 'my partial body'), file_system: StubFileSystem.new('my_partial' => 'my partial body'),
template_factory: template_factory, template_factory: template_factory,
}, }
) )
partial = Liquid::PartialCache.load( partial = Liquid::PartialCache.load(
'my_partial', 'my_partial',
context: context, context: context,
parse_context: Liquid::ParseContext.new, parse_context: Liquid::ParseContext.new
) )
assert_equal('my partial body', partial.render) assert_equal('my partial body', partial.render)
assert_equal(1, template_factory.count) assert_equal(1, template_factory.count)
end end
def test_cache_state_is_shared_for_subcontexts
parse_context = Liquid::ParseContext.new
shared_file_system = StubFileSystem.new(
'my_partial' => 'my shared value',
)
context = Liquid::Context.build(
registers: Liquid::Registers.new(
file_system: shared_file_system,
),
)
subcontext = context.new_isolated_subcontext
assert_equal(subcontext.registers[:cached_partials].object_id, context.registers[:cached_partials].object_id)
2.times do
Liquid::PartialCache.load(
'my_partial',
context: context,
parse_context: parse_context,
)
Liquid::PartialCache.load(
'my_partial',
context: subcontext,
parse_context: parse_context,
)
end
assert_equal(1, shared_file_system.file_read_count)
end
def test_uses_template_name_from_template_factory
template_factory = StubTemplateFactory.new
context = Liquid::Context.build(
registers: {
file_system: StubFileSystem.new('my_partial' => 'my partial body'),
template_factory: template_factory,
},
)
partial = Liquid::PartialCache.load(
'my_partial',
context: context,
parse_context: Liquid::ParseContext.new,
)
assert_equal('some/path/my_partial', partial.name)
end
end end
-13
View File
@@ -1,7 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'test_helper' require 'test_helper'
require 'timeout'
class RegexpUnitTest < Minitest::Test class RegexpUnitTest < Minitest::Test
include Liquid include Liquid
@@ -38,22 +37,10 @@ class RegexpUnitTest < Minitest::Test
def test_variable_parser def test_variable_parser
assert_equal(['var'], 'var'.scan(VariableParser)) assert_equal(['var'], 'var'.scan(VariableParser))
assert_equal(['[var]'], '[var]'.scan(VariableParser))
assert_equal(['var', 'method'], 'var.method'.scan(VariableParser)) assert_equal(['var', 'method'], 'var.method'.scan(VariableParser))
assert_equal(['var', '[method]'], 'var[method]'.scan(VariableParser)) assert_equal(['var', '[method]'], 'var[method]'.scan(VariableParser))
assert_equal(['var', '[method]', '[0]'], 'var[method][0]'.scan(VariableParser)) assert_equal(['var', '[method]', '[0]'], 'var[method][0]'.scan(VariableParser))
assert_equal(['var', '["method"]', '[0]'], 'var["method"][0]'.scan(VariableParser)) assert_equal(['var', '["method"]', '[0]'], 'var["method"][0]'.scan(VariableParser))
assert_equal(['var', '[method]', '[0]', 'method'], 'var[method][0].method'.scan(VariableParser)) assert_equal(['var', '[method]', '[0]', 'method'], 'var[method][0].method'.scan(VariableParser))
end end
def test_variable_parser_with_large_input
Timeout.timeout(1) { assert_equal(['[var]'], '[var]'.scan(VariableParser)) }
very_long_string = "foo" * 1000
# valid dynamic lookup
Timeout.timeout(1) { assert_equal(["[#{very_long_string}]"], "[#{very_long_string}]".scan(VariableParser)) }
# invalid dynamic lookup with missing closing bracket
Timeout.timeout(1) { assert_equal([very_long_string], "[#{very_long_string}".scan(VariableParser)) }
end
end # RegexpTest end # RegexpTest
@@ -2,11 +2,11 @@
require 'test_helper' require 'test_helper'
class RegistersUnitTest < Minitest::Test class StaticRegistersUnitTest < Minitest::Test
include Liquid include Liquid
def test_set def test_set
static_register = Registers.new(a: 1, b: 2) static_register = StaticRegisters.new(a: 1, b: 2)
static_register[:b] = 22 static_register[:b] = 22
static_register[:c] = 33 static_register[:c] = 33
@@ -16,13 +16,13 @@ class RegistersUnitTest < Minitest::Test
end end
def test_get_missing_key def test_get_missing_key
static_register = Registers.new static_register = StaticRegisters.new
assert_nil(static_register[:missing]) assert_nil(static_register[:missing])
end end
def test_delete def test_delete
static_register = Registers.new(a: 1, b: 2) static_register = StaticRegisters.new(a: 1, b: 2)
static_register[:b] = 22 static_register[:b] = 22
static_register[:c] = 33 static_register[:c] = 33
@@ -37,7 +37,7 @@ class RegistersUnitTest < Minitest::Test
end end
def test_fetch def test_fetch
static_register = Registers.new(a: 1, b: 2) static_register = StaticRegisters.new(a: 1, b: 2)
static_register[:b] = 22 static_register[:b] = 22
static_register[:c] = 33 static_register[:c] = 33
@@ -61,7 +61,7 @@ class RegistersUnitTest < Minitest::Test
end end
def test_key def test_key
static_register = Registers.new(a: 1, b: 2) static_register = StaticRegisters.new(a: 1, b: 2)
static_register[:b] = 22 static_register[:b] = 22
static_register[:c] = 33 static_register[:c] = 33
@@ -72,7 +72,7 @@ class RegistersUnitTest < Minitest::Test
end end
def test_static_register_can_be_frozen def test_static_register_can_be_frozen
static_register = Registers.new(a: 1) static_register = StaticRegisters.new(a: 1)
static_register.static.freeze static_register.static.freeze
@@ -94,14 +94,14 @@ class RegistersUnitTest < Minitest::Test
end end
def test_new_static_retains_static def test_new_static_retains_static
static_register = Registers.new(a: 1, b: 2) static_register = StaticRegisters.new(a: 1, b: 2)
static_register[:b] = 22 static_register[:b] = 22
static_register[:c] = 33 static_register[:c] = 33
new_static_register = Registers.new(static_register) new_static_register = StaticRegisters.new(static_register)
new_static_register[:b] = 222 new_static_register[:b] = 222
newest_static_register = Registers.new(new_static_register) newest_static_register = StaticRegisters.new(new_static_register)
newest_static_register[:c] = 333 newest_static_register[:c] = 333
assert_equal(1, static_register[:a]) assert_equal(1, static_register[:a])
@@ -118,11 +118,11 @@ class RegistersUnitTest < Minitest::Test
end end
def test_multiple_instances_are_unique def test_multiple_instances_are_unique
static_register_1 = Registers.new(a: 1, b: 2) static_register_1 = StaticRegisters.new(a: 1, b: 2)
static_register_1[:b] = 22 static_register_1[:b] = 22
static_register_1[:c] = 33 static_register_1[:c] = 33
static_register_2 = Registers.new(a: 10, b: 20) static_register_2 = StaticRegisters.new(a: 10, b: 20)
static_register_2[:b] = 220 static_register_2[:b] = 220
static_register_2[:c] = 330 static_register_2[:c] = 330
@@ -138,11 +138,11 @@ class RegistersUnitTest < Minitest::Test
end end
def test_initialization_reused_static_same_memory_object def test_initialization_reused_static_same_memory_object
static_register_1 = Registers.new(a: 1, b: 2) static_register_1 = StaticRegisters.new(a: 1, b: 2)
static_register_1[:b] = 22 static_register_1[:b] = 22
static_register_1[:c] = 33 static_register_1[:c] = 33
static_register_2 = Registers.new(static_register_1) static_register_2 = StaticRegisters.new(static_register_1)
assert_equal(1, static_register_2[:a]) assert_equal(1, static_register_2[:a])
assert_equal(2, static_register_2[:b]) assert_equal(2, static_register_2[:b])
+2 -3
View File
@@ -50,10 +50,9 @@ class StrainerFactoryUnitTest < Minitest::Test
assert_match( assert_match(
/\ALiquid error: wrong number of arguments \((1 for 0|given 1, expected 0)\)\z/, /\ALiquid error: wrong number of arguments \((1 for 0|given 1, expected 0)\)\z/,
exception.message, exception.message
) )
source = AccessScopeFilters.instance_method(:public_filter).source_location assert_equal(exception.backtrace[0].split(':')[0], __FILE__)
assert_equal(source.map(&:to_s), exception.backtrace[0].split(':')[0..1])
end end
def test_strainer_only_invokes_public_filter_methods def test_strainer_only_invokes_public_filter_methods
+1 -1
View File
@@ -57,8 +57,8 @@ class StrainerTemplateUnitTest < Minitest::Test
end end
def test_add_filter_does_not_raise_when_module_overrides_previously_registered_method def test_add_filter_does_not_raise_when_module_overrides_previously_registered_method
strainer = Context.new.strainer
with_global_filter do with_global_filter do
strainer = Context.new.strainer
strainer.class.add_filter(PublicMethodOverrideFilter) strainer.class.add_filter(PublicMethodOverrideFilter)
assert(strainer.class.send(:filter_methods).include?('public_filter')) assert(strainer.class.send(:filter_methods).include?('public_filter'))
end end