mirror of
https://github.com/Shopify/liquid.git
synced 2026-09-13 07:50:43 -07:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf5a42245f |
@@ -0,0 +1,2 @@
|
||||
enabled:
|
||||
- cla
|
||||
@@ -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 }}
|
||||
@@ -1,35 +0,0 @@
|
||||
name: Liquid
|
||||
on: [push, pull_request]
|
||||
|
||||
env:
|
||||
BUNDLE_JOBS: 4
|
||||
BUNDLE_RETRY: 3
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
entry:
|
||||
- { ruby: 2.7, allowed-failure: false } # minimum supported
|
||||
- { ruby: 3.2, allowed-failure: false } # latest
|
||||
- { ruby: ruby-head, allowed-failure: true }
|
||||
name: Test Ruby ${{ matrix.entry.ruby }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: ${{ matrix.entry.ruby }}
|
||||
bundler-cache: true
|
||||
- run: bundle exec rake
|
||||
continue-on-error: ${{ matrix.entry.allowed-failure }}
|
||||
|
||||
memory_profile:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 2.7
|
||||
bundler-cache: true
|
||||
- run: bundle exec rake memory_profile:run
|
||||
File diff suppressed because it is too large
Load Diff
+1
-14
@@ -1,7 +1,5 @@
|
||||
inherit_gem:
|
||||
rubocop-shopify: rubocop.yml
|
||||
|
||||
inherit_from:
|
||||
- 'https://shopify.github.io/ruby-style-guide/rubocop.yml'
|
||||
- .rubocop_todo.yml
|
||||
|
||||
require: rubocop-performance
|
||||
@@ -10,20 +8,9 @@ Performance:
|
||||
Enabled: true
|
||||
|
||||
AllCops:
|
||||
TargetRubyVersion: 2.7
|
||||
NewCops: disable
|
||||
SuggestExtensions: false
|
||||
Exclude:
|
||||
- 'vendor/bundle/**/*'
|
||||
|
||||
Naming/MethodName:
|
||||
Exclude:
|
||||
- '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'
|
||||
|
||||
+43
-129
@@ -1,84 +1,56 @@
|
||||
# This configuration was generated by
|
||||
# `rubocop --auto-gen-config`
|
||||
# on 2022-05-18 19:25:47 UTC using RuboCop version 1.29.1.
|
||||
# on 2019-09-11 06:34:25 +1000 using RuboCop version 0.74.0.
|
||||
# The point is for the user to remove these configuration records
|
||||
# one by one as the offenses are removed from the code base.
|
||||
# Note that changes in the inspected code, or installation of new
|
||||
# versions of RuboCop, may require this file to be generated again.
|
||||
|
||||
# Offense count: 1
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
# Configuration parameters: TreatCommentsAsGroupSeparators, ConsiderPunctuation, Include.
|
||||
# Include: **/*.gemspec
|
||||
Gemspec/OrderedDependencies:
|
||||
# Offense count: 2
|
||||
Lint/AmbiguousOperator:
|
||||
Exclude:
|
||||
- 'liquid.gemspec'
|
||||
- 'test/unit/condition_unit_test.rb'
|
||||
|
||||
# Offense count: 6
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
Layout/ClosingHeredocIndentation:
|
||||
# Offense count: 21
|
||||
# Configuration parameters: AllowSafeAssignment.
|
||||
Lint/AssignmentInCondition:
|
||||
Exclude:
|
||||
- 'test/integration/tags/for_tag_test.rb'
|
||||
|
||||
# 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/tags/raw.rb'
|
||||
- 'lib/liquid/variable.rb'
|
||||
- 'lib/liquid/variable_lookup.rb'
|
||||
- 'performance/shopify/money_filter.rb'
|
||||
- 'performance/shopify/paginate.rb'
|
||||
- 'performance/profile.rb'
|
||||
- 'test/test_helper.rb'
|
||||
- 'test/unit/tokenizer_unit_test.rb'
|
||||
|
||||
# Offense count: 8
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
# Configuration parameters: AllowAliasSyntax, AllowedMethods.
|
||||
# AllowedMethods: alias_method, public, protected, private
|
||||
Layout/EmptyLinesAroundAttributeAccessor:
|
||||
# Offense count: 2
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle.
|
||||
# SupportedStyles: runtime_error, standard_error
|
||||
Lint/InheritException:
|
||||
Exclude:
|
||||
- 'lib/liquid/template.rb'
|
||||
- 'test/integration/filter_test.rb'
|
||||
- 'test/integration/tags/include_tag_test.rb'
|
||||
- 'test/unit/strainer_template_unit_test.rb'
|
||||
- 'lib/liquid/interrupts.rb'
|
||||
|
||||
# Offense count: 17
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
# Configuration parameters: EnforcedStyle, IndentationWidth.
|
||||
# SupportedStyles: aligned, indented
|
||||
Layout/LineEndStringConcatenationIndentation:
|
||||
# Offense count: 2
|
||||
Lint/UselessAssignment:
|
||||
Exclude:
|
||||
- 'test/integration/tags/for_tag_test.rb'
|
||||
- 'test/integration/tags/increment_tag_test.rb'
|
||||
- 'performance/shopify/database.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
# Configuration parameters: EnforcedStyle, IndentationWidth.
|
||||
# SupportedStyles: aligned, indented
|
||||
Layout/MultilineOperationIndentation:
|
||||
# Configuration parameters: CheckForMethodsWithNoSideEffects.
|
||||
Lint/Void:
|
||||
Exclude:
|
||||
- 'lib/liquid/expression.rb'
|
||||
- 'lib/liquid/parse_context.rb'
|
||||
|
||||
# Offense count: 9
|
||||
Lint/MissingSuper:
|
||||
Exclude:
|
||||
- 'lib/liquid/forloop_drop.rb'
|
||||
- 'lib/liquid/tablerowloop_drop.rb'
|
||||
- 'test/integration/assign_test.rb'
|
||||
- 'test/integration/context_test.rb'
|
||||
- 'test/integration/filter_test.rb'
|
||||
- 'test/integration/standard_filter_test.rb'
|
||||
- 'test/integration/tags/for_tag_test.rb'
|
||||
- 'test/integration/tags/table_row_test.rb'
|
||||
# Offense count: 98
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
|
||||
# URISchemes: http, https
|
||||
Metrics/LineLength:
|
||||
Max: 294
|
||||
|
||||
# Offense count: 44
|
||||
Naming/ConstantName:
|
||||
@@ -91,6 +63,7 @@ Naming/ConstantName:
|
||||
- 'lib/liquid/tags/cycle.rb'
|
||||
- 'lib/liquid/tags/for.rb'
|
||||
- 'lib/liquid/tags/if.rb'
|
||||
- 'lib/liquid/tags/include.rb'
|
||||
- 'lib/liquid/tags/raw.rb'
|
||||
- 'lib/liquid/tags/table_row.rb'
|
||||
- 'lib/liquid/variable.rb'
|
||||
@@ -98,83 +71,24 @@ Naming/ConstantName:
|
||||
- 'performance/shopify/paginate.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: 5
|
||||
Style/ClassVars:
|
||||
Exclude:
|
||||
- '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'
|
||||
- 'lib/liquid/strainer.rb'
|
||||
- 'lib/liquid/template.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
# Configuration parameters: EnforcedStyleForMultiline.
|
||||
# SupportedStylesForMultiline: comma, consistent_comma, no_comma
|
||||
Style/TrailingCommaInHashLiteral:
|
||||
# Configuration parameters: AllowCoercion.
|
||||
Style/DateTime:
|
||||
Exclude:
|
||||
- 'lib/liquid/expression.rb'
|
||||
- 'test/unit/context_unit_test.rb'
|
||||
|
||||
# Offense count: 19
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
# Configuration parameters: EnforcedStyle, MinSize, WordRegex.
|
||||
# SupportedStyles: percent, brackets
|
||||
Style/WordArray:
|
||||
# Offense count: 9
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: AllowAsExpressionSeparator.
|
||||
Style/Semicolon:
|
||||
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
|
||||
- 'test/integration/error_handling_test.rb'
|
||||
- 'test/integration/template_test.rb'
|
||||
- 'test/unit/context_unit_test.rb'
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
language: ruby
|
||||
cache: bundler
|
||||
|
||||
rvm:
|
||||
- 2.4
|
||||
- 2.5
|
||||
- &latest_ruby 2.6
|
||||
- 2.7
|
||||
- ruby-head
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- rvm: *latest_ruby
|
||||
script: bundle exec rake memory_profile:run
|
||||
name: Profiling Memory Usage
|
||||
allow_failures:
|
||||
- rvm: ruby-head
|
||||
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- gh-pages
|
||||
- /.*-stable/
|
||||
|
||||
notifications:
|
||||
disable: true
|
||||
+1
-4
@@ -5,7 +5,6 @@
|
||||
* Bugfixes
|
||||
* Performance improvements
|
||||
* Features that are likely to be useful to the majority of Liquid users
|
||||
* Documentation updates that are concise and likely to be useful to the majority of Liquid users
|
||||
|
||||
## Things we won't merge
|
||||
|
||||
@@ -15,14 +14,12 @@
|
||||
* Features that can easily be implemented on top of Liquid (for example as a custom filter or custom filesystem)
|
||||
* Code that does not include tests
|
||||
* Code that breaks existing tests
|
||||
* Documentation changes that are verbose, incorrect or not important to most people (we want to keep it simple and easy to understand)
|
||||
|
||||
## Workflow
|
||||
|
||||
* [Sign the CLA](https://cla.shopify.com/) if you haven't already
|
||||
* Fork the Liquid repository
|
||||
* Create a new branch in your fork
|
||||
* For updating [Liquid documentation](https://shopify.github.io/liquid/), create it from `gh-pages` branch. (You can skip tests.)
|
||||
* If it makes sense, add tests for your code and/or run a performance benchmark
|
||||
* Make sure all tests pass (`bundle exec rake`)
|
||||
* Create a pull request
|
||||
|
||||
|
||||
@@ -18,8 +18,7 @@ group :benchmark, :test do
|
||||
end
|
||||
|
||||
group :test do
|
||||
gem 'rubocop', '~> 1.44.0'
|
||||
gem 'rubocop-shopify', '~> 2.12.0', require: false
|
||||
gem 'rubocop', '~> 0.74.0', require: false
|
||||
gem 'rubocop-performance', require: false
|
||||
|
||||
platform :mri, :truffleruby do
|
||||
|
||||
-89
@@ -1,94 +1,5 @@
|
||||
# Liquid Change Log
|
||||
|
||||
## 5.4.0 2022-07-29
|
||||
|
||||
### Breaking Changes
|
||||
* Drop support for end-of-life Ruby versions (2.5 and 2.6) (#1578) [Andy Waite]
|
||||
|
||||
### Features
|
||||
* Allow `#` to be used as an inline comment tag (#1498) [CP Clermont]
|
||||
|
||||
### 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
|
||||
|
||||
### Features
|
||||
* Add new `{% render %}` tag (#1122) [Samuel Doiron]
|
||||
* Add support for `as` in `{% render %}` and `{% include %}` (#1181) [Mike Angell]
|
||||
* Add `{% liquid %}` and `{% echo %}` tags (#1086) [Justin Li]
|
||||
* Add [usage tracking](README.md#usage-tracking) [Mike Angell]
|
||||
* Add `Tag.disable_tags` for disabling tags that prepend `Tag::Disableable` at render time (#1162, #1274, #1275) [Mike Angell]
|
||||
* Support using a profiler for multiple renders (#1365, #1366) [Dylan Thacker-Smith]
|
||||
|
||||
### Fixes
|
||||
* Fix catastrophic backtracking in `RANGES_REGEX` regular expression (#1357) [Dylan Thacker-Smith]
|
||||
* Make sure the for tag's limit and offset are integers (#1094) [David Cornu]
|
||||
* Invokable methods for enumerable reject include (#1151) [Thierry Joyal]
|
||||
* Allow `default` filter to handle `false` as value (#1144) [Mike Angell]
|
||||
* Fix render length resource limit so it doesn't multiply nested output (#1285) [Dylan Thacker-Smith]
|
||||
* Fix duplication of text in raw tags (#1304) [Peter Zhu]
|
||||
* Fix strict parsing of find variable with a name expression (#1317) [Dylan Thacker-Smith]
|
||||
* Use monotonic time to measure durations in Liquid::Profiler (#1362) [Dylan Thacker-Smith]
|
||||
|
||||
### Breaking Changes
|
||||
* Require Ruby >= 2.5 (#1131, #1310) [Mike Angell, Dylan Thacker-Smith]
|
||||
* Remove support for taint checking (#1268) [Dylan Thacker-Smith]
|
||||
* Split Strainer class into StrainerFactory and StrainerTemplate (#1208) [Thierry Joyal]
|
||||
* Remove handling of a nil context in the Strainer class (#1218) [Thierry Joyal]
|
||||
* Handle `BlockBody#blank?` at parse time (#1287) [Dylan Thacker-Smith]
|
||||
* Pass the tag markup and tokenizer to `Document#unknown_tag` (#1290) [Dylan Thacker-Smith]
|
||||
* And several internal changes
|
||||
|
||||
### Performance Improvements
|
||||
* Reduce allocations (#1073, #1091, #1115, #1099, #1117, #1141, #1322, #1341) [Richard Monette, Florian Weingarten, Ashwin Maroli]
|
||||
* Improve resources limits performance (#1093, #1323) [Florian Weingarten, Dylan Thacker-Smith]
|
||||
|
||||
## 4.0.3 / 2019-03-12
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
[](https://github.com/Shopify/liquid/actions/workflows/liquid.yml)
|
||||
[](http://travis-ci.org/Shopify/liquid)
|
||||
[](http://inch-ci.org/github/Shopify/liquid)
|
||||
|
||||
# Liquid template engine
|
||||
|
||||
* [Contributing guidelines](CONTRIBUTING.md)
|
||||
* [Version history](History.md)
|
||||
* [Liquid documentation from Shopify](https://shopify.dev/docs/api/liquid)
|
||||
* [Liquid documentation from Shopify](http://docs.shopify.com/themes/liquid-basics)
|
||||
* [Liquid Wiki at GitHub](https://github.com/Shopify/liquid/wiki)
|
||||
* [Website](http://liquidmarkup.org/)
|
||||
|
||||
@@ -56,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.
|
||||
Normally the parser is very lax and will accept almost anything without error. Unfortunately this can make
|
||||
it very hard to debug and can lead to unexpected behaviour.
|
||||
it very hard to debug and can lead to unexpected behaviour.
|
||||
|
||||
Liquid also comes with a stricter parser that can be used when editing templates to give better error messages
|
||||
when templates are invalid. You can enable this new parser like this:
|
||||
|
||||
```ruby
|
||||
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.
|
||||
```
|
||||
|
||||
If you want to set the error mode only on specific templates you can pass `:error_mode` as an option to `parse`:
|
||||
```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.
|
||||
|
||||
@@ -111,4 +111,4 @@ template.render!({ 'x' => 1}, { strict_variables: true })
|
||||
|
||||
To help track usages of a feature or code path in production, we have released opt-in usage tracking. To enable this, we provide an empty `Liquid:: Usage.increment` method which you can customize to your needs. The feature is well suited to https://github.com/Shopify/statsd-instrument. However, the choice of implementation is up to you.
|
||||
|
||||
Once you have enabled usage tracking, we recommend reporting any events through Github Issues that your system may be logging. It is highly likely this event has been added to consider deprecating or improving code specific to this event, so please raise any concerns.
|
||||
Once you have enabled usage tracking, we recommend reporting any events through Github Issues that your system may be logging. It is highly likely this event has been added to consider deprecating or improving code specific to this event, so please raise any concerns.
|
||||
@@ -9,15 +9,9 @@ task(default: [:test, :rubocop])
|
||||
|
||||
desc('run test suite with default parser')
|
||||
Rake::TestTask.new(:base_test) do |t|
|
||||
t.libs << 'lib' << 'test'
|
||||
t.libs << '.' << 'lib' << 'test'
|
||||
t.test_files = FileList['test/{integration,unit}/**/*_test.rb']
|
||||
t.verbose = false
|
||||
end
|
||||
|
||||
Rake::TestTask.new(:integration_test) do |t|
|
||||
t.libs << 'lib' << 'test'
|
||||
t.test_files = FileList['test/integration/**/*_test.rb']
|
||||
t.verbose = false
|
||||
t.verbose = false
|
||||
end
|
||||
|
||||
desc('run test suite with warn error mode')
|
||||
@@ -46,12 +40,12 @@ task :test do
|
||||
ENV['LIQUID_C'] = '1'
|
||||
|
||||
ENV['LIQUID_PARSER_MODE'] = 'lax'
|
||||
Rake::Task['integration_test'].reenable
|
||||
Rake::Task['integration_test'].invoke
|
||||
Rake::Task['base_test'].reenable
|
||||
Rake::Task['base_test'].invoke
|
||||
|
||||
ENV['LIQUID_PARSER_MODE'] = 'strict'
|
||||
Rake::Task['integration_test'].reenable
|
||||
Rake::Task['integration_test'].invoke
|
||||
Rake::Task['base_test'].reenable
|
||||
Rake::Task['base_test'].invoke
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -30,18 +30,14 @@ class Servlet < LiquidServlet
|
||||
private
|
||||
|
||||
def products_list
|
||||
[
|
||||
{ '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 Diamond', 'price' => 59900, 'description' => 'the *arbor diamond* is a made up product because im obsessed with arbor and have no creativity' }
|
||||
]
|
||||
[{ '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 Diamond', 'price' => 59900, 'description' => 'the *arbor diamond* is a made up product because im obsessed with arbor and have no creativity' }]
|
||||
end
|
||||
|
||||
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 Fish', 'price' => 40000, 'description' => 'the *arbor fish* is a compact pin that features an extended wheelbase and time-honored teardrop shape' }
|
||||
]
|
||||
[{ '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' }]
|
||||
end
|
||||
|
||||
def description
|
||||
|
||||
@@ -12,16 +12,16 @@ class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet
|
||||
private
|
||||
|
||||
def handle(_type, req, res)
|
||||
@request = req
|
||||
@request = req
|
||||
@response = res
|
||||
|
||||
@request.path_info =~ /(\w+)\z/
|
||||
@action = Regexp.last_match(1) || 'index'
|
||||
@action = Regexp.last_match(1) || 'index'
|
||||
@assigns = send(@action) if respond_to?(@action)
|
||||
|
||||
@response['Content-Type'] = "text/html"
|
||||
@response.status = 200
|
||||
@response.body = Liquid::Template.parse(read_template).render(@assigns, filters: [ProductsFilter])
|
||||
@response.body = Liquid::Template.parse(read_template).render(@assigns, filters: [ProductsFilter])
|
||||
end
|
||||
|
||||
def read_template(filename = @action)
|
||||
|
||||
+4
-11
@@ -29,7 +29,6 @@ module Liquid
|
||||
WhitespaceControl = '-'
|
||||
TagStart = /\{\%/
|
||||
TagEnd = /\%\}/
|
||||
TagName = /#|\w+/
|
||||
VariableSignature = /\(?[\w\-\.\[\]]\)?/
|
||||
VariableSegment = /[\w\-]/
|
||||
VariableStart = /\{\{/
|
||||
@@ -37,13 +36,11 @@ module Liquid
|
||||
VariableIncompleteEnd = /\}\}?/
|
||||
QuotedString = /"[^"]*"|'[^']*'/
|
||||
QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o
|
||||
TagAttributes = /(\w[\w-]*)\s*\:\s*(#{QuotedFragment})/o
|
||||
TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/o
|
||||
AnyStartingTag = /#{TagStart}|#{VariableStart}/o
|
||||
PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om
|
||||
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om
|
||||
VariableParser = /\[(?>[^\[\]]+|\g<0>)*\]|#{VariableSegment}+\??/o
|
||||
|
||||
RAISE_EXCEPTION_LAMBDA = ->(_e) { raise }
|
||||
VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o
|
||||
|
||||
singleton_class.send(:attr_accessor, :cache_classes)
|
||||
self.cache_classes = true
|
||||
@@ -60,14 +57,11 @@ require 'liquid/forloop_drop'
|
||||
require 'liquid/extensions'
|
||||
require 'liquid/errors'
|
||||
require 'liquid/interrupts'
|
||||
require 'liquid/strainer_template'
|
||||
require 'liquid/strainer_factory'
|
||||
require 'liquid/strainer'
|
||||
require 'liquid/expression'
|
||||
require 'liquid/context'
|
||||
require 'liquid/parser_switching'
|
||||
require 'liquid/tag'
|
||||
require 'liquid/tag/disabler'
|
||||
require 'liquid/tag/disableable'
|
||||
require 'liquid/block'
|
||||
require 'liquid/block_body'
|
||||
require 'liquid/document'
|
||||
@@ -84,8 +78,7 @@ require 'liquid/tokenizer'
|
||||
require 'liquid/parse_context'
|
||||
require 'liquid/partial_cache'
|
||||
require 'liquid/usage'
|
||||
require 'liquid/registers'
|
||||
require 'liquid/template_factory'
|
||||
require 'liquid/static_registers'
|
||||
|
||||
# Load all the tags of the standard library
|
||||
#
|
||||
|
||||
+10
-28
@@ -10,10 +10,9 @@ module Liquid
|
||||
end
|
||||
|
||||
def parse(tokens)
|
||||
@body = new_body
|
||||
@body = BlockBody.new
|
||||
while parse_body(@body, tokens)
|
||||
end
|
||||
@body.freeze
|
||||
end
|
||||
|
||||
# For backwards compatibility
|
||||
@@ -29,33 +28,20 @@ module Liquid
|
||||
@body.nodelist
|
||||
end
|
||||
|
||||
def unknown_tag(tag_name, _markup, _tokenizer)
|
||||
Block.raise_unknown_tag(tag_name, block_name, block_delimiter, parse_context)
|
||||
end
|
||||
|
||||
# @api private
|
||||
def self.raise_unknown_tag(tag, block_name, block_delimiter, parse_context)
|
||||
def unknown_tag(tag, _params, _tokens)
|
||||
if tag == 'else'
|
||||
raise SyntaxError, parse_context.locale.t(
|
||||
"errors.syntax.unexpected_else",
|
||||
block_name: block_name,
|
||||
)
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_else",
|
||||
block_name: block_name)
|
||||
elsif tag.start_with?('end')
|
||||
raise SyntaxError, parse_context.locale.t(
|
||||
"errors.syntax.invalid_delimiter",
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.invalid_delimiter",
|
||||
tag: tag,
|
||||
block_name: block_name,
|
||||
block_delimiter: block_delimiter,
|
||||
)
|
||||
block_delimiter: block_delimiter)
|
||||
else
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag", tag: tag)
|
||||
end
|
||||
end
|
||||
|
||||
def raise_tag_never_closed(block_name)
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_never_closed", block_name: block_name)
|
||||
end
|
||||
|
||||
def block_name
|
||||
@tag_name
|
||||
end
|
||||
@@ -64,14 +50,8 @@ module Liquid
|
||||
@block_delimiter ||= "end#{block_name}"
|
||||
end
|
||||
|
||||
private
|
||||
protected
|
||||
|
||||
# @api public
|
||||
def new_body
|
||||
parse_context.new_block_body
|
||||
end
|
||||
|
||||
# @api public
|
||||
def parse_body(body, tokens)
|
||||
if parse_context.depth >= MAX_DEPTH
|
||||
raise StackLevelError, "Nesting too deep"
|
||||
@@ -82,7 +62,9 @@ module Liquid
|
||||
@blank &&= body.blank?
|
||||
|
||||
return false if end_tag_name == block_delimiter
|
||||
raise_tag_never_closed(block_name) unless end_tag_name
|
||||
unless end_tag_name
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_never_closed", block_name: block_name)
|
||||
end
|
||||
|
||||
# this tag is not registered with the system
|
||||
# pass it to the current block for special handling or error reporting
|
||||
|
||||
+52
-131
@@ -1,27 +1,22 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'English'
|
||||
|
||||
module Liquid
|
||||
class BlockBody
|
||||
LiquidTagToken = /\A\s*(#{TagName})\s*(.*?)\z/o
|
||||
FullToken = /\A#{TagStart}#{WhitespaceControl}?(\s*)(#{TagName})(\s*)(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
|
||||
FullTokenPossiblyInvalid = /\A(.*)#{TagStart}#{WhitespaceControl}?\s*(\w+)\s*(.*)?#{WhitespaceControl}?#{TagEnd}\z/om
|
||||
ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
|
||||
LiquidTagToken = /\A\s*(\w+)\s*(.*?)\z/o
|
||||
FullToken = /\A#{TagStart}#{WhitespaceControl}?(\s*)(\w+)(\s*)(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
|
||||
ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
|
||||
WhitespaceOrNothing = /\A\s*\z/
|
||||
TAGSTART = "{%"
|
||||
VARSTART = "{{"
|
||||
TAGSTART = "{%"
|
||||
VARSTART = "{{"
|
||||
|
||||
attr_reader :nodelist
|
||||
|
||||
def initialize
|
||||
@nodelist = []
|
||||
@blank = true
|
||||
@blank = true
|
||||
end
|
||||
|
||||
def parse(tokenizer, parse_context, &block)
|
||||
raise FrozenError, "can't modify frozen Liquid::BlockBody" if frozen?
|
||||
|
||||
parse_context.line_number = tokenizer.line_number
|
||||
|
||||
if tokenizer.for_liquid_tag
|
||||
@@ -31,28 +26,17 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
def freeze
|
||||
@nodelist.freeze
|
||||
super
|
||||
end
|
||||
|
||||
private def parse_for_liquid_tag(tokenizer, parse_context)
|
||||
while (token = tokenizer.shift)
|
||||
unless token.empty? || token.match?(WhitespaceOrNothing)
|
||||
while token = tokenizer.shift
|
||||
unless token.empty? || token =~ WhitespaceOrNothing
|
||||
unless token =~ LiquidTagToken
|
||||
# line isn't empty but didn't match tag syntax, yield and let the
|
||||
# caller raise a syntax error
|
||||
return yield token, token
|
||||
end
|
||||
tag_name = Regexp.last_match(1)
|
||||
markup = Regexp.last_match(2)
|
||||
|
||||
if tag_name == 'liquid'
|
||||
parse_context.line_number -= 1
|
||||
next parse_liquid_tag(markup, parse_context)
|
||||
end
|
||||
|
||||
unless (tag = registered_tags[tag_name])
|
||||
markup = Regexp.last_match(2)
|
||||
unless tag = registered_tags[tag_name]
|
||||
# end parsing if we reach an unknown tag and let the caller decide
|
||||
# determine how to proceed
|
||||
return yield tag_name, markup
|
||||
@@ -67,74 +51,17 @@ module Liquid
|
||||
yield nil, nil
|
||||
end
|
||||
|
||||
# @api private
|
||||
def self.unknown_tag_in_liquid_tag(tag, parse_context)
|
||||
Block.raise_unknown_tag(tag, 'liquid', '%}', parse_context)
|
||||
end
|
||||
|
||||
# @api private
|
||||
def self.raise_missing_tag_terminator(token, parse_context)
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_termination", token: token, tag_end: TagEnd.inspect)
|
||||
end
|
||||
|
||||
# @api private
|
||||
def self.raise_missing_variable_terminator(token, parse_context)
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.variable_termination", token: token, tag_end: VariableEnd.inspect)
|
||||
end
|
||||
|
||||
# @api private
|
||||
def self.render_node(context, output, node)
|
||||
node.render_to_output_buffer(context, output)
|
||||
rescue => exc
|
||||
blank_tag = !node.instance_of?(Variable) && node.blank?
|
||||
rescue_render_node(context, output, node.line_number, exc, blank_tag)
|
||||
end
|
||||
|
||||
# @api private
|
||||
def self.rescue_render_node(context, output, line_number, exc, blank_tag)
|
||||
case exc
|
||||
when MemoryError
|
||||
raise
|
||||
when UndefinedVariable, UndefinedDropMethod, UndefinedFilter
|
||||
context.handle_error(exc, line_number)
|
||||
else
|
||||
error_message = context.handle_error(exc, line_number)
|
||||
unless blank_tag # conditional for backwards compatibility
|
||||
output << error_message
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private def parse_liquid_tag(markup, parse_context)
|
||||
liquid_tag_tokenizer = parse_context.new_tokenizer(
|
||||
markup, start_line_number: parse_context.line_number, for_liquid_tag: true
|
||||
)
|
||||
parse_for_liquid_tag(liquid_tag_tokenizer, parse_context) do |end_tag_name, _end_tag_markup|
|
||||
if end_tag_name
|
||||
BlockBody.unknown_tag_in_liquid_tag(end_tag_name, parse_context)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private def handle_invalid_tag_token(token, 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?
|
||||
case
|
||||
when token.start_with?(TAGSTART)
|
||||
whitespace_handler(token, parse_context)
|
||||
unless token =~ FullToken
|
||||
return handle_invalid_tag_token(token, parse_context, &block)
|
||||
raise_missing_tag_terminator(token, parse_context)
|
||||
end
|
||||
tag_name = Regexp.last_match(2)
|
||||
markup = Regexp.last_match(4)
|
||||
markup = Regexp.last_match(4)
|
||||
|
||||
if parse_context.line_number
|
||||
# newlines inside the tag should increase the line number,
|
||||
@@ -143,11 +70,11 @@ module Liquid
|
||||
end
|
||||
|
||||
if tag_name == 'liquid'
|
||||
parse_liquid_tag(markup, parse_context)
|
||||
next
|
||||
liquid_tag_tokenizer = Tokenizer.new(markup, line_number: parse_context.line_number, for_liquid_tag: true)
|
||||
next parse_for_liquid_tag(liquid_tag_tokenizer, parse_context, &block)
|
||||
end
|
||||
|
||||
unless (tag = registered_tags[tag_name])
|
||||
unless tag = registered_tags[tag_name]
|
||||
# end parsing if we reach an unknown tag and let the caller decide
|
||||
# determine how to proceed
|
||||
return yield tag_name, markup
|
||||
@@ -165,7 +92,7 @@ module Liquid
|
||||
end
|
||||
parse_context.trim_whitespace = false
|
||||
@nodelist << token
|
||||
@blank &&= token.match?(WhitespaceOrNothing)
|
||||
@blank &&= !!(token =~ WhitespaceOrNothing)
|
||||
end
|
||||
parse_context.line_number = tokenizer.line_number
|
||||
end
|
||||
@@ -177,11 +104,7 @@ module Liquid
|
||||
if token[2] == WhitespaceControl
|
||||
previous_token = @nodelist.last
|
||||
if previous_token.is_a?(String)
|
||||
first_byte = previous_token.getbyte(0)
|
||||
previous_token.rstrip!
|
||||
if previous_token.empty? && parse_context[:bug_compatible_whitespace_trimming] && first_byte
|
||||
previous_token << first_byte
|
||||
end
|
||||
end
|
||||
end
|
||||
parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
|
||||
@@ -191,49 +114,38 @@ module Liquid
|
||||
@blank
|
||||
end
|
||||
|
||||
# Remove blank strings in the block body for a control flow tag (e.g. `if`, `for`, `case`, `unless`)
|
||||
# with a blank body.
|
||||
#
|
||||
# For example, in a conditional assignment like the following
|
||||
#
|
||||
# ```
|
||||
# {% if size > max_size %}
|
||||
# {% assign size = max_size %}
|
||||
# {% endif %}
|
||||
# ```
|
||||
#
|
||||
# we assume the intention wasn't to output the blank spaces in the `if` tag's block body, so this method
|
||||
# will remove them to reduce the render output size.
|
||||
#
|
||||
# Note that it is now preferred to use the `liquid` tag for this use case.
|
||||
def remove_blank_strings
|
||||
raise "remove_blank_strings only support being called on a blank block body" unless @blank
|
||||
@nodelist.reject! { |node| node.instance_of?(String) }
|
||||
end
|
||||
|
||||
def render(context)
|
||||
render_to_output_buffer(context, +'')
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
freeze unless frozen?
|
||||
|
||||
context.resource_limits.increment_render_score(@nodelist.length)
|
||||
context.resource_limits.render_score += @nodelist.length
|
||||
|
||||
idx = 0
|
||||
while (node = @nodelist[idx])
|
||||
if node.instance_of?(String)
|
||||
while node = @nodelist[idx]
|
||||
previous_output_size = output.bytesize
|
||||
|
||||
case node
|
||||
when String
|
||||
output << node
|
||||
else
|
||||
when Variable
|
||||
render_node(context, output, node)
|
||||
when Block
|
||||
render_node(context, node.blank? ? +'' : output, node)
|
||||
break if context.interrupt? # might have happened in a for-block
|
||||
when Continue, Break
|
||||
# If we get an Interrupt that means the block must stop processing. An
|
||||
# Interrupt is any command that stops block execution such as {% break %}
|
||||
# or {% continue %}. These tags may also occur through Block or Include tags.
|
||||
break if context.interrupt? # might have happened in a for-block
|
||||
# or {% continue %}
|
||||
context.push_interrupt(node.interrupt)
|
||||
break
|
||||
else # Other non-Block tags
|
||||
render_node(context, output, node)
|
||||
break if context.interrupt? # might have happened through an include
|
||||
end
|
||||
idx += 1
|
||||
|
||||
context.resource_limits.increment_write_score(output)
|
||||
raise_if_resource_limits_reached(context, output.bytesize - previous_output_size)
|
||||
end
|
||||
|
||||
output
|
||||
@@ -242,25 +154,34 @@ module Liquid
|
||||
private
|
||||
|
||||
def render_node(context, output, node)
|
||||
BlockBody.render_node(context, output, node)
|
||||
node.render_to_output_buffer(context, output)
|
||||
rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
|
||||
context.handle_error(e, node.line_number)
|
||||
rescue ::StandardError => e
|
||||
line_number = node.is_a?(String) ? nil : node.line_number
|
||||
output << context.handle_error(e, line_number)
|
||||
end
|
||||
|
||||
def raise_if_resource_limits_reached(context, length)
|
||||
context.resource_limits.render_length += length
|
||||
return unless context.resource_limits.reached?
|
||||
raise MemoryError, "Memory limits exceeded"
|
||||
end
|
||||
|
||||
def create_variable(token, parse_context)
|
||||
if token =~ ContentOfVariable
|
||||
markup = Regexp.last_match(1)
|
||||
token.scan(ContentOfVariable) do |content|
|
||||
markup = content.first
|
||||
return Variable.new(markup, parse_context)
|
||||
end
|
||||
BlockBody.raise_missing_variable_terminator(token, parse_context)
|
||||
raise_missing_variable_terminator(token, parse_context)
|
||||
end
|
||||
|
||||
# @deprecated Use {.raise_missing_tag_terminator} instead
|
||||
def raise_missing_tag_terminator(token, parse_context)
|
||||
BlockBody.raise_missing_tag_terminator(token, parse_context)
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_termination", token: token, tag_end: TagEnd.inspect)
|
||||
end
|
||||
|
||||
# @deprecated Use {.raise_missing_variable_terminator} instead
|
||||
def raise_missing_variable_terminator(token, parse_context)
|
||||
BlockBody.raise_missing_variable_terminator(token, parse_context)
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.variable_termination", token: token, tag_end: VariableEnd.inspect)
|
||||
end
|
||||
|
||||
def registered_tags
|
||||
|
||||
+15
-42
@@ -8,7 +8,7 @@ module Liquid
|
||||
# c = Condition.new(1, '==', 1)
|
||||
# c.evaluate #=> true
|
||||
#
|
||||
class Condition # :nodoc:
|
||||
class Condition #:nodoc:
|
||||
@@operators = {
|
||||
'==' => ->(cond, left, right) { cond.send(:equal_variables, left, right) },
|
||||
'!=' => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
|
||||
@@ -27,41 +27,22 @@ module Liquid
|
||||
end,
|
||||
}
|
||||
|
||||
class MethodLiteral
|
||||
attr_reader :method_name, :to_s
|
||||
|
||||
def initialize(method_name, to_s)
|
||||
@method_name = method_name
|
||||
@to_s = to_s
|
||||
end
|
||||
end
|
||||
|
||||
@@method_literals = {
|
||||
'blank' => MethodLiteral.new(:blank?, '').freeze,
|
||||
'empty' => MethodLiteral.new(:empty?, '').freeze,
|
||||
}
|
||||
|
||||
def self.operators
|
||||
@@operators
|
||||
end
|
||||
|
||||
def self.parse_expression(parse_context, markup)
|
||||
@@method_literals[markup] || parse_context.parse_expression(markup)
|
||||
end
|
||||
|
||||
attr_reader :attachment, :child_condition
|
||||
attr_accessor :left, :operator, :right
|
||||
|
||||
def initialize(left = nil, operator = nil, right = nil)
|
||||
@left = left
|
||||
@left = left
|
||||
@operator = operator
|
||||
@right = right
|
||||
|
||||
@child_relation = nil
|
||||
@right = right
|
||||
@child_relation = nil
|
||||
@child_condition = nil
|
||||
end
|
||||
|
||||
def evaluate(context = deprecated_default_context)
|
||||
def evaluate(context = Context.new)
|
||||
condition = self
|
||||
result = nil
|
||||
loop do
|
||||
@@ -69,9 +50,9 @@ module Liquid
|
||||
|
||||
case condition.child_relation
|
||||
when :or
|
||||
break if Liquid::Utils.to_liquid_value(result)
|
||||
break if result
|
||||
when :and
|
||||
break unless Liquid::Utils.to_liquid_value(result)
|
||||
break unless result
|
||||
else
|
||||
break
|
||||
end
|
||||
@@ -81,12 +62,12 @@ module Liquid
|
||||
end
|
||||
|
||||
def or(condition)
|
||||
@child_relation = :or
|
||||
@child_relation = :or
|
||||
@child_condition = condition
|
||||
end
|
||||
|
||||
def and(condition)
|
||||
@child_relation = :and
|
||||
@child_relation = :and
|
||||
@child_condition = condition
|
||||
end
|
||||
|
||||
@@ -109,7 +90,7 @@ module Liquid
|
||||
private
|
||||
|
||||
def equal_variables(left, right)
|
||||
if left.is_a?(MethodLiteral)
|
||||
if left.is_a?(Liquid::Expression::MethodLiteral)
|
||||
if right.respond_to?(left.method_name)
|
||||
return right.send(left.method_name)
|
||||
else
|
||||
@@ -117,7 +98,7 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
if right.is_a?(MethodLiteral)
|
||||
if right.is_a?(Liquid::Expression::MethodLiteral)
|
||||
if left.respond_to?(right.method_name)
|
||||
return left.send(right.method_name)
|
||||
else
|
||||
@@ -134,8 +115,8 @@ module Liquid
|
||||
# return this as the result.
|
||||
return context.evaluate(left) if op.nil?
|
||||
|
||||
left = Liquid::Utils.to_liquid_value(context.evaluate(left))
|
||||
right = Liquid::Utils.to_liquid_value(context.evaluate(right))
|
||||
left = context.evaluate(left)
|
||||
right = context.evaluate(right)
|
||||
|
||||
operation = self.class.operators[op] || raise(Liquid::ArgumentError, "Unknown operator #{op}")
|
||||
|
||||
@@ -150,19 +131,11 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
def deprecated_default_context
|
||||
warn("DEPRECATION WARNING: Condition#evaluate without a context argument is deprecated" \
|
||||
" and will be removed from Liquid 6.0.0.")
|
||||
Context.new
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
[
|
||||
@node.left,
|
||||
@node.right,
|
||||
@node.child_condition,
|
||||
@node.attachment
|
||||
@node.left, @node.right,
|
||||
@node.child_condition, @node.attachment
|
||||
].compact
|
||||
end
|
||||
end
|
||||
|
||||
+19
-43
@@ -18,40 +18,32 @@ module Liquid
|
||||
attr_accessor :exception_renderer, :template_name, :partial, :global_filter, :strict_variables, :strict_filters
|
||||
|
||||
# rubocop:disable Metrics/ParameterLists
|
||||
def self.build(environments: {}, outer_scope: {}, registers: {}, rethrow_errors: false, resource_limits: nil, static_environments: {}, &block)
|
||||
new(environments, outer_scope, registers, rethrow_errors, resource_limits, static_environments, &block)
|
||||
def self.build(environments: {}, outer_scope: {}, registers: {}, rethrow_errors: false, resource_limits: nil, static_environments: {})
|
||||
new(environments, outer_scope, registers, rethrow_errors, resource_limits, static_environments)
|
||||
end
|
||||
|
||||
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil, static_environments = {})
|
||||
@environments = [environments]
|
||||
@environments.flatten!
|
||||
|
||||
@static_environments = [static_environments].flatten(1).freeze
|
||||
@static_environments = [static_environments].flat_map(&:freeze).freeze
|
||||
@scopes = [(outer_scope || {})]
|
||||
@registers = registers.is_a?(Registers) ? registers : Registers.new(registers)
|
||||
@registers = registers
|
||||
@errors = []
|
||||
@partial = false
|
||||
@strict_variables = false
|
||||
@resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
|
||||
@base_scope_depth = 0
|
||||
@interrupts = []
|
||||
@filters = []
|
||||
@global_filter = nil
|
||||
@disabled_tags = {}
|
||||
|
||||
@registers.static[:cached_partials] ||= {}
|
||||
@registers.static[:file_system] ||= Liquid::Template.file_system
|
||||
@registers.static[:template_factory] ||= Liquid::TemplateFactory.new
|
||||
squash_instance_assigns_with_environments
|
||||
|
||||
self.exception_renderer = Template.default_exception_renderer
|
||||
if rethrow_errors
|
||||
self.exception_renderer = Liquid::RAISE_EXCEPTION_LAMBDA
|
||||
self.exception_renderer = ->(_e) { raise }
|
||||
end
|
||||
|
||||
yield self if block_given?
|
||||
|
||||
# Do this last, since it could result in this object being passed to a Proc in the environment
|
||||
squash_instance_assigns_with_environments
|
||||
@interrupts = []
|
||||
@filters = []
|
||||
@global_filter = nil
|
||||
end
|
||||
# rubocop:enable Metrics/ParameterLists
|
||||
|
||||
@@ -60,7 +52,7 @@ module Liquid
|
||||
end
|
||||
|
||||
def strainer
|
||||
@strainer ||= StrainerFactory.create(self, @filters)
|
||||
@strainer ||= Strainer.create(self, @filters)
|
||||
end
|
||||
|
||||
# Adds filters to this context.
|
||||
@@ -95,7 +87,7 @@ module Liquid
|
||||
def handle_error(e, line_number = nil)
|
||||
e = internal_error unless e.is_a?(Liquid::Error)
|
||||
e.template_name ||= template_name
|
||||
e.line_number ||= line_number
|
||||
e.line_number ||= line_number
|
||||
errors.push(e)
|
||||
exception_renderer.call(e).to_s
|
||||
end
|
||||
@@ -128,7 +120,7 @@ module Liquid
|
||||
# context['var'] = 'hi'
|
||||
# end
|
||||
#
|
||||
# context['var'] #=> nil
|
||||
# context['var] #=> nil
|
||||
def stack(new_scope = {})
|
||||
push(new_scope)
|
||||
yield
|
||||
@@ -141,18 +133,17 @@ module Liquid
|
||||
def new_isolated_subcontext
|
||||
check_overflow
|
||||
|
||||
self.class.build(
|
||||
Context.build(
|
||||
resource_limits: resource_limits,
|
||||
static_environments: static_environments,
|
||||
registers: Registers.new(registers),
|
||||
registers: StaticRegisters.new(registers)
|
||||
).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.filters = @filters
|
||||
subcontext.filters = @filters
|
||||
subcontext.strainer = nil
|
||||
subcontext.errors = errors
|
||||
subcontext.errors = errors
|
||||
subcontext.warnings = warnings
|
||||
subcontext.disabled_tags = @disabled_tags
|
||||
end
|
||||
end
|
||||
|
||||
@@ -197,7 +188,7 @@ module Liquid
|
||||
try_variable_find_in_environments(key, raise_on_not_found: raise_on_not_found)
|
||||
end
|
||||
|
||||
variable = variable.to_liquid
|
||||
variable = variable.to_liquid
|
||||
variable.context = self if variable.respond_to?(:context=)
|
||||
|
||||
variable
|
||||
@@ -217,24 +208,9 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
def with_disabled_tags(tag_names)
|
||||
tag_names.each do |name|
|
||||
@disabled_tags[name] = @disabled_tags.fetch(name, 0) + 1
|
||||
end
|
||||
yield
|
||||
ensure
|
||||
tag_names.each do |name|
|
||||
@disabled_tags[name] -= 1
|
||||
end
|
||||
end
|
||||
|
||||
def tag_disabled?(tag_name)
|
||||
@disabled_tags.fetch(tag_name, 0) > 0
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
attr_writer :base_scope_depth, :warnings, :errors, :strainer, :filters, :disabled_tags
|
||||
attr_writer :base_scope_depth, :warnings, :errors, :strainer, :filters
|
||||
|
||||
private
|
||||
|
||||
|
||||
+6
-42
@@ -1,34 +1,23 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Document
|
||||
class Document < BlockBody
|
||||
def self.parse(tokens, parse_context)
|
||||
doc = new(parse_context)
|
||||
doc = new
|
||||
doc.parse(tokens, parse_context)
|
||||
doc
|
||||
end
|
||||
|
||||
attr_reader :parse_context, :body
|
||||
|
||||
def initialize(parse_context)
|
||||
@parse_context = parse_context
|
||||
@body = new_body
|
||||
end
|
||||
|
||||
def nodelist
|
||||
@body.nodelist
|
||||
end
|
||||
|
||||
def parse(tokenizer, parse_context)
|
||||
while parse_body(tokenizer)
|
||||
def parse(tokens, parse_context)
|
||||
super do |end_tag_name, _end_tag_params|
|
||||
unknown_tag(end_tag_name, parse_context) if end_tag_name
|
||||
end
|
||||
@body.freeze
|
||||
rescue SyntaxError => e
|
||||
e.line_number ||= parse_context.line_number
|
||||
raise
|
||||
end
|
||||
|
||||
def unknown_tag(tag, _markup, _tokenizer)
|
||||
def unknown_tag(tag, parse_context)
|
||||
case tag
|
||||
when 'else', 'end'
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_outer_tag", tag: tag)
|
||||
@@ -36,30 +25,5 @@ module Liquid
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag", tag: tag)
|
||||
end
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
@body.render_to_output_buffer(context, output)
|
||||
end
|
||||
|
||||
def render(context)
|
||||
render_to_output_buffer(context, +'')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def new_body
|
||||
parse_context.new_block_body
|
||||
end
|
||||
|
||||
def parse_body(tokenizer)
|
||||
@body.parse(tokenizer, parse_context) do |unknown_tag_name, unknown_tag_markup|
|
||||
if unknown_tag_name
|
||||
unknown_tag(unknown_tag_name, unknown_tag_markup, tokenizer)
|
||||
true
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -25,10 +25,6 @@ module Liquid
|
||||
class Drop
|
||||
attr_writer :context
|
||||
|
||||
def initialize
|
||||
@context = nil
|
||||
end
|
||||
|
||||
# Catch all for the method
|
||||
def liquid_method_missing(method)
|
||||
return nil unless @context&.strict_variables
|
||||
|
||||
+15
-16
@@ -40,20 +40,19 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
ArgumentError = Class.new(Error)
|
||||
ContextError = Class.new(Error)
|
||||
FileSystemError = Class.new(Error)
|
||||
StandardError = Class.new(Error)
|
||||
SyntaxError = Class.new(Error)
|
||||
StackLevelError = Class.new(Error)
|
||||
MemoryError = Class.new(Error)
|
||||
ZeroDivisionError = Class.new(Error)
|
||||
FloatDomainError = Class.new(Error)
|
||||
UndefinedVariable = Class.new(Error)
|
||||
UndefinedDropMethod = Class.new(Error)
|
||||
UndefinedFilter = Class.new(Error)
|
||||
MethodOverrideError = Class.new(Error)
|
||||
DisabledError = Class.new(Error)
|
||||
InternalError = Class.new(Error)
|
||||
TemplateEncodingError = Class.new(Error)
|
||||
ArgumentError = Class.new(Error)
|
||||
ContextError = Class.new(Error)
|
||||
FileSystemError = Class.new(Error)
|
||||
StandardError = Class.new(Error)
|
||||
SyntaxError = Class.new(Error)
|
||||
StackLevelError = Class.new(Error)
|
||||
TaintedError = Class.new(Error)
|
||||
MemoryError = Class.new(Error)
|
||||
ZeroDivisionError = Class.new(Error)
|
||||
FloatDomainError = Class.new(Error)
|
||||
UndefinedVariable = Class.new(Error)
|
||||
UndefinedDropMethod = Class.new(Error)
|
||||
UndefinedFilter = Class.new(Error)
|
||||
MethodOverrideError = Class.new(Error)
|
||||
InternalError = Class.new(Error)
|
||||
end
|
||||
|
||||
+36
-33
@@ -2,43 +2,46 @@
|
||||
|
||||
module Liquid
|
||||
class Expression
|
||||
LITERALS = {
|
||||
nil => nil,
|
||||
'nil' => nil,
|
||||
'null' => nil,
|
||||
'' => nil,
|
||||
'true' => true,
|
||||
'false' => false,
|
||||
'blank' => '',
|
||||
'empty' => ''
|
||||
}.freeze
|
||||
class MethodLiteral
|
||||
attr_reader :method_name, :to_s
|
||||
|
||||
INTEGERS_REGEX = /\A(-?\d+)\z/
|
||||
FLOATS_REGEX = /\A(-?\d[\d\.]+)\z/
|
||||
|
||||
# Use an atomic group (?>...) to avoid pathological backtracing from
|
||||
# malicious input as described in https://github.com/Shopify/liquid/issues/1357
|
||||
RANGES_REGEX = /\A\(\s*(?>(\S+)\s*\.\.)\s*(\S+)\s*\)\z/
|
||||
|
||||
def self.parse(markup)
|
||||
return nil unless markup
|
||||
|
||||
markup = markup.strip
|
||||
if (markup.start_with?('"') && markup.end_with?('"')) ||
|
||||
(markup.start_with?("'") && markup.end_with?("'"))
|
||||
return markup[1..-2]
|
||||
def initialize(method_name, to_s)
|
||||
@method_name = method_name
|
||||
@to_s = to_s
|
||||
end
|
||||
|
||||
case markup
|
||||
when INTEGERS_REGEX
|
||||
Regexp.last_match(1).to_i
|
||||
when RANGES_REGEX
|
||||
RangeLookup.parse(Regexp.last_match(1), Regexp.last_match(2))
|
||||
when FLOATS_REGEX
|
||||
Regexp.last_match(1).to_f
|
||||
def to_liquid
|
||||
to_s
|
||||
end
|
||||
end
|
||||
|
||||
LITERALS = {
|
||||
nil => nil, 'nil' => nil, 'null' => nil, '' => nil,
|
||||
'true' => true,
|
||||
'false' => false,
|
||||
'blank' => MethodLiteral.new(:blank?, '').freeze,
|
||||
'empty' => MethodLiteral.new(:empty?, '').freeze
|
||||
}.freeze
|
||||
|
||||
SINGLE_QUOTED_STRING = /\A'(.*)'\z/m
|
||||
DOUBLE_QUOTED_STRING = /\A"(.*)"\z/m
|
||||
INTEGERS_REGEX = /\A(-?\d+)\z/
|
||||
FLOATS_REGEX = /\A(-?\d[\d\.]+)\z/
|
||||
RANGES_REGEX = /\A\((\S+)\.\.(\S+)\)\z/
|
||||
|
||||
def self.parse(markup)
|
||||
if LITERALS.key?(markup)
|
||||
LITERALS[markup]
|
||||
else
|
||||
if LITERALS.key?(markup)
|
||||
LITERALS[markup]
|
||||
case markup
|
||||
when SINGLE_QUOTED_STRING, DOUBLE_QUOTED_STRING
|
||||
Regexp.last_match(1)
|
||||
when INTEGERS_REGEX
|
||||
Regexp.last_match(1).to_i
|
||||
when RANGES_REGEX
|
||||
RangeLookup.parse(Regexp.last_match(1), Regexp.last_match(2))
|
||||
when FLOATS_REGEX
|
||||
Regexp.last_match(1).to_f
|
||||
else
|
||||
VariableLookup.parse(markup)
|
||||
end
|
||||
|
||||
@@ -47,7 +47,7 @@ module Liquid
|
||||
attr_accessor :root
|
||||
|
||||
def initialize(root, pattern = "_%s.liquid")
|
||||
@root = root
|
||||
@root = root
|
||||
@pattern = pattern
|
||||
end
|
||||
|
||||
@@ -59,7 +59,7 @@ module Liquid
|
||||
end
|
||||
|
||||
def full_path(template_path)
|
||||
raise FileSystemError, "Illegal template name '#{template_path}'" unless %r{\A[^./][a-zA-Z0-9_/]+\z}.match?(template_path)
|
||||
raise FileSystemError, "Illegal template name '#{template_path}'" unless template_path =~ %r{\A[^./][a-zA-Z0-9_/]+\z}
|
||||
|
||||
full_path = if template_path.include?('/')
|
||||
File.join(root, File.dirname(template_path), @pattern % File.basename(template_path))
|
||||
|
||||
@@ -1,81 +1,36 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
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
|
||||
def initialize(name, length, parentloop)
|
||||
@name = name
|
||||
@length = length
|
||||
@name = name
|
||||
@length = length
|
||||
@parentloop = parentloop
|
||||
@index = 0
|
||||
@index = 0
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_name length
|
||||
# @liquid_summary
|
||||
# The total number of iterations in the loop.
|
||||
# @liquid_return [number]
|
||||
attr_reader :length
|
||||
attr_reader :name, :length, :parentloop
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_name parentloop
|
||||
# @liquid_summary
|
||||
# The parent `forloop` object.
|
||||
# @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
|
||||
@index + 1
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# The 0-based index of the current iteration.
|
||||
# @liquid_return [number]
|
||||
def index0
|
||||
@index
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# The 1-based index of the current iteration, in reverse order.
|
||||
# @liquid_return [number]
|
||||
def rindex
|
||||
@length - @index
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# The 0-based index of the current iteration, in reverse order.
|
||||
# @liquid_return [number]
|
||||
def rindex0
|
||||
@length - @index - 1
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# Returns `true` if the current iteration is the first. Returns `false` if not.
|
||||
# @liquid_return [boolean]
|
||||
def first
|
||||
@index == 0
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# Returns `true` if the current iteration is the last. Returns `false` if not.
|
||||
# @liquid_return [boolean]
|
||||
def last
|
||||
@index == @length - 1
|
||||
end
|
||||
|
||||
+12
-17
@@ -15,13 +15,12 @@ module Liquid
|
||||
'?' => :question,
|
||||
'-' => :dash,
|
||||
}.freeze
|
||||
IDENTIFIER = /[a-zA-Z_][\w-]*\??/
|
||||
IDENTIFIER = /[a-zA-Z_][\w-]*\??/
|
||||
SINGLE_STRING_LITERAL = /'[^\']*'/
|
||||
DOUBLE_STRING_LITERAL = /"[^\"]*"/
|
||||
STRING_LITERAL = Regexp.union(SINGLE_STRING_LITERAL, DOUBLE_STRING_LITERAL)
|
||||
NUMBER_LITERAL = /-?\d+(\.\d+)?/
|
||||
DOTDOT = /\.\./
|
||||
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/
|
||||
NUMBER_LITERAL = /-?\d+(\.\d+)?/
|
||||
DOTDOT = /\.\./
|
||||
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/
|
||||
WHITESPACE_OR_NOTHING = /\s*/
|
||||
|
||||
def initialize(input)
|
||||
@@ -34,19 +33,15 @@ module Liquid
|
||||
until @ss.eos?
|
||||
@ss.skip(WHITESPACE_OR_NOTHING)
|
||||
break if @ss.eos?
|
||||
tok = if (t = @ss.scan(COMPARISON_OPERATOR))
|
||||
[:comparison, t]
|
||||
elsif (t = @ss.scan(STRING_LITERAL))
|
||||
[:string, t]
|
||||
elsif (t = @ss.scan(NUMBER_LITERAL))
|
||||
[:number, t]
|
||||
elsif (t = @ss.scan(IDENTIFIER))
|
||||
[:id, t]
|
||||
elsif (t = @ss.scan(DOTDOT))
|
||||
[:dotdot, t]
|
||||
tok = if t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t]
|
||||
elsif t = @ss.scan(SINGLE_STRING_LITERAL) then [:string, t]
|
||||
elsif t = @ss.scan(DOUBLE_STRING_LITERAL) then [:string, t]
|
||||
elsif t = @ss.scan(NUMBER_LITERAL) then [:number, t]
|
||||
elsif t = @ss.scan(IDENTIFIER) then [:id, t]
|
||||
elsif t = @ss.scan(DOTDOT) then [:dotdot, t]
|
||||
else
|
||||
c = @ss.getch
|
||||
if (s = SPECIALS[c])
|
||||
c = @ss.getch
|
||||
if s = SPECIALS[c]
|
||||
[s, c]
|
||||
else
|
||||
raise SyntaxError, "Unexpected character #{c}"
|
||||
|
||||
@@ -13,18 +13,15 @@
|
||||
for_invalid_attribute: "Invalid attribute in for loop. Valid attributes are limit and offset"
|
||||
if: "Syntax Error in tag 'if' - Valid syntax: if [expression]"
|
||||
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_template_encoding: "Invalid template encoding"
|
||||
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_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}"
|
||||
tag_never_closed: "'%{block_name}' tag was never closed"
|
||||
meta_syntax_error: "Liquid syntax error: #{e.message}"
|
||||
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"
|
||||
argument:
|
||||
include: "Argument error in tag 'include' - Illegal template name"
|
||||
disabled:
|
||||
tag: "usage is not allowed in this context"
|
||||
|
||||
@@ -7,11 +7,9 @@ module Liquid
|
||||
|
||||
def initialize(options = {})
|
||||
@template_options = options ? options.dup : {}
|
||||
|
||||
@locale = @template_options[:locale] ||= I18n.new
|
||||
@locale = @template_options[:locale] ||= I18n.new
|
||||
@warnings = []
|
||||
|
||||
self.depth = 0
|
||||
self.depth = 0
|
||||
self.partial = false
|
||||
end
|
||||
|
||||
@@ -19,23 +17,11 @@ module Liquid
|
||||
@options[option_key]
|
||||
end
|
||||
|
||||
def new_block_body
|
||||
Liquid::BlockBody.new
|
||||
end
|
||||
|
||||
def new_tokenizer(markup, start_line_number: nil, for_liquid_tag: false)
|
||||
Tokenizer.new(markup, line_number: start_line_number, for_liquid_tag: for_liquid_tag)
|
||||
end
|
||||
|
||||
def parse_expression(markup)
|
||||
Expression.parse(markup)
|
||||
end
|
||||
|
||||
def partial=(value)
|
||||
@partial = value
|
||||
@options = value ? partial_options : @template_options
|
||||
|
||||
@error_mode = @options[:error_mode] || Template.error_mode
|
||||
value
|
||||
end
|
||||
|
||||
def partial_options
|
||||
|
||||
@@ -11,7 +11,7 @@ module Liquid
|
||||
end
|
||||
|
||||
def initialize(node, callbacks)
|
||||
@node = node
|
||||
@node = node
|
||||
@callbacks = callbacks
|
||||
end
|
||||
|
||||
|
||||
+19
-26
@@ -3,9 +3,9 @@
|
||||
module Liquid
|
||||
class Parser
|
||||
def initialize(input)
|
||||
l = Lexer.new(input)
|
||||
l = Lexer.new(input)
|
||||
@tokens = l.tokenize
|
||||
@p = 0 # pointer to current location
|
||||
@p = 0 # pointer to current location
|
||||
end
|
||||
|
||||
def jump(point)
|
||||
@@ -46,20 +46,16 @@ module Liquid
|
||||
tok[0] == type
|
||||
end
|
||||
|
||||
SINGLE_TOKEN_EXPRESSION_TYPES = [:string, :number].freeze
|
||||
private_constant :SINGLE_TOKEN_EXPRESSION_TYPES
|
||||
|
||||
def expression
|
||||
token = @tokens[@p]
|
||||
case token[0]
|
||||
when :id
|
||||
str = consume
|
||||
str << variable_lookups
|
||||
when :open_square
|
||||
str = consume
|
||||
str << expression
|
||||
str << consume(:close_square)
|
||||
str << variable_lookups
|
||||
when :string, :number
|
||||
if token[0] == :id
|
||||
variable_signature
|
||||
elsif SINGLE_TOKEN_EXPRESSION_TYPES.include?(token[0])
|
||||
consume
|
||||
when :open_round
|
||||
elsif token.first == :open_round
|
||||
consume
|
||||
first = expression
|
||||
consume(:dotdot)
|
||||
@@ -82,19 +78,16 @@ module Liquid
|
||||
str
|
||||
end
|
||||
|
||||
def variable_lookups
|
||||
str = +""
|
||||
loop do
|
||||
if look(:open_square)
|
||||
str << consume
|
||||
str << expression
|
||||
str << consume(:close_square)
|
||||
elsif look(:dot)
|
||||
str << consume
|
||||
str << consume(:id)
|
||||
else
|
||||
break
|
||||
end
|
||||
def variable_signature
|
||||
str = consume(:id)
|
||||
while look(:open_square)
|
||||
str << consume
|
||||
str << expression
|
||||
str << consume(:close_square)
|
||||
end
|
||||
if look(:dot)
|
||||
str << consume
|
||||
str << variable_signature
|
||||
end
|
||||
str
|
||||
end
|
||||
|
||||
@@ -2,28 +2,16 @@
|
||||
|
||||
module Liquid
|
||||
module ParserSwitching
|
||||
def strict_parse_with_error_mode_fallback(markup)
|
||||
strict_parse_with_error_context(markup)
|
||||
rescue SyntaxError => e
|
||||
case parse_context.error_mode
|
||||
when :strict
|
||||
raise
|
||||
when :warn
|
||||
parse_context.warnings << e
|
||||
end
|
||||
lax_parse(markup)
|
||||
end
|
||||
|
||||
def parse_with_selected_parser(markup)
|
||||
case parse_context.error_mode
|
||||
when :strict then strict_parse_with_error_context(markup)
|
||||
when :lax then lax_parse(markup)
|
||||
when :warn
|
||||
begin
|
||||
strict_parse_with_error_context(markup)
|
||||
return strict_parse_with_error_context(markup)
|
||||
rescue SyntaxError => e
|
||||
parse_context.warnings << e
|
||||
lax_parse(markup)
|
||||
return lax_parse(markup)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -33,7 +21,7 @@ module Liquid
|
||||
def strict_parse_with_error_context(markup)
|
||||
strict_parse(markup)
|
||||
rescue SyntaxError => e
|
||||
e.line_number = line_number
|
||||
e.line_number = line_number
|
||||
e.markup_context = markup_context(markup)
|
||||
raise e
|
||||
end
|
||||
|
||||
@@ -3,29 +3,16 @@
|
||||
module Liquid
|
||||
class PartialCache
|
||||
def self.load(template_name, context:, parse_context:)
|
||||
cached_partials = context.registers[:cached_partials]
|
||||
cache_key = "#{template_name}:#{parse_context.error_mode}"
|
||||
cached = cached_partials[cache_key]
|
||||
cached_partials = (context.registers[:cached_partials] ||= {})
|
||||
cached = cached_partials[template_name]
|
||||
return cached if cached
|
||||
|
||||
file_system = context.registers[:file_system]
|
||||
source = file_system.read_template_file(template_name)
|
||||
|
||||
file_system = (context.registers[:file_system] ||= Liquid::Template.file_system)
|
||||
source = file_system.read_template_file(template_name)
|
||||
parse_context.partial = true
|
||||
|
||||
template_factory = context.registers[:template_factory]
|
||||
template = template_factory.for(template_name)
|
||||
|
||||
begin
|
||||
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[cache_key] = partial
|
||||
partial = Liquid::Template.parse(source, parse_context)
|
||||
cached_partials[template_name] = partial
|
||||
ensure
|
||||
parse_context.partial = false
|
||||
end
|
||||
|
||||
+84
-63
@@ -25,7 +25,7 @@ module Liquid
|
||||
# node.code
|
||||
#
|
||||
# # Which template and line number of this node.
|
||||
# # The top-level template name is `nil` by default, but can be set in the Liquid::Context before rendering.
|
||||
# # If top level, this will be "<root>".
|
||||
# node.partial
|
||||
# node.line_number
|
||||
#
|
||||
@@ -46,94 +46,115 @@ module Liquid
|
||||
include Enumerable
|
||||
|
||||
class Timing
|
||||
attr_reader :code, :template_name, :line_number, :children
|
||||
attr_accessor :total_time
|
||||
alias_method :render_time, :total_time
|
||||
alias_method :partial, :template_name
|
||||
attr_reader :code, :partial, :line_number, :children
|
||||
|
||||
def initialize(code: nil, template_name: nil, line_number: nil)
|
||||
@code = code
|
||||
@template_name = template_name
|
||||
@line_number = line_number
|
||||
@children = []
|
||||
def initialize(node, partial)
|
||||
@code = node.respond_to?(:raw) ? node.raw : node
|
||||
@partial = partial
|
||||
@line_number = node.respond_to?(:line_number) ? node.line_number : nil
|
||||
@children = []
|
||||
end
|
||||
|
||||
def self_time
|
||||
@self_time ||= begin
|
||||
total_children_time = 0.0
|
||||
@children.each do |child|
|
||||
total_children_time += child.total_time
|
||||
end
|
||||
@total_time - total_children_time
|
||||
end
|
||||
def self.start(node, partial)
|
||||
new(node, partial).tap(&:start)
|
||||
end
|
||||
|
||||
def start
|
||||
@start_time = Time.now
|
||||
end
|
||||
|
||||
def finish
|
||||
@end_time = Time.now
|
||||
end
|
||||
|
||||
def render_time
|
||||
@end_time - @start_time
|
||||
end
|
||||
end
|
||||
|
||||
attr_reader :total_time
|
||||
alias_method :total_render_time, :total_time
|
||||
def self.profile_node_render(node)
|
||||
if Profiler.current_profile && node.respond_to?(:render)
|
||||
Profiler.current_profile.start_node(node)
|
||||
output = yield
|
||||
Profiler.current_profile.end_node(node)
|
||||
output
|
||||
else
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
def self.profile_children(template_name)
|
||||
if Profiler.current_profile
|
||||
Profiler.current_profile.push_partial(template_name)
|
||||
output = yield
|
||||
Profiler.current_profile.pop_partial
|
||||
output
|
||||
else
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
def self.current_profile
|
||||
Thread.current[:liquid_profiler]
|
||||
end
|
||||
|
||||
def initialize
|
||||
@root_children = []
|
||||
@current_children = nil
|
||||
@total_time = 0.0
|
||||
@partial_stack = ["<root>"]
|
||||
|
||||
@root_timing = Timing.new("", current_partial)
|
||||
@timing_stack = [@root_timing]
|
||||
|
||||
@render_start_at = Time.now
|
||||
@render_end_at = @render_start_at
|
||||
end
|
||||
|
||||
def profile(template_name, &block)
|
||||
# nested renders are done from a tag that already has a timing node
|
||||
return yield if @current_children
|
||||
|
||||
root_children = @root_children
|
||||
render_idx = root_children.length
|
||||
begin
|
||||
@current_children = root_children
|
||||
profile_node(template_name, &block)
|
||||
ensure
|
||||
@current_children = nil
|
||||
if (timing = root_children[render_idx])
|
||||
@total_time += timing.total_time
|
||||
end
|
||||
end
|
||||
def start
|
||||
Thread.current[:liquid_profiler] = self
|
||||
@render_start_at = Time.now
|
||||
end
|
||||
|
||||
def children
|
||||
children = @root_children
|
||||
if children.length == 1
|
||||
children.first.children
|
||||
else
|
||||
children
|
||||
end
|
||||
def stop
|
||||
Thread.current[:liquid_profiler] = nil
|
||||
@render_end_at = Time.now
|
||||
end
|
||||
|
||||
def total_render_time
|
||||
@render_end_at - @render_start_at
|
||||
end
|
||||
|
||||
def each(&block)
|
||||
children.each(&block)
|
||||
@root_timing.children.each(&block)
|
||||
end
|
||||
|
||||
def [](idx)
|
||||
children[idx]
|
||||
@root_timing.children[idx]
|
||||
end
|
||||
|
||||
def length
|
||||
children.length
|
||||
@root_timing.children.length
|
||||
end
|
||||
|
||||
def profile_node(template_name, code: nil, line_number: nil)
|
||||
timing = Timing.new(code: code, template_name: template_name, line_number: line_number)
|
||||
parent_children = @current_children
|
||||
start_time = monotonic_time
|
||||
begin
|
||||
@current_children = timing.children
|
||||
yield
|
||||
ensure
|
||||
@current_children = parent_children
|
||||
timing.total_time = monotonic_time - start_time
|
||||
parent_children << timing
|
||||
end
|
||||
def start_node(node)
|
||||
@timing_stack.push(Timing.start(node, current_partial))
|
||||
end
|
||||
|
||||
private
|
||||
def end_node(_node)
|
||||
timing = @timing_stack.pop
|
||||
timing.finish
|
||||
|
||||
def monotonic_time
|
||||
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
||||
@timing_stack.last.children << timing
|
||||
end
|
||||
|
||||
def current_partial
|
||||
@partial_stack.last
|
||||
end
|
||||
|
||||
def push_partial(partial_name)
|
||||
@partial_stack.push(partial_name)
|
||||
end
|
||||
|
||||
def pop_partial
|
||||
@partial_stack.pop
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,35 +1,25 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
module BlockBodyProfilingHook
|
||||
def render_node(context, output, node)
|
||||
if (profiler = context.profiler)
|
||||
profiler.profile_node(context.template_name, code: node.raw, line_number: node.line_number) do
|
||||
super
|
||||
end
|
||||
else
|
||||
super
|
||||
class BlockBody
|
||||
def render_node_with_profiling(context, output, node)
|
||||
Profiler.profile_node_render(node) do
|
||||
render_node_without_profiling(context, output, node)
|
||||
end
|
||||
end
|
||||
end
|
||||
BlockBody.prepend(BlockBodyProfilingHook)
|
||||
|
||||
module DocumentProfilingHook
|
||||
def render_to_output_buffer(context, output)
|
||||
return super unless context.profiler
|
||||
context.profiler.profile(context.template_name) { super }
|
||||
alias_method :render_node_without_profiling, :render_node
|
||||
alias_method :render_node, :render_node_with_profiling
|
||||
end
|
||||
|
||||
class Include < Tag
|
||||
def render_to_output_buffer_with_profiling(context, output)
|
||||
Profiler.profile_children(context.evaluate(@template_name_expr).to_s) do
|
||||
render_to_output_buffer_without_profiling(context, output)
|
||||
end
|
||||
end
|
||||
end
|
||||
Document.prepend(DocumentProfilingHook)
|
||||
|
||||
module ContextProfilingHook
|
||||
attr_accessor :profiler
|
||||
|
||||
def new_isolated_subcontext
|
||||
new_context = super
|
||||
new_context.profiler = profiler
|
||||
new_context
|
||||
end
|
||||
alias_method :render_to_output_buffer_without_profiling, :render_to_output_buffer
|
||||
alias_method :render_to_output_buffer, :render_to_output_buffer_with_profiling
|
||||
end
|
||||
Context.prepend(ContextProfilingHook)
|
||||
end
|
||||
|
||||
@@ -4,34 +4,22 @@ module Liquid
|
||||
class RangeLookup
|
||||
def self.parse(start_markup, end_markup)
|
||||
start_obj = Expression.parse(start_markup)
|
||||
end_obj = Expression.parse(end_markup)
|
||||
end_obj = Expression.parse(end_markup)
|
||||
if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate)
|
||||
new(start_obj, end_obj)
|
||||
else
|
||||
begin
|
||||
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
|
||||
start_obj.to_i..end_obj.to_i
|
||||
end
|
||||
end
|
||||
|
||||
attr_reader :start_obj, :end_obj
|
||||
|
||||
def initialize(start_obj, end_obj)
|
||||
@start_obj = start_obj
|
||||
@end_obj = end_obj
|
||||
@end_obj = end_obj
|
||||
end
|
||||
|
||||
def evaluate(context)
|
||||
start_int = to_integer(context.evaluate(@start_obj))
|
||||
end_int = to_integer(context.evaluate(@end_obj))
|
||||
end_int = to_integer(context.evaluate(@end_obj))
|
||||
start_int..end_int
|
||||
end
|
||||
|
||||
@@ -47,11 +35,5 @@ module Liquid
|
||||
Utils.to_integer(input)
|
||||
end
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
[@node.start_obj, @node.end_obj]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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
|
||||
@@ -2,61 +2,24 @@
|
||||
|
||||
module Liquid
|
||||
class ResourceLimits
|
||||
attr_accessor :render_length_limit, :render_score_limit, :assign_score_limit
|
||||
attr_reader :render_score, :assign_score
|
||||
attr_accessor :render_length, :render_score, :assign_score,
|
||||
:render_length_limit, :render_score_limit, :assign_score_limit
|
||||
|
||||
def initialize(limits)
|
||||
@render_length_limit = limits[:render_length_limit]
|
||||
@render_score_limit = limits[:render_score_limit]
|
||||
@assign_score_limit = limits[:assign_score_limit]
|
||||
@render_score_limit = limits[:render_score_limit]
|
||||
@assign_score_limit = limits[:assign_score_limit]
|
||||
reset
|
||||
end
|
||||
|
||||
def increment_render_score(amount)
|
||||
@render_score += amount
|
||||
raise_limits_reached if @render_score_limit && @render_score > @render_score_limit
|
||||
end
|
||||
|
||||
def increment_assign_score(amount)
|
||||
@assign_score += amount
|
||||
raise_limits_reached if @assign_score_limit && @assign_score > @assign_score_limit
|
||||
end
|
||||
|
||||
# update either render_length or assign_score based on whether or not the writes are captured
|
||||
def increment_write_score(output)
|
||||
if (last_captured = @last_capture_length)
|
||||
captured = output.bytesize
|
||||
increment = captured - last_captured
|
||||
@last_capture_length = captured
|
||||
increment_assign_score(increment)
|
||||
elsif @render_length_limit && output.bytesize > @render_length_limit
|
||||
raise_limits_reached
|
||||
end
|
||||
end
|
||||
|
||||
def raise_limits_reached
|
||||
@reached_limit = true
|
||||
raise MemoryError, "Memory limits exceeded"
|
||||
end
|
||||
|
||||
def reached?
|
||||
@reached_limit
|
||||
(@render_length_limit && @render_length > @render_length_limit) ||
|
||||
(@render_score_limit && @render_score > @render_score_limit) ||
|
||||
(@assign_score_limit && @assign_score > @assign_score_limit)
|
||||
end
|
||||
|
||||
def reset
|
||||
@reached_limit = false
|
||||
@last_capture_length = nil
|
||||
@render_score = @assign_score = 0
|
||||
end
|
||||
|
||||
def with_capture
|
||||
old_capture_length = @last_capture_length
|
||||
begin
|
||||
@last_capture_length = 0
|
||||
yield
|
||||
ensure
|
||||
@last_capture_length = old_capture_length
|
||||
end
|
||||
@render_length = @render_score = @assign_score = 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+96
-591
@@ -1,19 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'cgi'
|
||||
require 'base64'
|
||||
require 'bigdecimal'
|
||||
|
||||
module Liquid
|
||||
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 = {
|
||||
'&' => '&',
|
||||
'>' => '>',
|
||||
@@ -22,119 +13,46 @@ module Liquid
|
||||
"'" => ''',
|
||||
}.freeze
|
||||
HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/
|
||||
STRIP_HTML_BLOCKS = Regexp.union(
|
||||
STRIP_HTML_BLOCKS = Regexp.union(
|
||||
%r{<script.*?</script>}m,
|
||||
/<!--.*?-->/m,
|
||||
%r{<style.*?</style>}m,
|
||||
%r{<style.*?</style>}m
|
||||
)
|
||||
STRIP_HTML_TAGS = /<.*?>/m
|
||||
|
||||
class << self
|
||||
def try_coerce_encoding(input, encoding:)
|
||||
original_encoding = input.encoding
|
||||
if input.encoding != encoding
|
||||
input.force_encoding(encoding)
|
||||
unless input.valid_encoding?
|
||||
input.force_encoding(original_encoding)
|
||||
end
|
||||
end
|
||||
input
|
||||
end
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @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]
|
||||
# Return the size of an array or of an string
|
||||
def size(input)
|
||||
input.respond_to?(:size) ? input.size : 0
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category string
|
||||
# @liquid_summary
|
||||
# Converts a string to all lowercase characters.
|
||||
# @liquid_syntax string | downcase
|
||||
# @liquid_return [string]
|
||||
# convert an input string to DOWNCASE
|
||||
def downcase(input)
|
||||
input.to_s.downcase
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category string
|
||||
# @liquid_summary
|
||||
# Converts a string to all uppercase characters.
|
||||
# @liquid_syntax string | upcase
|
||||
# @liquid_return [string]
|
||||
# convert an input string to UPCASE
|
||||
def upcase(input)
|
||||
input.to_s.upcase
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category string
|
||||
# @liquid_summary
|
||||
# Capitalizes the first word in a string and downcases the remaining characters.
|
||||
# @liquid_syntax string | capitalize
|
||||
# @liquid_return [string]
|
||||
# capitalize words in the input centence
|
||||
def capitalize(input)
|
||||
input.to_s.capitalize
|
||||
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 don’t have a corresponding escape sequence.".
|
||||
# @liquid_syntax string | escape
|
||||
# @liquid_return [string]
|
||||
def escape(input)
|
||||
CGI.escapeHTML(input.to_s) unless input.nil?
|
||||
CGI.escapeHTML(input.to_s).untaint unless input.nil?
|
||||
end
|
||||
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)
|
||||
input.to_s.gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE)
|
||||
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)
|
||||
CGI.escape(input.to_s) unless input.nil?
|
||||
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)
|
||||
return if input.nil?
|
||||
|
||||
@@ -144,231 +62,79 @@ module Liquid
|
||||
result
|
||||
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)
|
||||
input = input.to_s
|
||||
StandardFilters.try_coerce_encoding(Base64.strict_decode64(input), encoding: input.encoding)
|
||||
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)
|
||||
input = input.to_s
|
||||
StandardFilters.try_coerce_encoding(Base64.urlsafe_decode64(input), encoding: input.encoding)
|
||||
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)
|
||||
offset = Utils.to_integer(offset)
|
||||
length = length ? Utils.to_integer(length) : 1
|
||||
|
||||
begin
|
||||
if input.is_a?(Array)
|
||||
input.slice(offset, length) || []
|
||||
else
|
||||
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
|
||||
if input.is_a?(Array)
|
||||
input.slice(offset, length) || []
|
||||
else
|
||||
input.to_s.slice(offset, length) || ''
|
||||
end
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @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]
|
||||
# Truncate a string down to x characters
|
||||
def truncate(input, length = 50, truncate_string = "...")
|
||||
return if input.nil?
|
||||
input_str = input.to_s
|
||||
length = Utils.to_integer(length)
|
||||
|
||||
length = Utils.to_integer(length)
|
||||
truncate_string_str = truncate_string.to_s
|
||||
|
||||
l = length - truncate_string_str.length
|
||||
l = 0 if l < 0
|
||||
|
||||
input_str.length > length ? input_str[0...l].concat(truncate_string_str) : input_str
|
||||
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 = "...")
|
||||
return if input.nil?
|
||||
input = input.to_s
|
||||
wordlist = input.to_s.split
|
||||
words = Utils.to_integer(words)
|
||||
words = 1 if words <= 0
|
||||
|
||||
wordlist = begin
|
||||
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
|
||||
|
||||
wordlist.pop
|
||||
wordlist.join(" ").concat(truncate_string.to_s)
|
||||
l = words - 1
|
||||
l = 0 if l < 0
|
||||
wordlist.length > l ? wordlist[0..l].join(" ").concat(truncate_string.to_s) : input
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category string
|
||||
# @liquid_summary
|
||||
# Splits a string into an array of substrings based on a given separator.
|
||||
# @liquid_syntax string | split: string
|
||||
# @liquid_return [array[string]]
|
||||
# Split input string into an array of substrings separated by given pattern.
|
||||
#
|
||||
# Example:
|
||||
# <div class="summary">{{ post | split '//' | first }}</div>
|
||||
#
|
||||
def split(input, pattern)
|
||||
input.to_s.split(pattern.to_s)
|
||||
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)
|
||||
input.to_s.strip
|
||||
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)
|
||||
input.to_s.lstrip
|
||||
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)
|
||||
input.to_s.rstrip
|
||||
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)
|
||||
empty = ''
|
||||
empty = ''
|
||||
result = input.to_s.gsub(STRIP_HTML_BLOCKS, empty)
|
||||
result.gsub!(STRIP_HTML_TAGS, empty)
|
||||
result
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @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]
|
||||
# Remove all newlines from the string
|
||||
def strip_newlines(input)
|
||||
input.to_s.gsub(/\r?\n/, '')
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @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]
|
||||
# Join elements of the array with certain character between them
|
||||
def join(input, glue = ' ')
|
||||
InputIterator.new(input, context).join(glue)
|
||||
InputIterator.new(input).join(glue)
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @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]]
|
||||
# Sort elements of the array
|
||||
# provide optional property with which to sort an array of hashes or drops
|
||||
def sort(input, property = nil)
|
||||
ary = InputIterator.new(input, context)
|
||||
ary = InputIterator.new(input)
|
||||
|
||||
return [] if ary.empty?
|
||||
|
||||
@@ -385,19 +151,10 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @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]]
|
||||
# Sort elements of an array ignoring case if strings
|
||||
# provide optional property with which to sort an array of hashes or drops
|
||||
def sort_natural(input, property = nil)
|
||||
ary = InputIterator.new(input, context)
|
||||
ary = InputIterator.new(input)
|
||||
|
||||
return [] if ary.empty?
|
||||
|
||||
@@ -414,88 +171,55 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @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]]
|
||||
# Filter the elements of an array to those with a certain property value.
|
||||
# By default the target is any truthy value.
|
||||
def where(input, property, target_value = nil)
|
||||
ary = InputIterator.new(input, context)
|
||||
ary = InputIterator.new(input)
|
||||
|
||||
if ary.empty?
|
||||
[]
|
||||
elsif target_value.nil?
|
||||
ary.select do |item|
|
||||
item[property]
|
||||
elsif ary.first.respond_to?(:[]) && target_value.nil?
|
||||
begin
|
||||
ary.select { |item| item[property] }
|
||||
rescue TypeError
|
||||
raise_property_error(property)
|
||||
rescue NoMethodError
|
||||
return nil unless item.respond_to?(:[])
|
||||
raise
|
||||
end
|
||||
else
|
||||
ary.select do |item|
|
||||
item[property] == target_value
|
||||
elsif ary.first.respond_to?(:[])
|
||||
begin
|
||||
ary.select { |item| item[property] == target_value }
|
||||
rescue TypeError
|
||||
raise_property_error(property)
|
||||
rescue NoMethodError
|
||||
return nil unless item.respond_to?(:[])
|
||||
raise
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category array
|
||||
# @liquid_summary
|
||||
# Removes any duplicate items in an array.
|
||||
# @liquid_syntax array | uniq
|
||||
# @liquid_return [array[untyped]]
|
||||
# Remove duplicate elements from an array
|
||||
# provide optional property with which to determine uniqueness
|
||||
def uniq(input, property = nil)
|
||||
ary = InputIterator.new(input, context)
|
||||
ary = InputIterator.new(input)
|
||||
|
||||
if property.nil?
|
||||
ary.uniq
|
||||
elsif ary.empty? # The next two cases assume a non-empty array.
|
||||
[]
|
||||
else
|
||||
ary.uniq do |item|
|
||||
item[property]
|
||||
elsif ary.first.respond_to?(:[])
|
||||
begin
|
||||
ary.uniq { |a| a[property] }
|
||||
rescue TypeError
|
||||
raise_property_error(property)
|
||||
rescue NoMethodError
|
||||
return nil unless item.respond_to?(:[])
|
||||
raise
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @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]]
|
||||
# Reverse the elements of an array
|
||||
def reverse(input)
|
||||
ary = InputIterator.new(input, context)
|
||||
ary = InputIterator.new(input)
|
||||
ary.reverse
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @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]]
|
||||
# map/collect on a given property
|
||||
def map(input, property)
|
||||
InputIterator.new(input, context).map do |e|
|
||||
InputIterator.new(input).map do |e|
|
||||
e = e.call if e.is_a?(Proc)
|
||||
|
||||
if property == "to_liquid"
|
||||
@@ -509,157 +233,64 @@ module Liquid
|
||||
raise_property_error(property)
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category array
|
||||
# @liquid_summary
|
||||
# Removes any `nil` items from an array.
|
||||
# @liquid_syntax array | compact
|
||||
# @liquid_return [array[untyped]]
|
||||
# Remove nils within an array
|
||||
# provide optional property with which to check for nil
|
||||
def compact(input, property = nil)
|
||||
ary = InputIterator.new(input, context)
|
||||
ary = InputIterator.new(input)
|
||||
|
||||
if property.nil?
|
||||
ary.compact
|
||||
elsif ary.empty? # The next two cases assume a non-empty array.
|
||||
[]
|
||||
else
|
||||
ary.reject do |item|
|
||||
item[property].nil?
|
||||
elsif ary.first.respond_to?(:[])
|
||||
begin
|
||||
ary.reject { |a| a[property].nil? }
|
||||
rescue TypeError
|
||||
raise_property_error(property)
|
||||
rescue NoMethodError
|
||||
return nil unless item.respond_to?(:[])
|
||||
raise
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @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]
|
||||
# Replace occurrences of a string with another
|
||||
def replace(input, string, replacement = '')
|
||||
input.to_s.gsub(string.to_s, replacement.to_s)
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @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]
|
||||
# Replace the first occurrences of a string with another
|
||||
def replace_first(input, string, replacement = '')
|
||||
input.to_s.sub(string.to_s, replacement.to_s)
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @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]
|
||||
# remove a substring
|
||||
def remove(input, string)
|
||||
replace(input, string, '')
|
||||
input.to_s.gsub(string.to_s, '')
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @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]
|
||||
# remove the first occurrences of a substring
|
||||
def remove_first(input, string)
|
||||
replace_first(input, string, '')
|
||||
input.to_s.sub(string.to_s, '')
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @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]
|
||||
# add one string to another
|
||||
def append(input, string)
|
||||
input.to_s + string.to_s
|
||||
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)
|
||||
unless array.respond_to?(:to_ary)
|
||||
raise ArgumentError, "concat filter requires an array argument"
|
||||
end
|
||||
InputIterator.new(input, context).concat(array)
|
||||
InputIterator.new(input).concat(array)
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @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]
|
||||
# prepend a string to another
|
||||
def prepend(input, string)
|
||||
string.to_s + input.to_s
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @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]
|
||||
# Add <br /> tags in front of all newlines in input string
|
||||
def newline_to_br(input)
|
||||
input.to_s.gsub(/\r?\n/, "<br />\n")
|
||||
input.to_s.gsub(/\n/, "<br />\n")
|
||||
end
|
||||
|
||||
# Reformat a date using Ruby's core Time#strftime( string ) -> string
|
||||
@@ -696,111 +327,63 @@ module Liquid
|
||||
def date(input, format)
|
||||
return input if format.to_s.empty?
|
||||
|
||||
return input unless (date = Utils.to_date(input))
|
||||
return input unless date = Utils.to_date(input)
|
||||
|
||||
date.strftime(format.to_s)
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category array
|
||||
# @liquid_summary
|
||||
# Returns the first item in an array.
|
||||
# @liquid_syntax array | first
|
||||
# @liquid_return [untyped]
|
||||
# Get the first element of the passed in array
|
||||
#
|
||||
# Example:
|
||||
# {{ product.images | first | to_img }}
|
||||
#
|
||||
def first(array)
|
||||
array.first if array.respond_to?(:first)
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category array
|
||||
# @liquid_summary
|
||||
# Returns the last item in an array.
|
||||
# @liquid_syntax array | last
|
||||
# @liquid_return [untyped]
|
||||
# Get the last element of the passed in array
|
||||
#
|
||||
# Example:
|
||||
# {{ product.images | last | to_img }}
|
||||
#
|
||||
def last(array)
|
||||
array.last if array.respond_to?(:last)
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category math
|
||||
# @liquid_summary
|
||||
# Returns the absolute value of a number.
|
||||
# @liquid_syntax number | abs
|
||||
# @liquid_return [number]
|
||||
# absolute value
|
||||
def abs(input)
|
||||
result = Utils.to_number(input).abs
|
||||
result.is_a?(BigDecimal) ? result.to_f : result
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category math
|
||||
# @liquid_summary
|
||||
# Adds two numbers.
|
||||
# @liquid_syntax number | plus: number
|
||||
# @liquid_return [number]
|
||||
# addition
|
||||
def plus(input, operand)
|
||||
apply_operation(input, operand, :+)
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category math
|
||||
# @liquid_summary
|
||||
# Subtracts a given number from another number.
|
||||
# @liquid_syntax number | minus: number
|
||||
# @liquid_return [number]
|
||||
# subtraction
|
||||
def minus(input, operand)
|
||||
apply_operation(input, operand, :-)
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category math
|
||||
# @liquid_summary
|
||||
# Multiplies a number by a given number.
|
||||
# @liquid_syntax number | times: number
|
||||
# @liquid_return [number]
|
||||
# multiplication
|
||||
def times(input, operand)
|
||||
apply_operation(input, operand, :*)
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @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]
|
||||
# division
|
||||
def divided_by(input, operand)
|
||||
apply_operation(input, operand, :/)
|
||||
rescue ::ZeroDivisionError => e
|
||||
raise Liquid::ZeroDivisionError, e.message
|
||||
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)
|
||||
apply_operation(input, operand, :%)
|
||||
rescue ::ZeroDivisionError => e
|
||||
raise Liquid::ZeroDivisionError, e.message
|
||||
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)
|
||||
result = Utils.to_number(input).round(Utils.to_number(n))
|
||||
result = result.to_f if result.is_a?(BigDecimal)
|
||||
@@ -810,39 +393,18 @@ module Liquid
|
||||
raise Liquid::FloatDomainError, e.message
|
||||
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)
|
||||
Utils.to_number(input).ceil.to_i
|
||||
rescue ::FloatDomainError => e
|
||||
raise Liquid::FloatDomainError, e.message
|
||||
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)
|
||||
Utils.to_number(input).floor.to_i
|
||||
rescue ::FloatDomainError => e
|
||||
raise Liquid::FloatDomainError, e.message
|
||||
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)
|
||||
min_value = Utils.to_number(n)
|
||||
|
||||
@@ -851,13 +413,6 @@ module Liquid
|
||||
result.is_a?(BigDecimal) ? result.to_f : result
|
||||
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)
|
||||
max_value = Utils.to_number(n)
|
||||
|
||||
@@ -866,58 +421,17 @@ module Liquid
|
||||
result.is_a?(BigDecimal) ? result.to_f : result
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category default
|
||||
# @liquid_summary
|
||||
# Sets a default value for any variable whose value is one of the following:
|
||||
#
|
||||
# - [`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 = {})
|
||||
options = {} unless options.is_a?(Hash)
|
||||
false_check = options['allow_false'] ? input.nil? : !Liquid::Utils.to_liquid_value(input)
|
||||
false_check || (input.respond_to?(:empty?) && input.empty?) ? default_value : input
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category array
|
||||
# @liquid_summary
|
||||
# Returns the sum of all elements in an array.
|
||||
# @liquid_syntax array | sum
|
||||
# @liquid_return [number]
|
||||
def sum(input, property = nil)
|
||||
ary = InputIterator.new(input, context)
|
||||
return 0 if ary.empty?
|
||||
|
||||
values_for_sum = ary.map do |item|
|
||||
if property.nil?
|
||||
item
|
||||
elsif item.respond_to?(:[])
|
||||
item[property]
|
||||
else
|
||||
0
|
||||
end
|
||||
rescue TypeError
|
||||
raise_property_error(property)
|
||||
def default(input, default_value = '')
|
||||
if !input || input.respond_to?(:empty?) && input.empty?
|
||||
Usage.increment("default_filter_received_false_value") if input == false # See https://github.com/Shopify/liquid/issues/1127
|
||||
default_value
|
||||
else
|
||||
input
|
||||
end
|
||||
|
||||
result = InputIterator.new(values_for_sum, context).sum do |item|
|
||||
Utils.to_number(item)
|
||||
end
|
||||
|
||||
result.is_a?(BigDecimal) ? result.to_f : result
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :context
|
||||
|
||||
def raise_property_error(property)
|
||||
raise Liquid::ArgumentError, "cannot select the property '#{property}'"
|
||||
end
|
||||
@@ -928,16 +442,10 @@ module Liquid
|
||||
end
|
||||
|
||||
def nil_safe_compare(a, b)
|
||||
result = a <=> b
|
||||
|
||||
if result
|
||||
result
|
||||
elsif a.nil?
|
||||
1
|
||||
elsif b.nil?
|
||||
-1
|
||||
if !a.nil? && !b.nil?
|
||||
a <=> b
|
||||
else
|
||||
raise Liquid::ArgumentError, "cannot sort values of incompatible types"
|
||||
a.nil? ? 1 : -1
|
||||
end
|
||||
end
|
||||
|
||||
@@ -952,9 +460,8 @@ module Liquid
|
||||
class InputIterator
|
||||
include Enumerable
|
||||
|
||||
def initialize(input, context)
|
||||
@context = context
|
||||
@input = if input.is_a?(Array)
|
||||
def initialize(input)
|
||||
@input = if input.is_a?(Array)
|
||||
input.flatten
|
||||
elsif input.is_a?(Hash)
|
||||
[input]
|
||||
@@ -992,9 +499,7 @@ module Liquid
|
||||
|
||||
def each
|
||||
@input.each do |e|
|
||||
e = e.respond_to?(:to_liquid) ? e.to_liquid : e
|
||||
e.context = @context if e.respond_to?(:context=)
|
||||
yield(e)
|
||||
yield(e.respond_to?(:to_liquid) ? e.to_liquid : e)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class StaticRegisters
|
||||
attr_reader :static, :registers
|
||||
|
||||
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
|
||||
|
||||
def fetch(key, default = nil)
|
||||
key?(key) ? self[key] : default
|
||||
end
|
||||
|
||||
def key?(key)
|
||||
@registers.key?(key) || @static.key?(key)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,68 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'set'
|
||||
|
||||
module Liquid
|
||||
# Strainer is the parent class for the filters system.
|
||||
# New filters are mixed into the strainer class which is then instantiated for each liquid template render run.
|
||||
#
|
||||
# The Strainer only allows method calls defined in filters given to it via Strainer.global_filter,
|
||||
# Context#add_filters or Template.register_filter
|
||||
class Strainer #:nodoc:
|
||||
@@global_strainer = Class.new(Strainer) do
|
||||
@filter_methods = Set.new
|
||||
end
|
||||
@@strainer_class_cache = Hash.new do |hash, filters|
|
||||
hash[filters] = Class.new(@@global_strainer) do
|
||||
@filter_methods = @@global_strainer.filter_methods.dup
|
||||
filters.each { |f| add_filter(f) }
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(context)
|
||||
@context = context
|
||||
end
|
||||
|
||||
class << self
|
||||
attr_reader :filter_methods
|
||||
end
|
||||
|
||||
def self.add_filter(filter)
|
||||
raise ArgumentError, "Expected module but got: #{filter.class}" unless filter.is_a?(Module)
|
||||
unless include?(filter)
|
||||
invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) }
|
||||
if invokable_non_public_methods.any?
|
||||
raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}"
|
||||
else
|
||||
send(:include, filter)
|
||||
@filter_methods.merge(filter.public_instance_methods.map(&:to_s))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.global_filter(filter)
|
||||
@@strainer_class_cache.clear
|
||||
@@global_strainer.add_filter(filter)
|
||||
end
|
||||
|
||||
def self.invokable?(method)
|
||||
@filter_methods.include?(method.to_s)
|
||||
end
|
||||
|
||||
def self.create(context, filters = [])
|
||||
@@strainer_class_cache[filters].new(context)
|
||||
end
|
||||
|
||||
def invoke(method, *args)
|
||||
if self.class.invokable?(method)
|
||||
send(method, *args)
|
||||
elsif @context&.strict_filters
|
||||
raise Liquid::UndefinedFilter, "undefined filter #{method}"
|
||||
else
|
||||
args.first
|
||||
end
|
||||
rescue ::ArgumentError => e
|
||||
raise Liquid::ArgumentError, e.message, e.backtrace
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,41 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# StrainerFactory is the factory for the filters system.
|
||||
module StrainerFactory
|
||||
extend self
|
||||
|
||||
def add_global_filter(filter)
|
||||
strainer_class_cache.clear
|
||||
GlobalCache.add_filter(filter)
|
||||
end
|
||||
|
||||
def create(context, filters = [])
|
||||
strainer_from_cache(filters).new(context)
|
||||
end
|
||||
|
||||
def global_filter_names
|
||||
GlobalCache.filter_method_names
|
||||
end
|
||||
|
||||
GlobalCache = Class.new(StrainerTemplate)
|
||||
|
||||
private
|
||||
|
||||
def strainer_from_cache(filters)
|
||||
if filters.empty?
|
||||
GlobalCache
|
||||
else
|
||||
strainer_class_cache[filters] ||= begin
|
||||
klass = Class.new(GlobalCache)
|
||||
filters.each { |f| klass.add_filter(f) }
|
||||
klass
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def strainer_class_cache
|
||||
@strainer_class_cache ||= {}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,62 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'set'
|
||||
|
||||
module Liquid
|
||||
# StrainerTemplate is the computed class for the filters system.
|
||||
# New filters are mixed into the strainer class which is then instantiated for each liquid template render run.
|
||||
#
|
||||
# The Strainer only allows method calls defined in filters given to it via StrainerFactory.add_global_filter,
|
||||
# Context#add_filters or Template.register_filter
|
||||
class StrainerTemplate
|
||||
def initialize(context)
|
||||
@context = context
|
||||
end
|
||||
|
||||
class << self
|
||||
def add_filter(filter)
|
||||
return if include?(filter)
|
||||
|
||||
invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) }
|
||||
if invokable_non_public_methods.any?
|
||||
raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}"
|
||||
end
|
||||
|
||||
include(filter)
|
||||
|
||||
filter_methods.merge(filter.public_instance_methods.map(&:to_s))
|
||||
end
|
||||
|
||||
def invokable?(method)
|
||||
filter_methods.include?(method.to_s)
|
||||
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
|
||||
|
||||
def filter_methods
|
||||
@filter_methods ||= Set.new
|
||||
end
|
||||
end
|
||||
|
||||
def invoke(method, *args)
|
||||
if self.class.invokable?(method)
|
||||
send(method, *args)
|
||||
elsif @context.strict_filters
|
||||
raise Liquid::UndefinedFilter, "undefined filter #{method}"
|
||||
else
|
||||
args.first
|
||||
end
|
||||
rescue ::ArgumentError => e
|
||||
raise Liquid::ArgumentError, e.message, e.backtrace
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,106 +1,49 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
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
|
||||
def initialize(length, cols)
|
||||
@length = length
|
||||
@row = 1
|
||||
@col = 1
|
||||
@cols = cols
|
||||
@index = 0
|
||||
@row = 1
|
||||
@col = 1
|
||||
@cols = cols
|
||||
@index = 0
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# The total number of iterations in the loop.
|
||||
# @liquid_return [number]
|
||||
attr_reader :length
|
||||
attr_reader :length, :col, :row
|
||||
|
||||
# @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
|
||||
@index + 1
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# The 0-based index of the current iteration.
|
||||
# @liquid_return [number]
|
||||
def index0
|
||||
@index
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# The 0-based index of the current column.
|
||||
# @liquid_return [number]
|
||||
def col0
|
||||
@col - 1
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# The 1-based index of the current iteration, in reverse order.
|
||||
# @liquid_return [number]
|
||||
def rindex
|
||||
@length - @index
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# The 0-based index of the current iteration, in reverse order.
|
||||
# @liquid_return [number]
|
||||
def rindex0
|
||||
@length - @index - 1
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# Returns `true` if the current iteration is the first. Returns `false` if not.
|
||||
# @liquid_return [boolean]
|
||||
def first
|
||||
@index == 0
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# Returns `true` if the current iteration is the last. Returns `false` if not.
|
||||
# @liquid_return [boolean]
|
||||
def last
|
||||
@index == @length - 1
|
||||
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
|
||||
@col == 1
|
||||
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
|
||||
@col == @cols
|
||||
end
|
||||
|
||||
+4
-23
@@ -13,26 +13,14 @@ module Liquid
|
||||
tag
|
||||
end
|
||||
|
||||
def disable_tags(*tag_names)
|
||||
tag_names += disabled_tags
|
||||
define_singleton_method(:disabled_tags) { tag_names }
|
||||
prepend(Disabler)
|
||||
end
|
||||
|
||||
private :new
|
||||
|
||||
protected
|
||||
|
||||
def disabled_tags
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(tag_name, markup, parse_context)
|
||||
@tag_name = tag_name
|
||||
@markup = markup
|
||||
@tag_name = tag_name
|
||||
@markup = markup
|
||||
@parse_context = parse_context
|
||||
@line_number = parse_context.line_number
|
||||
@line_number = parse_context.line_number
|
||||
end
|
||||
|
||||
def parse(_tokens)
|
||||
@@ -54,19 +42,12 @@ module Liquid
|
||||
# of the `render_to_output_buffer` method will become the default and the `render`
|
||||
# method will be removed.
|
||||
def render_to_output_buffer(context, output)
|
||||
render_result = render(context)
|
||||
output << render_result if render_result
|
||||
output << render(context)
|
||||
output
|
||||
end
|
||||
|
||||
def blank?
|
||||
false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse_expression(markup)
|
||||
parse_context.parse_expression(markup)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Tag
|
||||
module Disableable
|
||||
def render_to_output_buffer(context, output)
|
||||
if context.tag_disabled?(tag_name)
|
||||
output << disabled_error(context)
|
||||
return
|
||||
end
|
||||
super
|
||||
end
|
||||
|
||||
def disabled_error(context)
|
||||
# raise then rescue the exception so that the Context#exception_renderer can re-raise it
|
||||
raise DisabledError, "#{tag_name} #{parse_context[:locale].t('errors.disabled.tag')}"
|
||||
rescue DisabledError => exc
|
||||
context.handle_error(exc, line_number)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,13 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Tag
|
||||
module Disabler
|
||||
def render_to_output_buffer(context, output)
|
||||
context.with_disabled_tags(self.class.disabled_tags) do
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
+16
-28
@@ -1,42 +1,37 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category variable
|
||||
# @liquid_name assign
|
||||
# @liquid_summary
|
||||
# Creates a new variable.
|
||||
# @liquid_description
|
||||
# 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.
|
||||
# Assign sets a variable in your template.
|
||||
#
|
||||
# {% assign foo = 'monkey' %}
|
||||
#
|
||||
# You can then use the variable later in the page.
|
||||
#
|
||||
# {{ foo }}
|
||||
#
|
||||
class Assign < Tag
|
||||
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
|
||||
|
||||
# @api private
|
||||
def self.raise_syntax_error(parse_context)
|
||||
raise Liquid::SyntaxError, parse_context.locale.t('errors.syntax.assign')
|
||||
def self.syntax_error_translation_key
|
||||
"errors.syntax.assign"
|
||||
end
|
||||
|
||||
attr_reader :to, :from
|
||||
|
||||
def initialize(tag_name, markup, parse_context)
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
if markup =~ Syntax
|
||||
@to = Regexp.last_match(1)
|
||||
@from = Variable.new(Regexp.last_match(2), parse_context)
|
||||
@to = Regexp.last_match(1)
|
||||
@from = Variable.new(Regexp.last_match(2), options)
|
||||
else
|
||||
self.class.raise_syntax_error(parse_context)
|
||||
raise SyntaxError, options[:locale].t(self.class.syntax_error_translation_key)
|
||||
end
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
val = @from.render(context)
|
||||
context.scopes.last[@to] = val
|
||||
context.resource_limits.increment_assign_score(assign_score_of(val))
|
||||
context.resource_limits.assign_score += assign_score_of(val)
|
||||
output
|
||||
end
|
||||
|
||||
@@ -49,18 +44,11 @@ module Liquid
|
||||
def assign_score_of(val)
|
||||
if val.instance_of?(String)
|
||||
val.bytesize
|
||||
elsif val.instance_of?(Array)
|
||||
elsif val.instance_of?(Array) || val.instance_of?(Hash)
|
||||
sum = 1
|
||||
# Uses #each to avoid extra allocations.
|
||||
val.each { |child| sum += assign_score_of(child) }
|
||||
sum
|
||||
elsif val.instance_of?(Hash)
|
||||
sum = 1
|
||||
val.each do |key, entry_value|
|
||||
sum += assign_score_of(key)
|
||||
sum += assign_score_of(entry_value)
|
||||
end
|
||||
sum
|
||||
else
|
||||
1
|
||||
end
|
||||
|
||||
@@ -10,20 +10,9 @@ module Liquid
|
||||
# {% endif %}
|
||||
# {% 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
|
||||
INTERRUPT = BreakInterrupt.new.freeze
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
context.push_interrupt(INTERRUPT)
|
||||
output
|
||||
def interrupt
|
||||
BreakInterrupt.new
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
+14
-17
@@ -1,20 +1,17 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category variable
|
||||
# @liquid_name capture
|
||||
# @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
|
||||
# Capture stores the result of a block into a variable without rendering it inplace.
|
||||
#
|
||||
# {% capture heading %}
|
||||
# Monkeys!
|
||||
# {% 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
|
||||
Syntax = /(#{VariableSignature}+)/o
|
||||
|
||||
@@ -28,10 +25,10 @@ module Liquid
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
context.resource_limits.with_capture do
|
||||
capture_output = render(context)
|
||||
context.scopes.last[@to] = capture_output
|
||||
end
|
||||
previous_output_size = output.bytesize
|
||||
super
|
||||
context.scopes.last[@to] = output
|
||||
context.resource_limits.assign_score += (output.bytesize - previous_output_size)
|
||||
output
|
||||
end
|
||||
|
||||
|
||||
+6
-42
@@ -1,27 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
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
|
||||
Syntax = /(#{QuotedFragment})/o
|
||||
WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om
|
||||
@@ -33,23 +12,15 @@ module Liquid
|
||||
@blocks = []
|
||||
|
||||
if markup =~ Syntax
|
||||
@left = parse_expression(Regexp.last_match(1))
|
||||
@left = Expression.parse(Regexp.last_match(1))
|
||||
else
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.case")
|
||||
end
|
||||
end
|
||||
|
||||
def parse(tokens)
|
||||
body = case_body = new_body
|
||||
body = BlockBody.new
|
||||
body = @blocks.last.attachment while parse_body(body, tokens)
|
||||
@blocks.reverse_each do |condition|
|
||||
body = condition.attachment
|
||||
unless body.frozen?
|
||||
body.remove_blank_strings if blank?
|
||||
body.freeze
|
||||
end
|
||||
end
|
||||
case_body.freeze
|
||||
end
|
||||
|
||||
def nodelist
|
||||
@@ -73,14 +44,7 @@ module Liquid
|
||||
@blocks.each do |block|
|
||||
if block.else?
|
||||
block.attachment.render_to_output_buffer(context, output) if execute_else_block
|
||||
next
|
||||
end
|
||||
|
||||
result = Liquid::Utils.to_liquid_value(
|
||||
block.evaluate(context),
|
||||
)
|
||||
|
||||
if result
|
||||
elsif block.evaluate(context)
|
||||
execute_else_block = false
|
||||
block.attachment.render_to_output_buffer(context, output)
|
||||
end
|
||||
@@ -92,7 +56,7 @@ module Liquid
|
||||
private
|
||||
|
||||
def record_when_condition(markup)
|
||||
body = new_body
|
||||
body = BlockBody.new
|
||||
|
||||
while markup
|
||||
unless markup =~ WhenSyntax
|
||||
@@ -101,7 +65,7 @@ module Liquid
|
||||
|
||||
markup = Regexp.last_match(2)
|
||||
|
||||
block = Condition.new(@left, '==', Condition.parse_expression(parse_context, Regexp.last_match(1)))
|
||||
block = Condition.new(@left, '==', Expression.parse(Regexp.last_match(1)))
|
||||
block.attach(body)
|
||||
@blocks << block
|
||||
end
|
||||
@@ -113,7 +77,7 @@ module Liquid
|
||||
end
|
||||
|
||||
block = ElseCondition.new
|
||||
block.attach(new_body)
|
||||
block.attach(BlockBody.new)
|
||||
@blocks << block
|
||||
end
|
||||
|
||||
|
||||
@@ -1,19 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
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
|
||||
def render_to_output_buffer(_context, output)
|
||||
output
|
||||
@@ -25,65 +12,6 @@ module Liquid
|
||||
def blank?
|
||||
true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse_body(body, tokenizer)
|
||||
if parse_context.depth >= MAX_DEPTH
|
||||
raise StackLevelError, "Nesting too deep"
|
||||
end
|
||||
|
||||
parse_context.depth += 1
|
||||
comment_tag_depth = 1
|
||||
|
||||
begin
|
||||
# Consume tokens without creating child nodes.
|
||||
# The children tag doesn't require to be a valid Liquid except the comment and raw tag.
|
||||
# The child comment and raw tag must be closed.
|
||||
while (token = tokenizer.send(:shift))
|
||||
tag_name = if tokenizer.for_liquid_tag
|
||||
next if token.empty? || token.match?(BlockBody::WhitespaceOrNothing)
|
||||
|
||||
tag_name_match = BlockBody::LiquidTagToken.match(token)
|
||||
|
||||
next if tag_name_match.nil?
|
||||
|
||||
tag_name_match[1]
|
||||
else
|
||||
token =~ BlockBody::FullToken
|
||||
Regexp.last_match(2)
|
||||
end
|
||||
|
||||
case tag_name
|
||||
when "raw"
|
||||
parse_raw_tag_body(tokenizer)
|
||||
when "comment"
|
||||
comment_tag_depth += 1
|
||||
when "endcomment"
|
||||
comment_tag_depth -= 1
|
||||
end
|
||||
|
||||
if comment_tag_depth.zero?
|
||||
parse_context.trim_whitespace = (token[-3] == WhitespaceControl) unless tokenizer.for_liquid_tag
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
raise_tag_never_closed(block_name)
|
||||
ensure
|
||||
parse_context.depth -= 1
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def parse_raw_tag_body(tokenizer)
|
||||
while (token = tokenizer.send(:shift))
|
||||
return if token =~ BlockBody::FullTokenPossiblyInvalid && "endraw" == Regexp.last_match(2)
|
||||
end
|
||||
|
||||
raise_tag_never_closed("raw")
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('comment', Comment)
|
||||
|
||||
+11
-13
@@ -1,20 +1,18 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category iteration
|
||||
# @liquid_name continue
|
||||
# @liquid_summary
|
||||
# Causes a [`for` loop](/docs/api/liquid/tags/for) to skip to the next iteration.
|
||||
# @liquid_syntax
|
||||
# {% continue %}
|
||||
# Continue tag to be used to break out of a for loop.
|
||||
#
|
||||
# == Basic Usage:
|
||||
# {% for item in collection %}
|
||||
# {% if item.condition %}
|
||||
# {% continue %}
|
||||
# {% endif %}
|
||||
# {% endfor %}
|
||||
#
|
||||
class Continue < Tag
|
||||
INTERRUPT = ContinueInterrupt.new.freeze
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
context.push_interrupt(INTERRUPT)
|
||||
output
|
||||
def interrupt
|
||||
ContinueInterrupt.new
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
+17
-18
@@ -1,19 +1,18 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category iteration
|
||||
# @liquid_name cycle
|
||||
# @liquid_summary
|
||||
# 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
|
||||
# The `cycle` tag must be used inside a `for` loop.
|
||||
# Cycle is usually used within a loop to alternate between values, like colors or DOM classes.
|
||||
#
|
||||
# {% for item in items %}
|
||||
# <div class="{% cycle 'red', 'green', 'blue' %}"> {{ item }} </div>
|
||||
# {% end %}
|
||||
#
|
||||
# <div class="red"> Item one </div>
|
||||
# <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
|
||||
SimpleSyntax = /\A#{QuotedFragment}+/o
|
||||
NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om
|
||||
@@ -25,10 +24,10 @@ module Liquid
|
||||
case markup
|
||||
when NamedSyntax
|
||||
@variables = variables_from_string(Regexp.last_match(2))
|
||||
@name = parse_expression(Regexp.last_match(1))
|
||||
@name = Expression.parse(Regexp.last_match(1))
|
||||
when SimpleSyntax
|
||||
@variables = variables_from_string(markup)
|
||||
@name = @variables.to_s
|
||||
@name = @variables.to_s
|
||||
else
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.cycle")
|
||||
end
|
||||
@@ -37,7 +36,7 @@ module Liquid
|
||||
def render_to_output_buffer(context, output)
|
||||
context.registers[:cycle] ||= {}
|
||||
|
||||
key = context.evaluate(@name)
|
||||
key = context.evaluate(@name)
|
||||
iteration = context.registers[:cycle][key].to_i
|
||||
|
||||
val = context.evaluate(@variables[iteration])
|
||||
@@ -51,9 +50,9 @@ module Liquid
|
||||
output << val
|
||||
|
||||
iteration += 1
|
||||
iteration = 0 if iteration >= @variables.size
|
||||
|
||||
iteration = 0 if iteration >= @variables.size
|
||||
context.registers[:cycle][key] = iteration
|
||||
|
||||
output
|
||||
end
|
||||
|
||||
@@ -62,7 +61,7 @@ module Liquid
|
||||
def variables_from_string(markup)
|
||||
markup.split(',').collect do |var|
|
||||
var =~ /\s*(#{QuotedFragment})\s*/o
|
||||
Regexp.last_match(1) ? parse_expression(Regexp.last_match(1)) : nil
|
||||
Regexp.last_match(1) ? Expression.parse(Regexp.last_match(1)) : nil
|
||||
end.compact
|
||||
end
|
||||
|
||||
|
||||
@@ -1,36 +1,34 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category variable
|
||||
# @liquid_name decrement
|
||||
# @liquid_summary
|
||||
# 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.
|
||||
# decrement is used in a place where one needs to insert a counter
|
||||
# into a template, and needs the counter to survive across
|
||||
# multiple instantiations of the template.
|
||||
# NOTE: decrement is a pre-decrement, --i,
|
||||
# while increment is post: i++.
|
||||
#
|
||||
# Similarly, variables that are created with `decrement` are independent from those created with [`assign`](/docs/api/liquid/tags/assign)
|
||||
# and [`capture`](/docs/api/liquid/tags/capture). However, `decrement` and [`increment`](/docs/api/liquid/tags/increment) share
|
||||
# variables.
|
||||
# @liquid_syntax
|
||||
# {% decrement variable_name %}
|
||||
# @liquid_syntax_keyword variable_name The name of the variable being decremented.
|
||||
class Decrement < Tag
|
||||
attr_reader :variable_name
|
||||
# (To achieve the survival, the application must keep the context)
|
||||
#
|
||||
# if the variable does not exist, it is created with value 0.
|
||||
|
||||
# Hello: {% decrement variable %}
|
||||
#
|
||||
# gives you:
|
||||
#
|
||||
# Hello: -1
|
||||
# Hello: -2
|
||||
# Hello: -3
|
||||
#
|
||||
class Decrement < Tag
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
@variable_name = markup.strip
|
||||
@variable = markup.strip
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
counter_environment = context.environments.first
|
||||
value = counter_environment[@variable_name] || 0
|
||||
value = context.environments.first[@variable] ||= 0
|
||||
value -= 1
|
||||
counter_environment[@variable_name] = value
|
||||
context.environments.first[@variable] = value
|
||||
output << value.to_s
|
||||
output
|
||||
end
|
||||
|
||||
+9
-24
@@ -1,26 +1,17 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category syntax
|
||||
# @liquid_name echo
|
||||
# @liquid_summary
|
||||
# Outputs an expression.
|
||||
# @liquid_description
|
||||
# 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 outputs an expression
|
||||
#
|
||||
# {% echo monkey %}
|
||||
# {% echo user.name %}
|
||||
#
|
||||
# This is identical to variable output syntax, like {{ foo }}, but works
|
||||
# inside {% liquid %} tags. The full syntax is supported, including filters:
|
||||
#
|
||||
# {% echo user | link %}
|
||||
#
|
||||
# > 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
|
||||
attr_reader :variable
|
||||
|
||||
def initialize(tag_name, markup, parse_context)
|
||||
super
|
||||
@variable = Variable.new(markup, parse_context)
|
||||
@@ -29,12 +20,6 @@ module Liquid
|
||||
def render(context)
|
||||
@variable.render_to_output_buffer(context, +'')
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
[@node.variable]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('echo', Echo)
|
||||
|
||||
+61
-50
@@ -1,29 +1,50 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category iteration
|
||||
# @liquid_name for
|
||||
# @liquid_summary
|
||||
# Renders an expression for every item in an array.
|
||||
# @liquid_description
|
||||
# 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.
|
||||
# "For" iterates over an array or collection.
|
||||
# Several useful variables are available to you within the loop.
|
||||
#
|
||||
# == Basic usage:
|
||||
# {% for item in collection %}
|
||||
# {{ forloop.index }}: {{ item.name }}
|
||||
# {% endfor %}
|
||||
#
|
||||
# == 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
|
||||
Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
|
||||
|
||||
@@ -33,20 +54,13 @@ module Liquid
|
||||
super
|
||||
@from = @limit = nil
|
||||
parse_with_selected_parser(markup)
|
||||
@for_block = new_body
|
||||
@for_block = BlockBody.new
|
||||
@else_block = nil
|
||||
end
|
||||
|
||||
def parse(tokens)
|
||||
if parse_body(@for_block, tokens)
|
||||
parse_body(@else_block, tokens)
|
||||
end
|
||||
if blank?
|
||||
@else_block&.remove_blank_strings
|
||||
@for_block.remove_blank_strings
|
||||
end
|
||||
@else_block&.freeze
|
||||
@for_block.freeze
|
||||
return unless parse_body(@for_block, tokens)
|
||||
parse_body(@else_block, tokens)
|
||||
end
|
||||
|
||||
def nodelist
|
||||
@@ -55,7 +69,7 @@ module Liquid
|
||||
|
||||
def unknown_tag(tag, markup, tokens)
|
||||
return super unless tag == 'else'
|
||||
@else_block = new_body
|
||||
@else_block = BlockBody.new
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
@@ -74,11 +88,11 @@ module Liquid
|
||||
|
||||
def lax_parse(markup)
|
||||
if markup =~ Syntax
|
||||
@variable_name = Regexp.last_match(1)
|
||||
collection_name = Regexp.last_match(2)
|
||||
@reversed = !!Regexp.last_match(3)
|
||||
@name = "#{@variable_name}-#{collection_name}"
|
||||
@collection_name = parse_expression(collection_name)
|
||||
@variable_name = Regexp.last_match(1)
|
||||
collection_name = Regexp.last_match(2)
|
||||
@reversed = !!Regexp.last_match(3)
|
||||
@name = "#{@variable_name}-#{collection_name}"
|
||||
@collection_name = Expression.parse(collection_name)
|
||||
markup.scan(TagAttributes) do |key, value|
|
||||
set_attribute(key, value)
|
||||
end
|
||||
@@ -91,19 +105,16 @@ module Liquid
|
||||
p = Parser.new(markup)
|
||||
@variable_name = p.consume(:id)
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in") unless p.id?('in')
|
||||
|
||||
collection_name = p.expression
|
||||
@collection_name = parse_expression(collection_name)
|
||||
|
||||
@name = "#{@variable_name}-#{collection_name}"
|
||||
collection_name = p.expression
|
||||
@name = "#{@variable_name}-#{collection_name}"
|
||||
@collection_name = Expression.parse(collection_name)
|
||||
@reversed = p.id?('reversed')
|
||||
|
||||
while p.look(:comma) || p.look(:id)
|
||||
p.consume?(:comma)
|
||||
unless (attribute = p.id?('limit') || p.id?('offset'))
|
||||
while p.look(:id) && p.look(:colon, 1)
|
||||
unless attribute = p.id?('limit') || p.id?('offset')
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_attribute")
|
||||
end
|
||||
p.consume(:colon)
|
||||
p.consume
|
||||
set_attribute(attribute, p.expression)
|
||||
end
|
||||
p.consume(:end_of_string)
|
||||
@@ -145,7 +156,7 @@ module Liquid
|
||||
|
||||
def render_segment(context, output, segment)
|
||||
for_stack = context.registers[:for_stack] ||= []
|
||||
length = segment.length
|
||||
length = segment.length
|
||||
|
||||
context.stack do
|
||||
loop_vars = Liquid::ForloopDrop.new(@name, length, for_stack[-1])
|
||||
@@ -180,10 +191,10 @@ module Liquid
|
||||
@from = if expr == 'continue'
|
||||
:continue
|
||||
else
|
||||
parse_expression(expr)
|
||||
Expression.parse(expr)
|
||||
end
|
||||
when 'limit'
|
||||
@limit = parse_expression(expr)
|
||||
@limit = Expression.parse(expr)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
+20
-37
@@ -1,22 +1,20 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category conditional
|
||||
# @liquid_name if
|
||||
# @liquid_summary
|
||||
# Renders an expression if a specific condition is `true`.
|
||||
# @liquid_syntax
|
||||
# {% if condition %}
|
||||
# expression
|
||||
# If is the conditional block
|
||||
#
|
||||
# {% if user.admin %}
|
||||
# Admin user!
|
||||
# {% else %}
|
||||
# Not admin user
|
||||
# {% 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
|
||||
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
|
||||
BOOLEAN_OPERATORS = %w(and or).freeze
|
||||
BOOLEAN_OPERATORS = %w(and or).freeze
|
||||
|
||||
attr_reader :blocks
|
||||
|
||||
@@ -33,17 +31,10 @@ module Liquid
|
||||
def parse(tokens)
|
||||
while parse_body(@blocks.last.attachment, tokens)
|
||||
end
|
||||
@blocks.reverse_each do |block|
|
||||
block.attachment.remove_blank_strings if blank?
|
||||
block.attachment.freeze
|
||||
end
|
||||
end
|
||||
|
||||
ELSE_TAG_NAMES = ['elsif', 'else'].freeze
|
||||
private_constant :ELSE_TAG_NAMES
|
||||
|
||||
def unknown_tag(tag, markup, tokens)
|
||||
if ELSE_TAG_NAMES.include?(tag)
|
||||
if ['elsif', 'else'].include?(tag)
|
||||
push_block(tag, markup)
|
||||
else
|
||||
super
|
||||
@@ -52,11 +43,7 @@ module Liquid
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
@blocks.each do |block|
|
||||
result = Liquid::Utils.to_liquid_value(
|
||||
block.evaluate(context),
|
||||
)
|
||||
|
||||
if result
|
||||
if block.evaluate(context)
|
||||
return block.attachment.render_to_output_buffer(context, output)
|
||||
end
|
||||
end
|
||||
@@ -74,25 +61,21 @@ module Liquid
|
||||
end
|
||||
|
||||
@blocks.push(block)
|
||||
block.attach(new_body)
|
||||
end
|
||||
|
||||
def parse_expression(markup)
|
||||
Condition.parse_expression(parse_context, markup)
|
||||
block.attach(BlockBody.new)
|
||||
end
|
||||
|
||||
def lax_parse(markup)
|
||||
expressions = markup.scan(ExpressionsAndOperators)
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop =~ Syntax
|
||||
|
||||
condition = Condition.new(parse_expression(Regexp.last_match(1)), Regexp.last_match(2), parse_expression(Regexp.last_match(3)))
|
||||
condition = Condition.new(Expression.parse(Regexp.last_match(1)), Regexp.last_match(2), Expression.parse(Regexp.last_match(3)))
|
||||
|
||||
until expressions.empty?
|
||||
operator = expressions.pop.to_s.strip
|
||||
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop.to_s =~ Syntax
|
||||
|
||||
new_condition = Condition.new(parse_expression(Regexp.last_match(1)), Regexp.last_match(2), parse_expression(Regexp.last_match(3)))
|
||||
new_condition = Condition.new(Expression.parse(Regexp.last_match(1)), Regexp.last_match(2), Expression.parse(Regexp.last_match(3)))
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.if") unless BOOLEAN_OPERATORS.include?(operator)
|
||||
new_condition.send(operator, condition)
|
||||
condition = new_condition
|
||||
@@ -111,7 +94,7 @@ module Liquid
|
||||
def parse_binary_comparisons(p)
|
||||
condition = parse_comparison(p)
|
||||
first_condition = condition
|
||||
while (op = (p.id?('and') || p.id?('or')))
|
||||
while op = (p.id?('and') || p.id?('or'))
|
||||
child_condition = parse_comparison(p)
|
||||
condition.send(op, child_condition)
|
||||
condition = child_condition
|
||||
@@ -120,9 +103,9 @@ module Liquid
|
||||
end
|
||||
|
||||
def parse_comparison(p)
|
||||
a = parse_expression(p.expression)
|
||||
if (op = p.consume?(:comparison))
|
||||
b = parse_expression(p.expression)
|
||||
a = Expression.parse(p.expression)
|
||||
if op = p.consume?(:comparison)
|
||||
b = Expression.parse(p.expression)
|
||||
Condition.new(a, op, b)
|
||||
else
|
||||
Condition.new(a)
|
||||
|
||||
+25
-33
@@ -1,45 +1,39 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category theme
|
||||
# @liquid_name include
|
||||
# @liquid_summary
|
||||
# Renders a [snippet](/themes/architecture#snippets).
|
||||
# @liquid_description
|
||||
# Inside the snippet, you can access and alter variables that are [created](/docs/api/liquid/tags/variable-tags) outside of the
|
||||
# snippet.
|
||||
# @liquid_syntax
|
||||
# {% include 'filename' %}
|
||||
# @liquid_syntax_keyword filename The name of the snippet to render, without the `.liquid` extension.
|
||||
# @liquid_deprecated
|
||||
# Deprecated because the way that variables are handled reduces performance and makes code harder to both read and maintain.
|
||||
# Include allows templates to relate with other templates
|
||||
#
|
||||
# Simply include another template:
|
||||
#
|
||||
# {% include 'product' %}
|
||||
#
|
||||
# Include a template with a local variable:
|
||||
#
|
||||
# {% include 'product' with products[0] %}
|
||||
#
|
||||
# Include a template for a collection:
|
||||
#
|
||||
# {% include 'product' for products %}
|
||||
#
|
||||
# The `include` tag has been replaced by [`render`](/docs/api/liquid/tags/render).
|
||||
class Include < Tag
|
||||
prepend Tag::Disableable
|
||||
|
||||
SYNTAX = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
|
||||
Syntax = SYNTAX
|
||||
Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/o
|
||||
|
||||
attr_reader :template_name_expr, :variable_name_expr, :attributes
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
|
||||
if markup =~ SYNTAX
|
||||
if markup =~ Syntax
|
||||
|
||||
template_name = Regexp.last_match(1)
|
||||
variable_name = Regexp.last_match(3)
|
||||
|
||||
@alias_name = Regexp.last_match(5)
|
||||
@variable_name_expr = variable_name ? parse_expression(variable_name) : nil
|
||||
@template_name_expr = parse_expression(template_name)
|
||||
@attributes = {}
|
||||
@variable_name_expr = variable_name ? Expression.parse(variable_name) : nil
|
||||
@template_name_expr = Expression.parse(template_name)
|
||||
@attributes = {}
|
||||
|
||||
markup.scan(TagAttributes) do |key, value|
|
||||
@attributes[key] = parse_expression(value)
|
||||
@attributes[key] = Expression.parse(value)
|
||||
end
|
||||
|
||||
else
|
||||
@@ -52,15 +46,15 @@ module Liquid
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
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(
|
||||
template_name,
|
||||
context: context,
|
||||
parse_context: parse_context,
|
||||
parse_context: parse_context
|
||||
)
|
||||
|
||||
context_variable_name = @alias_name || template_name.split('/').last
|
||||
context_variable_name = template_name.split('/').last
|
||||
|
||||
variable = if @variable_name_expr
|
||||
context.evaluate(@variable_name_expr)
|
||||
@@ -69,12 +63,10 @@ module Liquid
|
||||
end
|
||||
|
||||
old_template_name = context.template_name
|
||||
old_partial = context.partial
|
||||
|
||||
old_partial = context.partial
|
||||
begin
|
||||
context.template_name = partial.name
|
||||
context.template_name = template_name
|
||||
context.partial = true
|
||||
|
||||
context.stack do
|
||||
@attributes.each do |key, value|
|
||||
context[key] = context.evaluate(value)
|
||||
@@ -92,7 +84,7 @@ module Liquid
|
||||
end
|
||||
ensure
|
||||
context.template_name = old_template_name
|
||||
context.partial = old_partial
|
||||
context.partial = old_partial
|
||||
end
|
||||
|
||||
output
|
||||
|
||||
@@ -1,35 +1,30 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category variable
|
||||
# @liquid_name increment
|
||||
# @liquid_summary
|
||||
# Creates a new variable, with a default value of 0, that's increased by 1 with each subsequent call.
|
||||
# @liquid_description
|
||||
# Variables that are declared with `increment` 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.
|
||||
# increment is used in a place where one needs to insert a counter
|
||||
# into a template, and needs the counter to survive across
|
||||
# multiple instantiations of the template.
|
||||
# (To achieve the survival, the application must keep the context)
|
||||
#
|
||||
# if the variable does not exist, it is created with value 0.
|
||||
#
|
||||
# Hello: {% increment variable %}
|
||||
#
|
||||
# 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
|
||||
attr_reader :variable_name
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
@variable_name = markup.strip
|
||||
@variable = markup.strip
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
counter_environment = context.environments.first
|
||||
value = counter_environment[@variable_name] || 0
|
||||
counter_environment[@variable_name] = value + 1
|
||||
value = context.environments.first[@variable] ||= 0
|
||||
context.environments.first[@variable] = value + 1
|
||||
|
||||
output << value.to_s
|
||||
output
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class InlineComment < Tag
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
|
||||
# Semantically, a comment should only ignore everything after it on the line.
|
||||
# Currently, this implementation doesn't support mixing a comment with another tag
|
||||
# but we need to reserve future support for this and prevent the introduction
|
||||
# of inline comments from being backward incompatible change.
|
||||
#
|
||||
# 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")
|
||||
end
|
||||
end
|
||||
|
||||
def render_to_output_buffer(_context, output)
|
||||
output
|
||||
end
|
||||
|
||||
def blank?
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('#', InlineComment)
|
||||
end
|
||||
+6
-17
@@ -1,19 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
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
|
||||
Syntax = /\A\s*\z/
|
||||
FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
|
||||
|
||||
def initialize(tag_name, markup, parse_context)
|
||||
super
|
||||
@@ -23,16 +13,15 @@ module Liquid
|
||||
|
||||
def parse(tokens)
|
||||
@body = +''
|
||||
while (token = tokens.shift)
|
||||
if token =~ BlockBody::FullTokenPossiblyInvalid && block_delimiter == Regexp.last_match(2)
|
||||
parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
|
||||
while token = tokens.shift
|
||||
if token =~ FullTokenPossiblyInvalid
|
||||
@body << Regexp.last_match(1) if Regexp.last_match(1) != ""
|
||||
return
|
||||
return if block_delimiter == Regexp.last_match(2)
|
||||
end
|
||||
@body << token unless token.empty?
|
||||
end
|
||||
|
||||
raise_tag_never_closed(block_name)
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_never_closed", block_name: block_name)
|
||||
end
|
||||
|
||||
def render_to_output_buffer(_context, output)
|
||||
@@ -51,7 +40,7 @@ module Liquid
|
||||
protected
|
||||
|
||||
def ensure_valid_markup(tag_name, markup, parse_context)
|
||||
unless Syntax.match?(markup)
|
||||
unless markup =~ Syntax
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_unexpected_args", tag: tag_name)
|
||||
end
|
||||
end
|
||||
|
||||
+14
-71
@@ -1,37 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
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
|
||||
FOR = 'for'
|
||||
SYNTAX = /(#{QuotedString}+)(\s+(with|#{FOR})\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
|
||||
SYNTAX = /(#{QuotedString})#{QuotedFragment}*/o
|
||||
|
||||
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)
|
||||
super
|
||||
@@ -39,62 +12,33 @@ module Liquid
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.render") unless markup =~ SYNTAX
|
||||
|
||||
template_name = Regexp.last_match(1)
|
||||
with_or_for = Regexp.last_match(3)
|
||||
variable_name = Regexp.last_match(4)
|
||||
|
||||
@alias_name = Regexp.last_match(6)
|
||||
@variable_name_expr = variable_name ? parse_expression(variable_name) : nil
|
||||
@template_name_expr = parse_expression(template_name)
|
||||
@is_for_loop = (with_or_for == FOR)
|
||||
@template_name_expr = Expression.parse(template_name)
|
||||
|
||||
@attributes = {}
|
||||
markup.scan(TagAttributes) do |key, value|
|
||||
@attributes[key] = parse_expression(value)
|
||||
@attributes[key] = Expression.parse(value)
|
||||
end
|
||||
end
|
||||
|
||||
def for_loop?
|
||||
@is_for_loop
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
render_tag(context, output)
|
||||
end
|
||||
|
||||
def render_tag(context, output)
|
||||
# The expression should be a String literal, which parses to a String object
|
||||
template_name = @template_name_expr
|
||||
raise ::ArgumentError unless template_name.is_a?(String)
|
||||
# Though we evaluate this here we will only ever parse it as a string literal.
|
||||
template_name = context.evaluate(@template_name_expr)
|
||||
raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name
|
||||
|
||||
partial = PartialCache.load(
|
||||
template_name,
|
||||
context: context,
|
||||
parse_context: parse_context,
|
||||
parse_context: parse_context
|
||||
)
|
||||
|
||||
context_variable_name = @alias_name || template_name.split('/').last
|
||||
|
||||
render_partial_func = ->(var, forloop) {
|
||||
inner_context = context.new_isolated_subcontext
|
||||
inner_context.template_name = partial.name
|
||||
inner_context.partial = true
|
||||
inner_context['forloop'] = forloop if forloop
|
||||
|
||||
@attributes.each do |key, value|
|
||||
inner_context[key] = context.evaluate(value)
|
||||
end
|
||||
inner_context[context_variable_name] = var unless var.nil?
|
||||
partial.render_to_output_buffer(inner_context, output)
|
||||
forloop&.send(:increment!)
|
||||
}
|
||||
|
||||
variable = @variable_name_expr ? context.evaluate(@variable_name_expr) : nil
|
||||
if @is_for_loop && variable.respond_to?(:each) && variable.respond_to?(:count)
|
||||
forloop = Liquid::ForloopDrop.new(template_name, variable.count, nil)
|
||||
variable.each { |var| render_partial_func.call(var, forloop) }
|
||||
else
|
||||
render_partial_func.call(variable, nil)
|
||||
inner_context = context.new_isolated_subcontext
|
||||
inner_context.template_name = template_name
|
||||
inner_context.partial = true
|
||||
@attributes.each do |key, value|
|
||||
inner_context[key] = context.evaluate(value)
|
||||
end
|
||||
partial.render_to_output_buffer(inner_context, output)
|
||||
|
||||
output
|
||||
end
|
||||
@@ -103,7 +47,6 @@ module Liquid
|
||||
def children
|
||||
[
|
||||
@node.template_name_expr,
|
||||
@node.variable_name_expr,
|
||||
] + @node.attributes.values
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,28 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
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
|
||||
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
|
||||
|
||||
@@ -31,11 +9,11 @@ module Liquid
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
if markup =~ Syntax
|
||||
@variable_name = Regexp.last_match(1)
|
||||
@collection_name = parse_expression(Regexp.last_match(2))
|
||||
@attributes = {}
|
||||
@variable_name = Regexp.last_match(1)
|
||||
@collection_name = Expression.parse(Regexp.last_match(2))
|
||||
@attributes = {}
|
||||
markup.scan(TagAttributes) do |key, value|
|
||||
@attributes[key] = parse_expression(value)
|
||||
@attributes[key] = Expression.parse(value)
|
||||
end
|
||||
else
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.table_row")
|
||||
@@ -45,13 +23,14 @@ module Liquid
|
||||
def render_to_output_buffer(context, output)
|
||||
(collection = context.evaluate(@collection_name)) || (return '')
|
||||
|
||||
from = @attributes.key?('offset') ? to_integer(context.evaluate(@attributes['offset'])) : 0
|
||||
to = @attributes.key?('limit') ? from + to_integer(context.evaluate(@attributes['limit'])) : nil
|
||||
from = @attributes.key?('offset') ? context.evaluate(@attributes['offset']).to_i : 0
|
||||
to = @attributes.key?('limit') ? from + context.evaluate(@attributes['limit']).to_i : nil
|
||||
|
||||
collection = Utils.slice_collection(collection, from, to)
|
||||
length = collection.length
|
||||
|
||||
cols = @attributes.key?('cols') ? to_integer(context.evaluate(@attributes['cols'])) : length
|
||||
length = collection.length
|
||||
|
||||
cols = context.evaluate(@attributes['cols']).to_i
|
||||
|
||||
output << "<tr class=\"row1\">\n"
|
||||
context.stack do
|
||||
@@ -82,14 +61,6 @@ module Liquid
|
||||
super + @node.attributes.values + [@node.collection_name]
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def to_integer(value)
|
||||
value.to_i
|
||||
rescue NoMethodError
|
||||
raise Liquid::ArgumentError, "invalid integer"
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('tablerow', TableRow)
|
||||
|
||||
@@ -3,40 +3,21 @@
|
||||
require_relative 'if'
|
||||
|
||||
module Liquid
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category conditional
|
||||
# @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.
|
||||
# Unless is a conditional just like 'if' but works on the inverse logic.
|
||||
#
|
||||
# {% unless x < 0 %} x is greater than zero {% endunless %}
|
||||
#
|
||||
class Unless < If
|
||||
def render_to_output_buffer(context, output)
|
||||
# First condition is interpreted backwards ( if not )
|
||||
first_block = @blocks.first
|
||||
result = Liquid::Utils.to_liquid_value(
|
||||
first_block.evaluate(context),
|
||||
)
|
||||
|
||||
unless result
|
||||
unless first_block.evaluate(context)
|
||||
return first_block.attachment.render_to_output_buffer(context, output)
|
||||
end
|
||||
|
||||
# After the first condition unless works just like if
|
||||
@blocks[1..-1].each do |block|
|
||||
result = Liquid::Utils.to_liquid_value(
|
||||
block.evaluate(context),
|
||||
)
|
||||
|
||||
if result
|
||||
if block.evaluate(context)
|
||||
return block.attachment.render_to_output_buffer(context, output)
|
||||
end
|
||||
end
|
||||
|
||||
+72
-51
@@ -15,14 +15,16 @@ module Liquid
|
||||
# template.render('user_name' => 'bob')
|
||||
#
|
||||
class Template
|
||||
attr_accessor :root, :name
|
||||
attr_accessor :root
|
||||
attr_reader :resource_limits, :warnings
|
||||
|
||||
@@file_system = BlankFileSystem.new
|
||||
|
||||
class TagRegistry
|
||||
include Enumerable
|
||||
|
||||
def initialize
|
||||
@tags = {}
|
||||
@tags = {}
|
||||
@cache = {}
|
||||
end
|
||||
|
||||
@@ -61,59 +63,76 @@ module Liquid
|
||||
# :lax acts like liquid 2.5 and silently ignores malformed tags in most cases.
|
||||
# :warn is the default and will give deprecation warnings when invalid syntax is used.
|
||||
# :strict will enforce correct syntax.
|
||||
attr_accessor :error_mode
|
||||
Template.error_mode = :lax
|
||||
attr_writer :error_mode
|
||||
|
||||
# Sets how strict the taint checker should be.
|
||||
# :lax is the default, and ignores the taint flag completely
|
||||
# :warn adds a warning, but does not interrupt the rendering
|
||||
# :error raises an error when tainted output is used
|
||||
attr_writer :taint_mode
|
||||
|
||||
attr_accessor :default_exception_renderer
|
||||
Template.default_exception_renderer = lambda do |exception|
|
||||
exception
|
||||
end
|
||||
|
||||
attr_accessor :file_system
|
||||
Template.file_system = BlankFileSystem.new
|
||||
def file_system
|
||||
@@file_system
|
||||
end
|
||||
|
||||
attr_accessor :tags
|
||||
Template.tags = TagRegistry.new
|
||||
private :tags=
|
||||
def file_system=(obj)
|
||||
@@file_system = obj
|
||||
end
|
||||
|
||||
def register_tag(name, klass)
|
||||
tags[name.to_s] = klass
|
||||
end
|
||||
|
||||
def tags
|
||||
@tags ||= TagRegistry.new
|
||||
end
|
||||
|
||||
def error_mode
|
||||
@error_mode ||= :lax
|
||||
end
|
||||
|
||||
def taint_mode
|
||||
@taint_mode ||= :lax
|
||||
end
|
||||
|
||||
# Pass a module with filter methods which should be available
|
||||
# to all liquid views. Good for registering the standard library
|
||||
def register_filter(mod)
|
||||
StrainerFactory.add_global_filter(mod)
|
||||
Strainer.global_filter(mod)
|
||||
end
|
||||
|
||||
attr_accessor :default_resource_limits
|
||||
Template.default_resource_limits = {}
|
||||
private :default_resource_limits=
|
||||
def default_resource_limits
|
||||
@default_resource_limits ||= {}
|
||||
end
|
||||
|
||||
# creates a new <tt>Template</tt> object from liquid source code
|
||||
# To enable profiling, pass in <tt>profile: true</tt> as an option.
|
||||
# See Liquid::Profiler for more information
|
||||
def parse(source, options = {})
|
||||
new.parse(source, options)
|
||||
template = Template.new
|
||||
template.parse(source, options)
|
||||
end
|
||||
end
|
||||
|
||||
def initialize
|
||||
@rethrow_errors = false
|
||||
@resource_limits = ResourceLimits.new(Template.default_resource_limits)
|
||||
@rethrow_errors = false
|
||||
@resource_limits = ResourceLimits.new(self.class.default_resource_limits)
|
||||
end
|
||||
|
||||
# Parse source code.
|
||||
# Returns self for easy chaining
|
||||
def parse(source, options = {})
|
||||
parse_context = configure_options(options)
|
||||
|
||||
unless source.valid_encoding?
|
||||
raise TemplateEncodingError, parse_context.locale.t("errors.syntax.invalid_template_encoding")
|
||||
end
|
||||
|
||||
tokenizer = parse_context.new_tokenizer(source, start_line_number: @line_numbers && 1)
|
||||
@root = Document.parse(tokenizer, parse_context)
|
||||
@options = options
|
||||
@profiling = options[:profile]
|
||||
@line_numbers = options[:line_numbers] || @profiling
|
||||
parse_context = options.is_a?(ParseContext) ? options : ParseContext.new(options)
|
||||
@root = Document.parse(tokenize(source), parse_context)
|
||||
@warnings = parse_context.warnings
|
||||
self
|
||||
end
|
||||
|
||||
@@ -155,12 +174,12 @@ module Liquid
|
||||
c = args.shift
|
||||
|
||||
if @rethrow_errors
|
||||
c.exception_renderer = Liquid::RAISE_EXCEPTION_LAMBDA
|
||||
c.exception_renderer = ->(_e) { raise }
|
||||
end
|
||||
|
||||
c
|
||||
when Liquid::Drop
|
||||
drop = args.shift
|
||||
drop = args.shift
|
||||
drop.context = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits)
|
||||
when Hash
|
||||
Context.new([args.shift, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits)
|
||||
@@ -175,12 +194,9 @@ module Liquid
|
||||
case args.last
|
||||
when Hash
|
||||
options = args.pop
|
||||
output = options[:output] if options[:output]
|
||||
static_registers = context.registers.static
|
||||
output = options[:output] if options[:output]
|
||||
|
||||
options[:registers]&.each do |key, register|
|
||||
static_registers[key] = register
|
||||
end
|
||||
registers.merge!(options[:registers]) if options[:registers].is_a?(Hash)
|
||||
|
||||
apply_options_to_context(context, options)
|
||||
when Module, Array
|
||||
@@ -190,15 +206,12 @@ module Liquid
|
||||
# Retrying a render resets resource usage
|
||||
context.resource_limits.reset
|
||||
|
||||
if @profiling && context.profiler.nil?
|
||||
@profiler = context.profiler = Liquid::Profiler.new
|
||||
end
|
||||
|
||||
context.template_name ||= name
|
||||
|
||||
begin
|
||||
# render the nodelist.
|
||||
@root.render_to_output_buffer(context, output || +'')
|
||||
# for performance reasons we get an array back here. join will make a string out of it.
|
||||
with_profiling(context) do
|
||||
@root.render_to_output_buffer(context, output || +'')
|
||||
end
|
||||
rescue Liquid::MemoryError => e
|
||||
context.handle_error(e)
|
||||
ensure
|
||||
@@ -217,25 +230,33 @@ module Liquid
|
||||
|
||||
private
|
||||
|
||||
def configure_options(options)
|
||||
if (profiling = options[:profile])
|
||||
raise "Profiler not loaded, require 'liquid/profiler' first" unless defined?(Liquid::Profiler)
|
||||
end
|
||||
def tokenize(source)
|
||||
Tokenizer.new(source, @line_numbers)
|
||||
end
|
||||
|
||||
@options = options
|
||||
@profiling = profiling
|
||||
@line_numbers = options[:line_numbers] || @profiling
|
||||
parse_context = options.is_a?(ParseContext) ? options : ParseContext.new(options)
|
||||
@warnings = parse_context.warnings
|
||||
parse_context
|
||||
def with_profiling(context)
|
||||
if @profiling && !context.partial
|
||||
raise "Profiler not loaded, require 'liquid/profiler' first" unless defined?(Liquid::Profiler)
|
||||
|
||||
@profiler = Profiler.new
|
||||
@profiler.start
|
||||
|
||||
begin
|
||||
yield
|
||||
ensure
|
||||
@profiler.stop
|
||||
end
|
||||
else
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
def apply_options_to_context(context, options)
|
||||
context.add_filters(options[:filters]) if options[:filters]
|
||||
context.global_filter = options[:global_filter] if options[:global_filter]
|
||||
context.global_filter = options[:global_filter] if options[:global_filter]
|
||||
context.exception_renderer = options[:exception_renderer] if options[:exception_renderer]
|
||||
context.strict_variables = options[:strict_variables] if options[:strict_variables]
|
||||
context.strict_filters = options[:strict_filters] if options[:strict_filters]
|
||||
context.strict_variables = options[:strict_variables] if options[:strict_variables]
|
||||
context.strict_filters = options[:strict_filters] if options[:strict_filters]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class TemplateFactory
|
||||
def for(_template_name)
|
||||
Liquid::Template.new
|
||||
end
|
||||
end
|
||||
end
|
||||
+6
-12
@@ -5,18 +5,14 @@ module Liquid
|
||||
attr_reader :line_number, :for_liquid_tag
|
||||
|
||||
def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false)
|
||||
@source = source.to_s.to_str
|
||||
@line_number = line_number || (line_numbers ? 1 : nil)
|
||||
@source = source
|
||||
@line_number = line_number || (line_numbers ? 1 : nil)
|
||||
@for_liquid_tag = for_liquid_tag
|
||||
@offset = 0
|
||||
@tokens = tokenize
|
||||
@tokens = tokenize
|
||||
end
|
||||
|
||||
def shift
|
||||
token = @tokens[@offset]
|
||||
return nil unless token
|
||||
|
||||
@offset += 1
|
||||
(token = @tokens.shift) || return
|
||||
|
||||
if @line_number
|
||||
@line_number += @for_liquid_tag ? 1 : token.count("\n")
|
||||
@@ -28,16 +24,14 @@ module Liquid
|
||||
private
|
||||
|
||||
def tokenize
|
||||
return [] if @source.empty?
|
||||
return [] if @source.to_s.empty?
|
||||
|
||||
return @source.split("\n") if @for_liquid_tag
|
||||
|
||||
tokens = @source.split(TemplateParser)
|
||||
|
||||
# removes the rogue empty element at the beginning of the array
|
||||
if tokens[0]&.empty?
|
||||
@offset += 1
|
||||
end
|
||||
tokens.shift if tokens[0]&.empty?
|
||||
|
||||
tokens
|
||||
end
|
||||
|
||||
+2
-10
@@ -12,7 +12,7 @@ module Liquid
|
||||
|
||||
def self.slice_collection_using_each(collection, from, to)
|
||||
segments = []
|
||||
index = 0
|
||||
index = 0
|
||||
|
||||
# Maintains Ruby 1.8.7 String#each behaviour on 1.9
|
||||
if collection.is_a?(String)
|
||||
@@ -52,7 +52,7 @@ module Liquid
|
||||
when Numeric
|
||||
obj
|
||||
when String
|
||||
/\A-?\d+\.\d+\z/.match?(obj.strip) ? BigDecimal(obj) : obj.to_i
|
||||
obj.strip =~ /\A-?\d+\.\d+\z/ ? BigDecimal(obj) : obj.to_i
|
||||
else
|
||||
if obj.respond_to?(:to_number)
|
||||
obj.to_number
|
||||
@@ -81,13 +81,5 @@ module Liquid
|
||||
rescue ::ArgumentError
|
||||
nil
|
||||
end
|
||||
|
||||
def self.to_liquid_value(obj)
|
||||
# Enable "obj" to represent itself as a primitive value like integer, string, or boolean
|
||||
return obj.to_liquid_value if obj.respond_to?(:to_liquid_value)
|
||||
|
||||
# Otherwise return the object itself
|
||||
obj
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+40
-31
@@ -12,10 +12,10 @@ module Liquid
|
||||
# {{ user | link }}
|
||||
#
|
||||
class Variable
|
||||
FilterMarkupRegex = /#{FilterSeparator}\s*(.*)/om
|
||||
FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o
|
||||
FilterArgsRegex = /(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o
|
||||
JustTagAttributes = /\A#{TagAttributes}\z/o
|
||||
FilterMarkupRegex = /#{FilterSeparator}\s*(.*)/om
|
||||
FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o
|
||||
FilterArgsRegex = /(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o
|
||||
JustTagAttributes = /\A#{TagAttributes}\z/o
|
||||
MarkupWithQuotedFragment = /(#{QuotedFragment})(.*)/om
|
||||
|
||||
attr_accessor :filters, :name, :line_number
|
||||
@@ -25,12 +25,12 @@ module Liquid
|
||||
include ParserSwitching
|
||||
|
||||
def initialize(markup, parse_context)
|
||||
@markup = markup
|
||||
@name = nil
|
||||
@markup = markup
|
||||
@name = nil
|
||||
@parse_context = parse_context
|
||||
@line_number = parse_context.line_number
|
||||
@line_number = parse_context.line_number
|
||||
|
||||
strict_parse_with_error_mode_fallback(markup)
|
||||
parse_with_selected_parser(markup)
|
||||
end
|
||||
|
||||
def raw
|
||||
@@ -45,9 +45,9 @@ module Liquid
|
||||
@filters = []
|
||||
return unless markup =~ MarkupWithQuotedFragment
|
||||
|
||||
name_markup = Regexp.last_match(1)
|
||||
name_markup = Regexp.last_match(1)
|
||||
filter_markup = Regexp.last_match(2)
|
||||
@name = parse_context.parse_expression(name_markup)
|
||||
@name = Expression.parse(name_markup)
|
||||
if filter_markup =~ FilterMarkupRegex
|
||||
filters = Regexp.last_match(1).scan(FilterParser)
|
||||
filters.each do |f|
|
||||
@@ -63,9 +63,7 @@ module Liquid
|
||||
@filters = []
|
||||
p = Parser.new(markup)
|
||||
|
||||
return if p.look(:end_of_string)
|
||||
|
||||
@name = parse_context.parse_expression(p.expression)
|
||||
@name = Expression.parse(p.expression)
|
||||
while p.consume?(:pipe)
|
||||
filtername = p.consume(:id)
|
||||
filterargs = p.consume?(:colon) ? parse_filterargs(p) : []
|
||||
@@ -83,14 +81,14 @@ module Liquid
|
||||
end
|
||||
|
||||
def render(context)
|
||||
obj = context.evaluate(@name)
|
||||
|
||||
@filters.each do |filter_name, filter_args, filter_kwargs|
|
||||
obj = @filters.inject(context.evaluate(@name)) do |output, (filter_name, filter_args, filter_kwargs)|
|
||||
filter_args = evaluate_filter_expressions(context, filter_args, filter_kwargs)
|
||||
obj = context.invoke(filter_name, obj, *filter_args)
|
||||
context.invoke(filter_name, output, *filter_args)
|
||||
end
|
||||
|
||||
context.apply_global_filter(obj)
|
||||
obj = context.apply_global_filter(obj)
|
||||
taint_check(context, obj)
|
||||
obj
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
@@ -106,25 +104,17 @@ module Liquid
|
||||
output
|
||||
end
|
||||
|
||||
def disabled?(_context)
|
||||
false
|
||||
end
|
||||
|
||||
def disabled_tags
|
||||
[]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse_filter_expressions(filter_name, unparsed_args)
|
||||
filter_args = []
|
||||
filter_args = []
|
||||
keyword_args = nil
|
||||
unparsed_args.each do |a|
|
||||
if (matches = a.match(JustTagAttributes))
|
||||
keyword_args ||= {}
|
||||
keyword_args[matches[1]] = parse_context.parse_expression(matches[2])
|
||||
if matches = a.match(JustTagAttributes)
|
||||
keyword_args ||= {}
|
||||
keyword_args[matches[1]] = Expression.parse(matches[2])
|
||||
else
|
||||
filter_args << parse_context.parse_expression(a)
|
||||
filter_args << Expression.parse(a)
|
||||
end
|
||||
end
|
||||
result = [filter_name, filter_args]
|
||||
@@ -144,6 +134,25 @@ module Liquid
|
||||
parsed_args
|
||||
end
|
||||
|
||||
def taint_check(context, obj)
|
||||
return unless obj.tainted?
|
||||
return if Template.taint_mode == :lax
|
||||
|
||||
@markup =~ QuotedFragment
|
||||
name = Regexp.last_match(0)
|
||||
|
||||
error = TaintedError.new("variable '#{name}' is tainted and was not escaped")
|
||||
error.line_number = line_number
|
||||
error.template_name = context.template_name
|
||||
|
||||
case Template.taint_mode
|
||||
when :warn
|
||||
context.warnings << error
|
||||
when :error
|
||||
raise error
|
||||
end
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
[@node.name] + @node.filters.flatten
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
module Liquid
|
||||
class VariableLookup
|
||||
SQUARE_BRACKETED = /\A\[(.*)\]\z/m
|
||||
COMMAND_METHODS = ['size', 'first', 'last'].freeze
|
||||
|
||||
attr_reader :name, :lookups
|
||||
@@ -14,38 +15,31 @@ module Liquid
|
||||
lookups = markup.scan(VariableParser)
|
||||
|
||||
name = lookups.shift
|
||||
if name&.start_with?('[') && name&.end_with?(']')
|
||||
name = Expression.parse(name[1..-2])
|
||||
if name =~ SQUARE_BRACKETED
|
||||
name = Expression.parse(Regexp.last_match(1))
|
||||
end
|
||||
@name = name
|
||||
|
||||
@lookups = lookups
|
||||
@lookups = lookups
|
||||
@command_flags = 0
|
||||
|
||||
@lookups.each_index do |i|
|
||||
lookup = lookups[i]
|
||||
if lookup&.start_with?('[') && lookup&.end_with?(']')
|
||||
lookups[i] = Expression.parse(lookup[1..-2])
|
||||
if lookup =~ SQUARE_BRACKETED
|
||||
lookups[i] = Expression.parse(Regexp.last_match(1))
|
||||
elsif COMMAND_METHODS.include?(lookup)
|
||||
@command_flags |= 1 << i
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def lookup_command?(lookup_index)
|
||||
@command_flags & (1 << lookup_index) != 0
|
||||
end
|
||||
|
||||
def evaluate(context)
|
||||
name = context.evaluate(@name)
|
||||
name = context.evaluate(@name)
|
||||
object = context.find_variable(name)
|
||||
|
||||
@lookups.each_index do |i|
|
||||
key = context.evaluate(@lookups[i])
|
||||
|
||||
# Cast "key" to its liquid value to enable it to act as a primitive value
|
||||
key = Liquid::Utils.to_liquid_value(key)
|
||||
|
||||
# If object is a hash- or array-like object we look for the
|
||||
# presence of the key and if its available we return it
|
||||
if object.respond_to?(:[]) &&
|
||||
@@ -53,13 +47,13 @@ module Liquid
|
||||
(object.respond_to?(:fetch) && key.is_a?(Integer)))
|
||||
|
||||
# if its a proc we will replace the entry with the proc
|
||||
res = context.lookup_and_evaluate(object, key)
|
||||
res = context.lookup_and_evaluate(object, key)
|
||||
object = res.to_liquid
|
||||
|
||||
# Some special cases. If the part wasn't in square brackets and
|
||||
# no key with the same name was found we interpret following calls
|
||||
# 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
|
||||
|
||||
# No key was present with the desired value and it wasn't one of the directly supported
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
VERSION = "5.4.0"
|
||||
VERSION = "4.0.3"
|
||||
end
|
||||
|
||||
+4
-5
@@ -17,17 +17,16 @@ Gem::Specification.new do |s|
|
||||
s.license = "MIT"
|
||||
# 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.4.0"
|
||||
s.required_rubygems_version = ">= 1.3.7"
|
||||
|
||||
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.require_path = "lib"
|
||||
|
||||
s.add_development_dependency('rake', '~> 13.0')
|
||||
s.add_development_dependency('rake', '~> 11.3')
|
||||
s.add_development_dependency('minitest')
|
||||
end
|
||||
|
||||
@@ -7,7 +7,7 @@ Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
|
||||
profiler = ThemeRunner.new
|
||||
|
||||
Benchmark.ips do |x|
|
||||
x.time = 10
|
||||
x.time = 10
|
||||
x.warmup = 5
|
||||
|
||||
puts
|
||||
|
||||
@@ -21,8 +21,8 @@ class Profiler
|
||||
end
|
||||
|
||||
def profile(phase, &block)
|
||||
print(LOG_LABEL)
|
||||
print("#{phase}.. ".ljust(10))
|
||||
print LOG_LABEL
|
||||
print "#{phase}.. ".ljust(10)
|
||||
report = MemoryProfiler.report(&block)
|
||||
puts 'Done.'
|
||||
@headings << phase.capitalize
|
||||
|
||||
@@ -15,7 +15,7 @@ profiler.run
|
||||
end
|
||||
end
|
||||
|
||||
if profile_type == :cpu && (graph_filename = ENV['GRAPH_FILENAME'])
|
||||
if profile_type == :cpu && graph_filename = ENV['GRAPH_FILENAME']
|
||||
File.open(graph_filename, 'w') do |f|
|
||||
StackProf::Report.new(results).print_graphviz(nil, f)
|
||||
end
|
||||
|
||||
@@ -8,7 +8,7 @@ class CommentForm < Liquid::Block
|
||||
|
||||
if markup =~ Syntax
|
||||
@variable_name = Regexp.last_match(1)
|
||||
@attributes = {}
|
||||
@attributes = {}
|
||||
else
|
||||
raise SyntaxError, "Syntax Error in 'comment_form' - Valid syntax: comment_form [article]"
|
||||
end
|
||||
|
||||
@@ -3,19 +3,11 @@
|
||||
require 'yaml'
|
||||
|
||||
module Database
|
||||
DATABASE_FILE_PATH = "#{__dir__}/vision.database.yml"
|
||||
|
||||
# Load the standard vision toolkit database and re-arrage it to be simply exportable
|
||||
# to liquid as assigns. All this is based on Shopify
|
||||
def self.tables
|
||||
@tables ||= begin
|
||||
db =
|
||||
if YAML.respond_to?(:unsafe_load_file) # Only Psych 4+ can use unsafe_load_file
|
||||
# unsafe_load_file is needed for YAML references
|
||||
YAML.unsafe_load_file(DATABASE_FILE_PATH)
|
||||
else
|
||||
YAML.load_file(DATABASE_FILE_PATH)
|
||||
end
|
||||
db = YAML.load_file("#{__dir__}/vision.database.yml")
|
||||
|
||||
# From vision source
|
||||
db['products'].each do |product|
|
||||
@@ -40,8 +32,8 @@ module Database
|
||||
db['article'] = db['blog']['articles'].first
|
||||
|
||||
db['cart'] = {
|
||||
'total_price' => db['line_items'].values.inject(0) { |sum, item| sum + item['line_price'] * item['quantity'] },
|
||||
'item_count' => db['line_items'].values.inject(0) { |sum, item| sum + item['quantity'] },
|
||||
'total_price' => db['line_items'].values.inject(0) { |sum, item| sum += item['line_price'] * item['quantity'] },
|
||||
'item_count' => db['line_items'].values.inject(0) { |sum, item| sum += item['quantity'] },
|
||||
'items' => db['line_items'].values,
|
||||
}
|
||||
|
||||
@@ -49,3 +41,8 @@ module Database
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if __FILE__ == $PROGRAM_NAME
|
||||
p(Database.tables['collections']['frontpage'].keys)
|
||||
# p Database.tables['blog']['articles']
|
||||
end
|
||||
|
||||
@@ -8,7 +8,7 @@ class Paginate < Liquid::Block
|
||||
|
||||
if markup =~ Syntax
|
||||
@collection_name = Regexp.last_match(1)
|
||||
@page_size = if Regexp.last_match(2)
|
||||
@page_size = if Regexp.last_match(2)
|
||||
Regexp.last_match(3).to_i
|
||||
else
|
||||
20
|
||||
@@ -45,8 +45,8 @@ class Paginate < Liquid::Block
|
||||
|
||||
pagination['items'] = collection_size
|
||||
pagination['pages'] = page_count - 1
|
||||
pagination['previous'] = link('« Previous', current_page - 1) if 1 < current_page
|
||||
pagination['next'] = link('Next »', current_page + 1) if page_count > current_page + 1
|
||||
pagination['previous'] = link('« Previous', current_page - 1) unless 1 >= current_page
|
||||
pagination['next'] = link('Next »', current_page + 1) unless page_count <= current_page + 1
|
||||
pagination['parts'] = []
|
||||
|
||||
hellip_break = false
|
||||
|
||||
@@ -31,7 +31,7 @@ module ShopFilter
|
||||
|
||||
def link_to_vendor(vendor)
|
||||
if vendor
|
||||
link_to(vendor, url_for_vendor(vendor), vendor)
|
||||
link_to vendor, url_for_vendor(vendor), vendor
|
||||
else
|
||||
'Unknown Vendor'
|
||||
end
|
||||
@@ -39,7 +39,7 @@ module ShopFilter
|
||||
|
||||
def link_to_type(type)
|
||||
if type
|
||||
link_to(type, url_for_type(type), type)
|
||||
link_to type, url_for_type(type), type
|
||||
else
|
||||
'Unknown Vendor'
|
||||
end
|
||||
@@ -60,7 +60,7 @@ module ShopFilter
|
||||
|
||||
case style
|
||||
when 'original'
|
||||
'/files/shops/random_number/' + url
|
||||
return '/files/shops/random_number/' + url
|
||||
when 'grande', 'large', 'medium', 'compact', 'small', 'thumb', 'icon'
|
||||
"/files/shops/random_number/products/#{Regexp.last_match(1)}_#{style}.#{Regexp.last_match(2)}"
|
||||
else
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# This profiler run simulates Shopify.
|
||||
# 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.
|
||||
# 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
|
||||
# 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.
|
||||
@@ -58,9 +58,9 @@ class ThemeRunner
|
||||
# `render` is called to benchmark just the render portion of liquid
|
||||
def render
|
||||
@compiled_tests.each do |test|
|
||||
tmpl = test[:tmpl]
|
||||
tmpl = test[:tmpl]
|
||||
assigns = test[:assigns]
|
||||
layout = test[:layout]
|
||||
layout = test[:layout]
|
||||
|
||||
if layout
|
||||
assigns['content_for_layout'] = tmpl.render!(assigns)
|
||||
@@ -73,14 +73,10 @@ class ThemeRunner
|
||||
|
||||
private
|
||||
|
||||
def render_layout(template, layout, assigns)
|
||||
assigns['content_for_layout'] = template.render!(assigns)
|
||||
layout&.render!(assigns)
|
||||
end
|
||||
|
||||
def compile_and_render(template, layout, assigns, page_template, template_file)
|
||||
compiled_test = compile_test(template, layout, assigns, page_template, template_file)
|
||||
render_layout(compiled_test[:tmpl], compiled_test[:layout], compiled_test[:assigns])
|
||||
assigns['content_for_layout'] = compiled_test[:tmpl].render!(assigns)
|
||||
compiled_test[:layout].render!(assigns) if layout
|
||||
end
|
||||
|
||||
def compile_all_tests
|
||||
@@ -92,7 +88,7 @@ class ThemeRunner
|
||||
end
|
||||
|
||||
def compile_test(template, layout, assigns, page_template, template_file)
|
||||
tmpl = init_template(page_template, template_file)
|
||||
tmpl = init_template(page_template, template_file)
|
||||
parsed_template = tmpl.parse(template).dup
|
||||
|
||||
if layout
|
||||
@@ -117,9 +113,9 @@ class ThemeRunner
|
||||
|
||||
# set up a new Liquid::Template object for use in `compile_and_render` and `compile_test`
|
||||
def init_template(page_template, template_file)
|
||||
tmpl = Liquid::Template.new
|
||||
tmpl.assigns['page_title'] = 'Page title'
|
||||
tmpl.assigns['template'] = page_template
|
||||
tmpl = Liquid::Template.new
|
||||
tmpl.assigns['page_title'] = 'Page title'
|
||||
tmpl.assigns['template'] = page_template
|
||||
tmpl.registers[:file_system] = ThemeRunner::FileSystem.new(File.dirname(template_file))
|
||||
tmpl
|
||||
end
|
||||
|
||||
@@ -6,112 +6,45 @@ class AssignTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def test_assign_with_hyphen_in_variable_name
|
||||
template_source = <<~END_TEMPLATE
|
||||
{% assign this-thing = 'Print this-thing' -%}
|
||||
{{ this-thing -}}
|
||||
template_source = <<-END_TEMPLATE
|
||||
{% assign this-thing = 'Print this-thing' %}
|
||||
{{ this-thing }}
|
||||
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
|
||||
|
||||
def test_assigned_variable
|
||||
assert_template_result(
|
||||
'.foo.',
|
||||
assert_template_result('.foo.',
|
||||
'{% assign foo = values %}.{{ foo[0] }}.',
|
||||
{ 'values' => %w(foo bar baz) },
|
||||
)
|
||||
'values' => %w(foo bar baz))
|
||||
|
||||
assert_template_result(
|
||||
'.bar.',
|
||||
assert_template_result('.bar.',
|
||||
'{% assign foo = values %}.{{ foo[1] }}.',
|
||||
{ 'values' => %w(foo bar baz) },
|
||||
)
|
||||
'values' => %w(foo bar baz))
|
||||
end
|
||||
|
||||
def test_assign_with_filter
|
||||
assert_template_result(
|
||||
'.bar.',
|
||||
assert_template_result('.bar.',
|
||||
'{% assign foo = values | split: "," %}.{{ foo[1] }}.',
|
||||
{ 'values' => "foo,bar,baz" },
|
||||
)
|
||||
'values' => "foo,bar,baz")
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def test_assign_uses_error_mode
|
||||
assert_match_syntax_error(
|
||||
"Expected dotdot but found pipe in ",
|
||||
"{% assign foo = ('X' | downcase) %}",
|
||||
error_mode: :strict,
|
||||
)
|
||||
assert_template_result("", "{% assign foo = ('X' | downcase) %}", error_mode: :lax)
|
||||
end
|
||||
|
||||
def test_expression_with_whitespace_in_square_brackets
|
||||
source = "{% assign r = a[ 'b' ] %}{{ r }}"
|
||||
assert_template_result('result', source, { 'a' => { 'b' => 'result' } })
|
||||
end
|
||||
|
||||
def test_assign_score_exceeding_resource_limit
|
||||
t = Template.parse("{% assign foo = 42 %}{% assign bar = 23 %}")
|
||||
t.resource_limits.assign_score_limit = 1
|
||||
assert_equal("Liquid error: Memory limits exceeded", t.render)
|
||||
assert(t.resource_limits.reached?)
|
||||
|
||||
t.resource_limits.assign_score_limit = 2
|
||||
assert_equal("", t.render!)
|
||||
refute_nil(t.resource_limits.assign_score)
|
||||
end
|
||||
|
||||
def test_assign_score_exceeding_limit_from_composite_object
|
||||
t = Template.parse("{% assign foo = 'aaaa' | reverse %}")
|
||||
|
||||
t.resource_limits.assign_score_limit = 3
|
||||
assert_equal("Liquid error: Memory limits exceeded", t.render)
|
||||
assert(t.resource_limits.reached?)
|
||||
|
||||
t.resource_limits.assign_score_limit = 5
|
||||
assert_equal("", t.render!)
|
||||
end
|
||||
|
||||
def test_assign_score_of_int
|
||||
assert_equal(1, assign_score_of(123))
|
||||
end
|
||||
|
||||
def test_assign_score_of_string_counts_bytes
|
||||
assert_equal(3, assign_score_of('123'))
|
||||
assert_equal(5, assign_score_of('12345'))
|
||||
assert_equal(9, assign_score_of('すごい'))
|
||||
end
|
||||
|
||||
def test_assign_score_of_array
|
||||
assert_equal(1, assign_score_of([]))
|
||||
assert_equal(2, assign_score_of([123]))
|
||||
assert_equal(6, assign_score_of([123, 'abcd']))
|
||||
end
|
||||
|
||||
def test_assign_score_of_hash
|
||||
assert_equal(1, assign_score_of({}))
|
||||
assert_equal(5, assign_score_of('int' => 123))
|
||||
assert_equal(12, assign_score_of('int' => 123, 'str' => 'abcd'))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
class ObjectWrapperDrop < Liquid::Drop
|
||||
def initialize(obj)
|
||||
@obj = obj
|
||||
with_error_mode(:strict) do
|
||||
assert_raises(SyntaxError) do
|
||||
Template.parse("{% assign foo = ('X' | downcase) %}")
|
||||
end
|
||||
end
|
||||
|
||||
def value
|
||||
@obj
|
||||
with_error_mode(:lax) do
|
||||
assert Template.parse("{% assign foo = ('X' | downcase) %}")
|
||||
end
|
||||
end
|
||||
|
||||
def assign_score_of(obj)
|
||||
context = Liquid::Context.new('drop' => ObjectWrapperDrop.new(obj))
|
||||
Liquid::Template.parse('{% assign obj = drop.value %}').render!(context)
|
||||
context.resource_limits.assign_score
|
||||
end
|
||||
end
|
||||
end # AssignTest
|
||||
|
||||
@@ -9,6 +9,12 @@ class FoobarTag < Liquid::Tag
|
||||
end
|
||||
end
|
||||
|
||||
class BlankTestFileSystem
|
||||
def read_template_file(template_path)
|
||||
template_path
|
||||
end
|
||||
end
|
||||
|
||||
class BlankTest < Minitest::Test
|
||||
include Liquid
|
||||
N = 10
|
||||
@@ -27,7 +33,7 @@ class BlankTest < Minitest::Test
|
||||
|
||||
def test_new_tags_are_not_blank_by_default
|
||||
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
|
||||
|
||||
@@ -57,11 +63,9 @@ class BlankTest < Minitest::Test
|
||||
|
||||
def test_nested_blocks_are_blank_but_only_if_all_children_are
|
||||
assert_template_result("", wrap(wrap(" ")))
|
||||
assert_template_result(
|
||||
"\n but this is not " * (N + 1),
|
||||
assert_template_result("\n but this is not " * (N + 1),
|
||||
wrap('{% if true %} {% comment %} this is blank {% endcomment %} {% endif %}
|
||||
{% if true %} but this is not {% endif %}'),
|
||||
)
|
||||
{% if true %} but this is not {% endif %}'))
|
||||
end
|
||||
|
||||
def test_assigns_are_blank
|
||||
@@ -91,21 +95,10 @@ class BlankTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_include_is_blank
|
||||
assert_template_result(
|
||||
"foobar" * (N + 1),
|
||||
wrap("{% include 'foobar' %}"),
|
||||
partials: { 'foobar' => 'foobar' },
|
||||
)
|
||||
assert_template_result(
|
||||
" foobar " * (N + 1),
|
||||
wrap("{% include ' foobar ' %}"),
|
||||
partials: { ' foobar ' => ' foobar ' },
|
||||
)
|
||||
assert_template_result(
|
||||
" " * (N + 1),
|
||||
wrap(" {% include ' ' %} "),
|
||||
partials: { ' ' => ' ' },
|
||||
)
|
||||
Liquid::Template.file_system = BlankTestFileSystem.new
|
||||
assert_template_result "foobar" * (N + 1), wrap("{% include 'foobar' %}")
|
||||
assert_template_result " foobar " * (N + 1), wrap("{% include ' foobar ' %}")
|
||||
assert_template_result " " * (N + 1), wrap(" {% include ' ' %} ")
|
||||
end
|
||||
|
||||
def test_case_is_blank
|
||||
|
||||
@@ -6,51 +6,9 @@ class BlockTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def test_unexpected_end_tag
|
||||
source = '{% if true %}{% endunless %}'
|
||||
assert_match_syntax_error("Liquid syntax error (line 1): 'endunless' is not a valid delimiter for if tags. use endif", source)
|
||||
end
|
||||
|
||||
def test_with_custom_tag
|
||||
with_custom_tag('testtag', Block) do
|
||||
assert(Liquid::Template.parse("{% testtag %} {% endtesttag %}"))
|
||||
end
|
||||
end
|
||||
|
||||
def test_custom_block_tags_have_a_default_render_to_output_buffer_method_for_backwards_compatibility
|
||||
klass1 = Class.new(Block) do
|
||||
def render(*)
|
||||
'hello'
|
||||
end
|
||||
end
|
||||
|
||||
with_custom_tag('blabla', klass1) do
|
||||
template = Liquid::Template.parse("{% blabla %} bla {% endblabla %}")
|
||||
|
||||
assert_equal('hello', template.render)
|
||||
|
||||
buf = +''
|
||||
output = template.render({}, output: buf)
|
||||
assert_equal('hello', output)
|
||||
assert_equal('hello', buf)
|
||||
assert_equal(buf.object_id, output.object_id)
|
||||
end
|
||||
|
||||
klass2 = Class.new(klass1) do
|
||||
def render(*)
|
||||
'foo' + super + 'bar'
|
||||
end
|
||||
end
|
||||
|
||||
with_custom_tag('blabla', klass2) do
|
||||
template = Liquid::Template.parse("{% blabla %} foo {% endblabla %}")
|
||||
|
||||
assert_equal('foohellobar', template.render)
|
||||
|
||||
buf = +''
|
||||
output = template.render({}, output: buf)
|
||||
assert_equal('foohellobar', output)
|
||||
assert_equal('foohellobar', buf)
|
||||
assert_equal(buf.object_id, output.object_id)
|
||||
exc = assert_raises(SyntaxError) do
|
||||
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
|
||||
|
||||
@@ -10,43 +10,43 @@ class CaptureTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_capture_with_hyphen_in_variable_name
|
||||
template_source = <<~END_TEMPLATE
|
||||
{% capture this-thing %}Print this-thing{% endcapture -%}
|
||||
{{ this-thing -}}
|
||||
template_source = <<-END_TEMPLATE
|
||||
{% capture this-thing %}Print this-thing{% endcapture %}
|
||||
{{ this-thing }}
|
||||
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
|
||||
|
||||
def test_capture_to_variable_from_outer_scope_if_existing
|
||||
template_source = <<~END_TEMPLATE
|
||||
{% assign var = '' -%}
|
||||
{% if true -%}
|
||||
{% capture var %}first-block-string{% endcapture -%}
|
||||
{% endif -%}
|
||||
{% if true -%}
|
||||
{% capture var %}test-string{% endcapture -%}
|
||||
{% endif -%}
|
||||
{{var-}}
|
||||
template_source = <<-END_TEMPLATE
|
||||
{% assign var = '' %}
|
||||
{% if true %}
|
||||
{% capture var %}first-block-string{% endcapture %}
|
||||
{% endif %}
|
||||
{% if true %}
|
||||
{% capture var %}test-string{% endcapture %}
|
||||
{% endif %}
|
||||
{{var}}
|
||||
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
|
||||
|
||||
def test_assigning_from_capture
|
||||
template_source = <<~END_TEMPLATE
|
||||
{% assign first = '' -%}
|
||||
{% assign second = '' -%}
|
||||
{% for number in (1..3) -%}
|
||||
{% capture first %}{{number}}{% endcapture -%}
|
||||
{% assign second = first -%}
|
||||
{% endfor -%}
|
||||
{{ first }}-{{ second -}}
|
||||
template_source = <<-END_TEMPLATE
|
||||
{% assign first = '' %}
|
||||
{% assign second = '' %}
|
||||
{% for number in (1..3) %}
|
||||
{% capture first %}{{number}}{% endcapture %}
|
||||
{% assign second = first %}
|
||||
{% endfor %}
|
||||
{{ first }}-{{ second }}
|
||||
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
|
||||
|
||||
def test_increment_assign_score_by_bytes_not_characters
|
||||
t = Template.parse("{% capture foo %}すごい{% endcapture %}")
|
||||
t.render!
|
||||
assert_equal(9, t.resource_limits.assign_score)
|
||||
end
|
||||
end
|
||||
end # CaptureTest
|
||||
|
||||
@@ -2,598 +2,9 @@
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class HundredCentes
|
||||
def to_liquid
|
||||
100
|
||||
end
|
||||
end
|
||||
|
||||
class CentsDrop < Liquid::Drop
|
||||
def amount
|
||||
HundredCentes.new
|
||||
end
|
||||
|
||||
def non_zero?
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
class ContextSensitiveDrop < Liquid::Drop
|
||||
def test
|
||||
@context['test']
|
||||
end
|
||||
end
|
||||
|
||||
class Category
|
||||
attr_accessor :name
|
||||
|
||||
def initialize(name)
|
||||
@name = name
|
||||
end
|
||||
|
||||
def to_liquid
|
||||
CategoryDrop.new(self)
|
||||
end
|
||||
end
|
||||
|
||||
class CategoryDrop < Liquid::Drop
|
||||
attr_accessor :category, :context
|
||||
|
||||
def initialize(category)
|
||||
@category = category
|
||||
end
|
||||
end
|
||||
|
||||
class CounterDrop < Liquid::Drop
|
||||
def count
|
||||
@count ||= 0
|
||||
@count += 1
|
||||
end
|
||||
end
|
||||
|
||||
class ArrayLike
|
||||
def fetch(index)
|
||||
end
|
||||
|
||||
def [](index)
|
||||
@counts ||= []
|
||||
@counts[index] ||= 0
|
||||
@counts[index] += 1
|
||||
end
|
||||
|
||||
def to_liquid
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
class ContextTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def setup
|
||||
@context = Liquid::Context.new
|
||||
end
|
||||
|
||||
def test_variables
|
||||
@context['string'] = 'string'
|
||||
assert_equal('string', @context['string'])
|
||||
|
||||
@context['num'] = 5
|
||||
assert_equal(5, @context['num'])
|
||||
|
||||
@context['time'] = Time.parse('2006-06-06 12:00:00')
|
||||
assert_equal(Time.parse('2006-06-06 12:00:00'), @context['time'])
|
||||
|
||||
@context['date'] = Date.today
|
||||
assert_equal(Date.today, @context['date'])
|
||||
|
||||
now = Time.now
|
||||
@context['datetime'] = now
|
||||
assert_equal(now, @context['datetime'])
|
||||
|
||||
@context['bool'] = true
|
||||
assert_equal(true, @context['bool'])
|
||||
|
||||
@context['bool'] = false
|
||||
assert_equal(false, @context['bool'])
|
||||
|
||||
@context['nil'] = nil
|
||||
assert_nil(@context['nil'])
|
||||
assert_nil(@context['nil'])
|
||||
end
|
||||
|
||||
def test_variables_not_existing
|
||||
assert_template_result("true", "{% if does_not_exist == nil %}true{% endif %}")
|
||||
end
|
||||
|
||||
def test_scoping
|
||||
@context.push
|
||||
@context.pop
|
||||
|
||||
assert_raises(Liquid::ContextError) do
|
||||
@context.pop
|
||||
end
|
||||
|
||||
assert_raises(Liquid::ContextError) do
|
||||
@context.push
|
||||
@context.pop
|
||||
@context.pop
|
||||
end
|
||||
end
|
||||
|
||||
def test_length_query
|
||||
assert_template_result(
|
||||
"true",
|
||||
"{% if numbers.size == 4 %}true{% endif %}",
|
||||
{ "numbers" => [1, 2, 3, 4] },
|
||||
)
|
||||
|
||||
assert_template_result(
|
||||
"true",
|
||||
"{% if numbers.size == 4 %}true{% endif %}",
|
||||
{ "numbers" => { 1 => 1, 2 => 2, 3 => 3, 4 => 4 } },
|
||||
)
|
||||
|
||||
assert_template_result(
|
||||
"true",
|
||||
"{% if numbers.size == 1000 %}true{% endif %}",
|
||||
{ "numbers" => { 1 => 1, 2 => 2, 3 => 3, 4 => 4, 'size' => 1000 } },
|
||||
)
|
||||
end
|
||||
|
||||
def test_hyphenated_variable
|
||||
assert_template_result("godz", "{{ oh-my }}", { "oh-my" => 'godz' })
|
||||
end
|
||||
|
||||
def test_add_filter
|
||||
filter = Module.new do
|
||||
def hi(output)
|
||||
output + ' hi!'
|
||||
end
|
||||
end
|
||||
|
||||
context = Context.new
|
||||
context.add_filters(filter)
|
||||
assert_equal('hi? hi!', context.invoke(:hi, 'hi?'))
|
||||
|
||||
context = Context.new
|
||||
assert_equal('hi?', context.invoke(:hi, 'hi?'))
|
||||
|
||||
context.add_filters(filter)
|
||||
assert_equal('hi? hi!', context.invoke(:hi, 'hi?'))
|
||||
end
|
||||
|
||||
def test_only_intended_filters_make_it_there
|
||||
filter = Module.new do
|
||||
def hi(output)
|
||||
output + ' hi!'
|
||||
end
|
||||
end
|
||||
|
||||
context = Context.new
|
||||
assert_equal("Wookie", context.invoke("hi", "Wookie"))
|
||||
|
||||
context.add_filters(filter)
|
||||
assert_equal("Wookie hi!", context.invoke("hi", "Wookie"))
|
||||
end
|
||||
|
||||
def test_add_item_in_outer_scope
|
||||
@context['test'] = 'test'
|
||||
@context.push
|
||||
assert_equal('test', @context['test'])
|
||||
@context.pop
|
||||
assert_equal('test', @context['test'])
|
||||
end
|
||||
|
||||
def test_add_item_in_inner_scope
|
||||
@context.push
|
||||
@context['test'] = 'test'
|
||||
assert_equal('test', @context['test'])
|
||||
@context.pop
|
||||
assert_nil(@context['test'])
|
||||
end
|
||||
|
||||
def test_hierachical_data
|
||||
assigns = { 'hash' => { "name" => 'tobi' } }
|
||||
assert_template_result("tobi", "{{ hash.name }}", assigns)
|
||||
assert_template_result("tobi", '{{ hash["name"] }}', assigns)
|
||||
end
|
||||
|
||||
def test_keywords
|
||||
assert_template_result("pass", "{% if true == expect %}pass{% endif %}", { "expect" => true })
|
||||
assert_template_result("pass", "{% if false == expect %}pass{% endif %}", { "expect" => false })
|
||||
end
|
||||
|
||||
def test_digits
|
||||
assert_template_result("pass", "{% if 100 == expect %}pass{% endif %}", { "expect" => 100 })
|
||||
assert_template_result("pass", "{% if 100.00 == expect %}pass{% endif %}", { "expect" => 100.00 })
|
||||
end
|
||||
|
||||
def test_strings
|
||||
assert_template_result("hello!", '{{ "hello!" }}')
|
||||
assert_template_result("hello!", "{{ 'hello!' }}")
|
||||
end
|
||||
|
||||
def test_merge
|
||||
@context.merge("test" => "test")
|
||||
assert_equal('test', @context['test'])
|
||||
@context.merge("test" => "newvalue", "foo" => "bar")
|
||||
assert_equal('newvalue', @context['test'])
|
||||
assert_equal('bar', @context['foo'])
|
||||
end
|
||||
|
||||
def test_array_notation
|
||||
assigns = { "test" => ["a", "b"] }
|
||||
assert_template_result("a", "{{ test[0] }}", assigns)
|
||||
assert_template_result("b", "{{ test[1] }}", assigns)
|
||||
assert_template_result("pass", "{% if test[2] == nil %}pass{% endif %}", assigns)
|
||||
end
|
||||
|
||||
def test_recoursive_array_notation
|
||||
assigns = { "test" => { 'test' => [1, 2, 3, 4, 5] } }
|
||||
assert_template_result("1", "{{ test.test[0] }}", assigns)
|
||||
|
||||
assigns = { "test" => [{ 'test' => 'worked' }] }
|
||||
assert_template_result("worked", "{{ test[0].test }}", assigns)
|
||||
end
|
||||
|
||||
def test_hash_to_array_transition
|
||||
assigns = {
|
||||
'colors' => {
|
||||
'Blue' => ['003366', '336699', '6699CC', '99CCFF'],
|
||||
'Green' => ['003300', '336633', '669966', '99CC99'],
|
||||
'Yellow' => ['CC9900', 'FFCC00', 'FFFF99', 'FFFFCC'],
|
||||
'Red' => ['660000', '993333', 'CC6666', 'FF9999'],
|
||||
},
|
||||
}
|
||||
|
||||
assert_template_result("003366", "{{ colors.Blue[0] }}", assigns)
|
||||
assert_template_result("FF9999", "{{ colors.Red[3] }}", assigns)
|
||||
end
|
||||
|
||||
def test_try_first
|
||||
assigns = { '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_template_result("1", "{{ test.test.first }}", assigns)
|
||||
assert_template_result("5", "{{ test.test.last }}", assigns)
|
||||
|
||||
assigns = { "test" => [1] }
|
||||
assert_template_result("1", "{{ test.first }}", assigns)
|
||||
assert_template_result("1", "{{ test.last }}", assigns)
|
||||
end
|
||||
|
||||
def test_access_hashes_with_hash_notation
|
||||
assigns = { 'products' => { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] } }
|
||||
assert_template_result("5", '{{ products["count"] }}', assigns)
|
||||
assert_template_result("deepsnow", '{{ products["tags"][0] }}', assigns)
|
||||
assert_template_result("deepsnow", '{{ products["tags"].first }}', assigns)
|
||||
|
||||
assigns = { 'product' => { 'variants' => [{ 'title' => 'draft151cm' }, { 'title' => 'element151cm' }] } }
|
||||
assert_template_result("draft151cm", '{{ product["variants"][0]["title"] }}', assigns)
|
||||
assert_template_result("element151cm", '{{ product["variants"][1]["title"] }}', assigns)
|
||||
assert_template_result("draft151cm", '{{ product["variants"].first["title"] }}', assigns)
|
||||
assert_template_result("element151cm", '{{ product["variants"].last["title"] }}', assigns)
|
||||
end
|
||||
|
||||
def test_access_variable_with_hash_notation
|
||||
assert_template_result('baz', '{{ ["foo"] }}', { "foo" => "baz" })
|
||||
assert_template_result('baz', '{{ [bar] }}', { 'foo' => 'baz', 'bar' => 'foo' })
|
||||
end
|
||||
|
||||
def test_access_hashes_with_hash_access_variables
|
||||
assigns = {
|
||||
'var' => 'tags',
|
||||
'nested' => { 'var' => 'tags' },
|
||||
'products' => { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] },
|
||||
}
|
||||
|
||||
assert_template_result('deepsnow', '{{ products[var].first }}', assigns)
|
||||
assert_template_result('freestyle', '{{ products[nested.var].last }}', assigns)
|
||||
end
|
||||
|
||||
def test_hash_notation_only_for_hash_access
|
||||
assigns = { "array" => [1, 2, 3, 4, 5] }
|
||||
assert_template_result("1", "{{ array.first }}", assigns)
|
||||
assert_template_result("pass", '{% if array["first"] == nil %}pass{% endif %}', assigns)
|
||||
|
||||
assert_template_result("Hello", '{{ hash["first"] }}', { "hash" => { "first" => "Hello" } })
|
||||
end
|
||||
|
||||
def test_first_can_appear_in_middle_of_callchain
|
||||
assigns = { "product" => { 'variants' => [{ 'title' => 'draft151cm' }, { 'title' => 'element151cm' }] } }
|
||||
|
||||
assert_template_result('draft151cm', '{{ product.variants[0].title }}', assigns)
|
||||
assert_template_result('element151cm', '{{ product.variants[1].title }}', assigns)
|
||||
assert_template_result('draft151cm', '{{ product.variants.first.title }}', assigns)
|
||||
assert_template_result('element151cm', '{{ product.variants.last.title }}', assigns)
|
||||
end
|
||||
|
||||
def test_cents
|
||||
@context.merge("cents" => HundredCentes.new)
|
||||
assert_equal(100, @context['cents'])
|
||||
end
|
||||
|
||||
def test_nested_cents
|
||||
@context.merge("cents" => { 'amount' => HundredCentes.new })
|
||||
assert_equal(100, @context['cents.amount'])
|
||||
|
||||
@context.merge("cents" => { 'cents' => { 'amount' => HundredCentes.new } })
|
||||
assert_equal(100, @context['cents.cents.amount'])
|
||||
end
|
||||
|
||||
def test_cents_through_drop
|
||||
@context.merge("cents" => CentsDrop.new)
|
||||
assert_equal(100, @context['cents.amount'])
|
||||
end
|
||||
|
||||
def test_nested_cents_through_drop
|
||||
@context.merge("vars" => { "cents" => CentsDrop.new })
|
||||
assert_equal(100, @context['vars.cents.amount'])
|
||||
end
|
||||
|
||||
def test_drop_methods_with_question_marks
|
||||
@context.merge("cents" => CentsDrop.new)
|
||||
assert(@context['cents.non_zero?'])
|
||||
end
|
||||
|
||||
def test_context_from_within_drop
|
||||
@context.merge("test" => '123', "vars" => ContextSensitiveDrop.new)
|
||||
assert_equal('123', @context['vars.test'])
|
||||
end
|
||||
|
||||
def test_nested_context_from_within_drop
|
||||
@context.merge("test" => '123', "vars" => { "local" => ContextSensitiveDrop.new })
|
||||
assert_equal('123', @context['vars.local.test'])
|
||||
end
|
||||
|
||||
def test_ranges
|
||||
assert_template_result("1..5", '{{ (1..5) }}')
|
||||
assert_template_result("pass", '{% if (1..5) == expect %}pass{% endif %}', { "expect" => (1..5) })
|
||||
|
||||
assigns = { "test" => '5' }
|
||||
assert_template_result("1..5", "{{ (1..test) }}", assigns)
|
||||
assert_template_result("5..5", "{{ (test..test) }}", assigns)
|
||||
end
|
||||
|
||||
def test_cents_through_drop_nestedly
|
||||
@context.merge("cents" => { "cents" => CentsDrop.new })
|
||||
assert_equal(100, @context['cents.cents.amount'])
|
||||
|
||||
@context.merge("cents" => { "cents" => { "cents" => CentsDrop.new } })
|
||||
assert_equal(100, @context['cents.cents.cents.amount'])
|
||||
end
|
||||
|
||||
def test_drop_with_variable_called_only_once
|
||||
@context['counter'] = CounterDrop.new
|
||||
|
||||
assert_equal(1, @context['counter.count'])
|
||||
assert_equal(2, @context['counter.count'])
|
||||
assert_equal(3, @context['counter.count'])
|
||||
end
|
||||
|
||||
def test_drop_with_key_called_only_once
|
||||
@context['counter'] = CounterDrop.new
|
||||
|
||||
assert_equal(1, @context['counter["count"]'])
|
||||
assert_equal(2, @context['counter["count"]'])
|
||||
assert_equal(3, @context['counter["count"]'])
|
||||
end
|
||||
|
||||
def test_proc_as_variable
|
||||
@context['dynamic'] = proc { 'Hello' }
|
||||
|
||||
assert_equal('Hello', @context['dynamic'])
|
||||
end
|
||||
|
||||
def test_lambda_as_variable
|
||||
@context['dynamic'] = proc { 'Hello' }
|
||||
|
||||
assert_equal('Hello', @context['dynamic'])
|
||||
end
|
||||
|
||||
def test_nested_lambda_as_variable
|
||||
@context['dynamic'] = { "lambda" => proc { 'Hello' } }
|
||||
|
||||
assert_equal('Hello', @context['dynamic.lambda'])
|
||||
end
|
||||
|
||||
def test_array_containing_lambda_as_variable
|
||||
@context['dynamic'] = [1, 2, proc { 'Hello' }, 4, 5]
|
||||
|
||||
assert_equal('Hello', @context['dynamic[2]'])
|
||||
end
|
||||
|
||||
def test_lambda_is_called_once
|
||||
@global = 0
|
||||
|
||||
@context['callcount'] = proc {
|
||||
@global += 1
|
||||
@global.to_s
|
||||
}
|
||||
|
||||
assert_equal('1', @context['callcount'])
|
||||
assert_equal('1', @context['callcount'])
|
||||
assert_equal('1', @context['callcount'])
|
||||
end
|
||||
|
||||
def test_nested_lambda_is_called_once
|
||||
@global = 0
|
||||
|
||||
@context['callcount'] = {
|
||||
"lambda" => proc {
|
||||
@global += 1
|
||||
@global.to_s
|
||||
},
|
||||
}
|
||||
|
||||
assert_equal('1', @context['callcount.lambda'])
|
||||
assert_equal('1', @context['callcount.lambda'])
|
||||
assert_equal('1', @context['callcount.lambda'])
|
||||
end
|
||||
|
||||
def test_lambda_in_array_is_called_once
|
||||
@global = 0
|
||||
|
||||
p = proc {
|
||||
@global += 1
|
||||
@global.to_s
|
||||
}
|
||||
@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]'])
|
||||
end
|
||||
|
||||
def test_access_to_context_from_proc
|
||||
@context.registers[:magic] = 345392
|
||||
|
||||
@context['magic'] = proc { @context.registers[:magic] }
|
||||
|
||||
assert_equal(345392, @context['magic'])
|
||||
end
|
||||
|
||||
def test_to_liquid_and_context_at_first_level
|
||||
@context['category'] = Category.new("foobar")
|
||||
assert_kind_of(CategoryDrop, @context['category'])
|
||||
assert_equal(@context, @context['category'].context)
|
||||
end
|
||||
|
||||
def test_interrupt_avoids_object_allocations
|
||||
@context.interrupt? # ruby 3.0.0 allocates on the first call
|
||||
assert_no_object_allocations do
|
||||
@context.interrupt?
|
||||
end
|
||||
end
|
||||
|
||||
def test_context_initialization_with_a_proc_in_environment
|
||||
contx = Context.new([test: ->(c) { c['poutine'] }], test: :foo)
|
||||
|
||||
assert(contx)
|
||||
assert_nil(contx['poutine'])
|
||||
end
|
||||
|
||||
def test_apply_global_filter
|
||||
global_filter_proc = ->(output) { "#{output} filtered" }
|
||||
|
||||
context = Context.new
|
||||
context.global_filter = global_filter_proc
|
||||
|
||||
assert_equal('hi filtered', context.apply_global_filter('hi'))
|
||||
end
|
||||
|
||||
def test_static_environments_are_read_with_lower_priority_than_environments
|
||||
context = Context.build(
|
||||
static_environments: { 'shadowed' => 'static', 'unshadowed' => 'static' },
|
||||
environments: { 'shadowed' => 'dynamic' },
|
||||
)
|
||||
|
||||
assert_equal('dynamic', context['shadowed'])
|
||||
assert_equal('static', context['unshadowed'])
|
||||
end
|
||||
|
||||
def test_apply_global_filter_when_no_global_filter_exist
|
||||
context = Context.new
|
||||
assert_equal('hi', context.apply_global_filter('hi'))
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_does_not_inherit_variables
|
||||
super_context = Context.new
|
||||
super_context['my_variable'] = 'some value'
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
|
||||
assert_nil(subcontext['my_variable'])
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_inherits_static_environment
|
||||
super_context = Context.build(static_environments: { 'my_environment_value' => 'my value' })
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
|
||||
assert_equal('my value', subcontext['my_environment_value'])
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_inherits_resource_limits
|
||||
resource_limits = ResourceLimits.new({})
|
||||
super_context = Context.new({}, {}, {}, false, resource_limits)
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
assert_equal(resource_limits, subcontext.resource_limits)
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_inherits_exception_renderer
|
||||
super_context = Context.new
|
||||
super_context.exception_renderer = ->(_e) { 'my exception message' }
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
assert_equal('my exception message', subcontext.handle_error(Liquid::Error.new))
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_does_not_inherit_non_static_registers
|
||||
registers = {
|
||||
my_register: :my_value,
|
||||
}
|
||||
super_context = Context.new({}, {}, Registers.new(registers))
|
||||
super_context.registers[:my_register] = :my_alt_value
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
assert_equal(:my_value, subcontext.registers[:my_register])
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_inherits_static_registers
|
||||
super_context = Context.build(registers: { my_register: :my_value })
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
assert_equal(:my_value, subcontext.registers[:my_register])
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_registers_do_not_pollute_context
|
||||
super_context = Context.build(registers: { my_register: :my_value })
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
subcontext.registers[:my_register] = :my_alt_value
|
||||
assert_equal(:my_value, super_context.registers[:my_register])
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_inherits_filters
|
||||
my_filter = Module.new do
|
||||
def my_filter(*)
|
||||
'my filter result'
|
||||
end
|
||||
end
|
||||
|
||||
super_context = Context.new
|
||||
super_context.add_filters([my_filter])
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
template = Template.parse('{{ 123 | my_filter }}')
|
||||
assert_equal('my filter result', template.render(subcontext))
|
||||
end
|
||||
|
||||
def test_disables_tag_specified
|
||||
context = Context.new
|
||||
context.with_disabled_tags(%w(foo bar)) do
|
||||
assert_equal(true, context.tag_disabled?("foo"))
|
||||
assert_equal(true, context.tag_disabled?("bar"))
|
||||
assert_equal(false, context.tag_disabled?("unknown"))
|
||||
end
|
||||
end
|
||||
|
||||
def test_disables_nested_tags
|
||||
context = Context.new
|
||||
context.with_disabled_tags(["foo"]) do
|
||||
context.with_disabled_tags(["foo"]) do
|
||||
assert_equal(true, context.tag_disabled?("foo"))
|
||||
assert_equal(false, context.tag_disabled?("bar"))
|
||||
end
|
||||
context.with_disabled_tags(["bar"]) do
|
||||
assert_equal(true, context.tag_disabled?("foo"))
|
||||
assert_equal(true, context.tag_disabled?("bar"))
|
||||
context.with_disabled_tags(["foo"]) do
|
||||
assert_equal(true, context.tag_disabled?("foo"))
|
||||
assert_equal(true, context.tag_disabled?("bar"))
|
||||
end
|
||||
end
|
||||
assert_equal(true, context.tag_disabled?("foo"))
|
||||
assert_equal(false, context.tag_disabled?("bar"))
|
||||
end
|
||||
end
|
||||
|
||||
def test_override_global_filter
|
||||
global = Module.new do
|
||||
def notice(output)
|
||||
@@ -608,44 +19,16 @@ class ContextTest < Minitest::Test
|
||||
end
|
||||
|
||||
with_global_filter(global) do
|
||||
assert_equal('Global test', Template.parse("{{'test' | notice }}").render!)
|
||||
assert_equal('Local test', Template.parse("{{'test' | notice }}").render!({}, filters: [local]))
|
||||
assert_equal 'Global test', Template.parse("{{'test' | notice }}").render!
|
||||
assert_equal 'Local test', Template.parse("{{'test' | notice }}").render!({}, filters: [local])
|
||||
end
|
||||
end
|
||||
|
||||
def test_has_key_will_not_add_an_error_for_missing_keys
|
||||
with_error_mode(:strict) do
|
||||
with_error_mode :strict do
|
||||
context = Context.new
|
||||
context.key?('unknown')
|
||||
assert_empty(context.errors)
|
||||
assert_empty context.errors
|
||||
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
|
||||
|
||||
def assert_no_object_allocations
|
||||
unless RUBY_ENGINE == 'ruby'
|
||||
skip("stackprof needed to count object allocations")
|
||||
end
|
||||
require 'stackprof'
|
||||
|
||||
profile = StackProf.run(mode: :object) do
|
||||
yield
|
||||
end
|
||||
assert_equal(0, profile[:samples])
|
||||
end
|
||||
end # ContextTest
|
||||
end
|
||||
|
||||
@@ -6,12 +6,16 @@ class DocumentTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def test_unexpected_outer_tag
|
||||
source = "{% else %}"
|
||||
assert_match_syntax_error("Liquid syntax error (line 1): Unexpected outer 'else' tag", source)
|
||||
exc = assert_raises(SyntaxError) do
|
||||
Template.parse("{% else %}")
|
||||
end
|
||||
assert_equal exc.message, "Liquid syntax error: Unexpected outer 'else' tag"
|
||||
end
|
||||
|
||||
def test_unknown_tag
|
||||
source = "{% foo %}"
|
||||
assert_match_syntax_error("Liquid syntax error (line 1): Unknown tag 'foo'", source)
|
||||
exc = assert_raises(SyntaxError) do
|
||||
Template.parse("{% foo %}")
|
||||
end
|
||||
assert_equal exc.message, "Liquid syntax error: Unknown tag 'foo'"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -49,6 +49,10 @@ class ProductDrop < Liquid::Drop
|
||||
ContextDrop.new
|
||||
end
|
||||
|
||||
def user_input
|
||||
(+"foo").taint
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def callmenot
|
||||
@@ -107,151 +111,172 @@ class DropsTest < Minitest::Test
|
||||
|
||||
def test_product_drop
|
||||
tpl = Liquid::Template.parse(' ')
|
||||
assert_equal(' ', tpl.render!('product' => ProductDrop.new))
|
||||
assert_equal ' ', tpl.render!('product' => ProductDrop.new)
|
||||
end
|
||||
|
||||
def test_rendering_raises_on_tainted_attr
|
||||
with_taint_mode(:error) do
|
||||
tpl = Liquid::Template.parse('{{ product.user_input }}')
|
||||
assert_raises TaintedError do
|
||||
tpl.render!('product' => ProductDrop.new)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_rendering_warns_on_tainted_attr
|
||||
with_taint_mode(:warn) do
|
||||
tpl = Liquid::Template.parse('{{ product.user_input }}')
|
||||
context = Context.new('product' => ProductDrop.new)
|
||||
tpl.render!(context)
|
||||
assert_equal [Liquid::TaintedError], context.warnings.map(&:class)
|
||||
assert_equal "variable 'product.user_input' is tainted and was not escaped", context.warnings.first.to_s(false)
|
||||
end
|
||||
end
|
||||
|
||||
def test_rendering_doesnt_raise_on_escaped_tainted_attr
|
||||
with_taint_mode(:error) do
|
||||
tpl = Liquid::Template.parse('{{ product.user_input | escape }}')
|
||||
tpl.render!('product' => ProductDrop.new)
|
||||
end
|
||||
end
|
||||
|
||||
def test_drop_does_only_respond_to_whitelisted_methods
|
||||
assert_equal("", Liquid::Template.parse("{{ product.inspect }}").render!('product' => ProductDrop.new))
|
||||
assert_equal("", Liquid::Template.parse("{{ product.pretty_inspect }}").render!('product' => ProductDrop.new))
|
||||
assert_equal("", Liquid::Template.parse("{{ product.whatever }}").render!('product' => ProductDrop.new))
|
||||
assert_equal("", Liquid::Template.parse('{{ product | map: "inspect" }}').render!('product' => ProductDrop.new))
|
||||
assert_equal("", Liquid::Template.parse('{{ product | map: "pretty_inspect" }}').render!('product' => ProductDrop.new))
|
||||
assert_equal("", Liquid::Template.parse('{{ product | map: "whatever" }}').render!('product' => ProductDrop.new))
|
||||
assert_equal "", Liquid::Template.parse("{{ product.inspect }}").render!('product' => ProductDrop.new)
|
||||
assert_equal "", Liquid::Template.parse("{{ product.pretty_inspect }}").render!('product' => ProductDrop.new)
|
||||
assert_equal "", Liquid::Template.parse("{{ product.whatever }}").render!('product' => ProductDrop.new)
|
||||
assert_equal "", Liquid::Template.parse('{{ product | map: "inspect" }}').render!('product' => ProductDrop.new)
|
||||
assert_equal "", Liquid::Template.parse('{{ product | map: "pretty_inspect" }}').render!('product' => ProductDrop.new)
|
||||
assert_equal "", Liquid::Template.parse('{{ product | map: "whatever" }}').render!('product' => ProductDrop.new)
|
||||
end
|
||||
|
||||
def test_drops_respond_to_to_liquid
|
||||
assert_equal("text1", Liquid::Template.parse("{{ product.to_liquid.texts.text }}").render!('product' => ProductDrop.new))
|
||||
assert_equal("text1", Liquid::Template.parse('{{ product | map: "to_liquid" | map: "texts" | map: "text" }}').render!('product' => ProductDrop.new))
|
||||
assert_equal "text1", Liquid::Template.parse("{{ product.to_liquid.texts.text }}").render!('product' => ProductDrop.new)
|
||||
assert_equal "text1", Liquid::Template.parse('{{ product | map: "to_liquid" | map: "texts" | map: "text" }}').render!('product' => ProductDrop.new)
|
||||
end
|
||||
|
||||
def test_text_drop
|
||||
output = Liquid::Template.parse(' {{ product.texts.text }} ').render!('product' => ProductDrop.new)
|
||||
assert_equal(' text1 ', output)
|
||||
assert_equal ' text1 ', output
|
||||
end
|
||||
|
||||
def test_catchall_unknown_method
|
||||
output = Liquid::Template.parse(' {{ product.catchall.unknown }} ').render!('product' => ProductDrop.new)
|
||||
assert_equal(' catchall_method: unknown ', output)
|
||||
assert_equal ' catchall_method: unknown ', output
|
||||
end
|
||||
|
||||
def test_catchall_integer_argument_drop
|
||||
output = Liquid::Template.parse(' {{ product.catchall[8] }} ').render!('product' => ProductDrop.new)
|
||||
assert_equal(' catchall_method: 8 ', output)
|
||||
assert_equal ' catchall_method: 8 ', output
|
||||
end
|
||||
|
||||
def test_text_array_drop
|
||||
output = Liquid::Template.parse('{% for text in product.texts.array %} {{text}} {% endfor %}').render!('product' => ProductDrop.new)
|
||||
assert_equal(' text1 text2 ', output)
|
||||
assert_equal ' text1 text2 ', output
|
||||
end
|
||||
|
||||
def test_context_drop
|
||||
output = Liquid::Template.parse(' {{ context.bar }} ').render!('context' => ContextDrop.new, 'bar' => "carrot")
|
||||
assert_equal(' carrot ', output)
|
||||
end
|
||||
|
||||
def test_context_drop_array_with_map
|
||||
output = Liquid::Template.parse(' {{ contexts | map: "bar" }} ').render!('contexts' => [ContextDrop.new, ContextDrop.new], 'bar' => "carrot")
|
||||
assert_equal(' carrotcarrot ', output)
|
||||
assert_equal ' carrot ', output
|
||||
end
|
||||
|
||||
def test_nested_context_drop
|
||||
output = Liquid::Template.parse(' {{ product.context.foo }} ').render!('product' => ProductDrop.new, 'foo' => "monkey")
|
||||
assert_equal(' monkey ', output)
|
||||
assert_equal ' monkey ', output
|
||||
end
|
||||
|
||||
def test_protected
|
||||
output = Liquid::Template.parse(' {{ product.callmenot }} ').render!('product' => ProductDrop.new)
|
||||
assert_equal(' ', output)
|
||||
assert_equal ' ', output
|
||||
end
|
||||
|
||||
def test_object_methods_not_allowed
|
||||
[:dup, :clone, :singleton_class, :eval, :class_eval, :inspect].each do |method|
|
||||
output = Liquid::Template.parse(" {{ product.#{method} }} ").render!('product' => ProductDrop.new)
|
||||
assert_equal(' ', output)
|
||||
assert_equal ' ', output
|
||||
end
|
||||
end
|
||||
|
||||
def test_scope
|
||||
assert_equal('1', Liquid::Template.parse('{{ context.scopes }}').render!('context' => ContextDrop.new))
|
||||
assert_equal('2', Liquid::Template.parse('{%for i in dummy%}{{ context.scopes }}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1]))
|
||||
assert_equal('3', Liquid::Template.parse('{%for i in dummy%}{%for i in dummy%}{{ context.scopes }}{%endfor%}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1]))
|
||||
assert_equal '1', Liquid::Template.parse('{{ context.scopes }}').render!('context' => ContextDrop.new)
|
||||
assert_equal '2', Liquid::Template.parse('{%for i in dummy%}{{ context.scopes }}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1])
|
||||
assert_equal '3', Liquid::Template.parse('{%for i in dummy%}{%for i in dummy%}{{ context.scopes }}{%endfor%}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1])
|
||||
end
|
||||
|
||||
def test_scope_though_proc
|
||||
assert_equal('1', Liquid::Template.parse('{{ s }}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] }))
|
||||
assert_equal('2', Liquid::Template.parse('{%for i in dummy%}{{ s }}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] }, 'dummy' => [1]))
|
||||
assert_equal('3', Liquid::Template.parse('{%for i in dummy%}{%for i in dummy%}{{ s }}{%endfor%}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] }, 'dummy' => [1]))
|
||||
assert_equal '1', Liquid::Template.parse('{{ s }}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] })
|
||||
assert_equal '2', Liquid::Template.parse('{%for i in dummy%}{{ s }}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] }, 'dummy' => [1])
|
||||
assert_equal '3', Liquid::Template.parse('{%for i in dummy%}{%for i in dummy%}{{ s }}{%endfor%}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] }, 'dummy' => [1])
|
||||
end
|
||||
|
||||
def test_scope_with_assigns
|
||||
assert_equal('variable', Liquid::Template.parse('{% assign a = "variable"%}{{a}}').render!('context' => ContextDrop.new))
|
||||
assert_equal('variable', Liquid::Template.parse('{% assign a = "variable"%}{%for i in dummy%}{{a}}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1]))
|
||||
assert_equal('test', Liquid::Template.parse('{% assign header_gif = "test"%}{{header_gif}}').render!('context' => ContextDrop.new))
|
||||
assert_equal('test', Liquid::Template.parse("{% assign header_gif = 'test'%}{{header_gif}}").render!('context' => ContextDrop.new))
|
||||
assert_equal 'variable', Liquid::Template.parse('{% assign a = "variable"%}{{a}}').render!('context' => ContextDrop.new)
|
||||
assert_equal 'variable', Liquid::Template.parse('{% assign a = "variable"%}{%for i in dummy%}{{a}}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1])
|
||||
assert_equal 'test', Liquid::Template.parse('{% assign header_gif = "test"%}{{header_gif}}').render!('context' => ContextDrop.new)
|
||||
assert_equal 'test', Liquid::Template.parse("{% assign header_gif = 'test'%}{{header_gif}}").render!('context' => ContextDrop.new)
|
||||
end
|
||||
|
||||
def test_scope_from_tags
|
||||
assert_equal('1', Liquid::Template.parse('{% for i in context.scopes_as_array %}{{i}}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1]))
|
||||
assert_equal('12', Liquid::Template.parse('{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1]))
|
||||
assert_equal('123', Liquid::Template.parse('{%for a in dummy%}{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1]))
|
||||
assert_equal '1', Liquid::Template.parse('{% for i in context.scopes_as_array %}{{i}}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1])
|
||||
assert_equal '12', Liquid::Template.parse('{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1])
|
||||
assert_equal '123', Liquid::Template.parse('{%for a in dummy%}{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1])
|
||||
end
|
||||
|
||||
def test_access_context_from_drop
|
||||
assert_equal('123', Liquid::Template.parse('{%for a in dummy%}{{ context.loop_pos }}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1, 2, 3]))
|
||||
assert_equal '123', Liquid::Template.parse('{%for a in dummy%}{{ context.loop_pos }}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1, 2, 3])
|
||||
end
|
||||
|
||||
def test_enumerable_drop
|
||||
assert_equal('123', Liquid::Template.parse('{% for c in collection %}{{c}}{% endfor %}').render!('collection' => EnumerableDrop.new))
|
||||
assert_equal '123', Liquid::Template.parse('{% for c in collection %}{{c}}{% endfor %}').render!('collection' => EnumerableDrop.new)
|
||||
end
|
||||
|
||||
def test_enumerable_drop_size
|
||||
assert_equal('3', Liquid::Template.parse('{{collection.size}}').render!('collection' => EnumerableDrop.new))
|
||||
assert_equal '3', Liquid::Template.parse('{{collection.size}}').render!('collection' => EnumerableDrop.new)
|
||||
end
|
||||
|
||||
def test_enumerable_drop_will_invoke_liquid_method_missing_for_clashing_method_names
|
||||
["select", "each", "map", "cycle"].each do |method|
|
||||
assert_equal(method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new))
|
||||
assert_equal(method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new))
|
||||
assert_equal(method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new))
|
||||
assert_equal(method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new))
|
||||
assert_equal method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new)
|
||||
assert_equal method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new)
|
||||
assert_equal method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new)
|
||||
assert_equal method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new)
|
||||
end
|
||||
end
|
||||
|
||||
def test_some_enumerable_methods_still_get_invoked
|
||||
[:count, :max].each do |method|
|
||||
assert_equal("3", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new))
|
||||
assert_equal("3", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new))
|
||||
assert_equal("3", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new))
|
||||
assert_equal("3", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new))
|
||||
assert_equal "3", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new)
|
||||
assert_equal "3", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new)
|
||||
assert_equal "3", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new)
|
||||
assert_equal "3", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new)
|
||||
end
|
||||
|
||||
assert_equal("yes", Liquid::Template.parse("{% if collection contains 3 %}yes{% endif %}").render!('collection' => RealEnumerableDrop.new))
|
||||
assert_equal "yes", Liquid::Template.parse("{% if collection contains 3 %}yes{% endif %}").render!('collection' => RealEnumerableDrop.new)
|
||||
|
||||
[:min, :first].each do |method|
|
||||
assert_equal("1", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new))
|
||||
assert_equal("1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new))
|
||||
assert_equal("1", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new))
|
||||
assert_equal("1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new))
|
||||
assert_equal "1", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new)
|
||||
assert_equal "1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new)
|
||||
assert_equal "1", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new)
|
||||
assert_equal "1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new)
|
||||
end
|
||||
end
|
||||
|
||||
def test_empty_string_value_access
|
||||
assert_equal('', Liquid::Template.parse('{{ product[value] }}').render!('product' => ProductDrop.new, 'value' => ''))
|
||||
assert_equal '', Liquid::Template.parse('{{ product[value] }}').render!('product' => ProductDrop.new, 'value' => '')
|
||||
end
|
||||
|
||||
def test_nil_value_access
|
||||
assert_equal('', Liquid::Template.parse('{{ product[value] }}').render!('product' => ProductDrop.new, 'value' => nil))
|
||||
assert_equal '', Liquid::Template.parse('{{ product[value] }}').render!('product' => ProductDrop.new, 'value' => nil)
|
||||
end
|
||||
|
||||
def test_default_to_s_on_drops
|
||||
assert_equal('ProductDrop', Liquid::Template.parse("{{ product }}").render!('product' => ProductDrop.new))
|
||||
assert_equal('EnumerableDrop', Liquid::Template.parse('{{ collection }}').render!('collection' => EnumerableDrop.new))
|
||||
assert_equal 'ProductDrop', Liquid::Template.parse("{{ product }}").render!('product' => ProductDrop.new)
|
||||
assert_equal 'EnumerableDrop', Liquid::Template.parse('{{ collection }}').render!('collection' => EnumerableDrop.new)
|
||||
end
|
||||
|
||||
def test_invokable_methods
|
||||
assert_equal(%w(to_liquid catchall context texts).to_set, ProductDrop.invokable_methods)
|
||||
assert_equal(%w(to_liquid scopes_as_array loop_pos scopes).to_set, ContextDrop.invokable_methods)
|
||||
assert_equal(%w(to_liquid size max min first count).to_set, EnumerableDrop.invokable_methods)
|
||||
assert_equal(%w(to_liquid max min sort count first).to_set, RealEnumerableDrop.invokable_methods)
|
||||
assert_equal %w(to_liquid catchall user_input context texts).to_set, ProductDrop.invokable_methods
|
||||
assert_equal %w(to_liquid scopes_as_array loop_pos scopes).to_set, ContextDrop.invokable_methods
|
||||
assert_equal %w(to_liquid size max min first count).to_set, EnumerableDrop.invokable_methods
|
||||
assert_equal %w(to_liquid max min sort count first).to_set, RealEnumerableDrop.invokable_methods
|
||||
end
|
||||
end # DropsTest
|
||||
|
||||
@@ -35,35 +35,37 @@ class ErrorHandlingTest < Minitest::Test
|
||||
TEXT
|
||||
|
||||
output = Liquid::Template.parse(template, line_numbers: true).render('errors' => ErrorDrop.new)
|
||||
assert_equal(expected, output)
|
||||
assert_equal expected, output
|
||||
end
|
||||
|
||||
def test_standard_error
|
||||
template = Liquid::Template.parse(' {{ errors.standard_error }} ')
|
||||
assert_equal(' Liquid error: standard error ', template.render('errors' => ErrorDrop.new))
|
||||
assert_equal ' Liquid error: standard error ', template.render('errors' => ErrorDrop.new)
|
||||
|
||||
assert_equal(1, template.errors.size)
|
||||
assert_equal(StandardError, template.errors.first.class)
|
||||
assert_equal 1, template.errors.size
|
||||
assert_equal StandardError, template.errors.first.class
|
||||
end
|
||||
|
||||
def test_syntax
|
||||
template = Liquid::Template.parse(' {{ errors.syntax_error }} ')
|
||||
assert_equal(' Liquid syntax error: syntax error ', template.render('errors' => ErrorDrop.new))
|
||||
assert_equal ' Liquid syntax error: syntax error ', template.render('errors' => ErrorDrop.new)
|
||||
|
||||
assert_equal(1, template.errors.size)
|
||||
assert_equal(SyntaxError, template.errors.first.class)
|
||||
assert_equal 1, template.errors.size
|
||||
assert_equal SyntaxError, template.errors.first.class
|
||||
end
|
||||
|
||||
def test_argument
|
||||
template = Liquid::Template.parse(' {{ errors.argument_error }} ')
|
||||
assert_equal(' Liquid error: argument error ', template.render('errors' => ErrorDrop.new))
|
||||
assert_equal ' Liquid error: argument error ', template.render('errors' => ErrorDrop.new)
|
||||
|
||||
assert_equal(1, template.errors.size)
|
||||
assert_equal(ArgumentError, template.errors.first.class)
|
||||
assert_equal 1, template.errors.size
|
||||
assert_equal ArgumentError, template.errors.first.class
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def test_unrecognized_operator
|
||||
@@ -76,37 +78,43 @@ class ErrorHandlingTest < Minitest::Test
|
||||
|
||||
def test_lax_unrecognized_operator
|
||||
template = Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', error_mode: :lax)
|
||||
assert_equal(' Liquid error: Unknown operator =! ', template.render)
|
||||
assert_equal(1, template.errors.size)
|
||||
assert_equal(Liquid::ArgumentError, template.errors.first.class)
|
||||
assert_equal ' Liquid error: Unknown operator =! ', template.render
|
||||
assert_equal 1, template.errors.size
|
||||
assert_equal Liquid::ArgumentError, template.errors.first.class
|
||||
end
|
||||
|
||||
def test_with_line_numbers_adds_numbers_to_parser_errors
|
||||
source = <<~LIQUID
|
||||
foobar
|
||||
err = assert_raises(SyntaxError) do
|
||||
Liquid::Template.parse('
|
||||
foobar
|
||||
|
||||
{% "cat" | foobar %}
|
||||
{% "cat" | foobar %}
|
||||
|
||||
bla
|
||||
LIQUID
|
||||
assert_match_syntax_error(/Liquid syntax error \(line 3\)/, source)
|
||||
bla
|
||||
',
|
||||
line_numbers: true)
|
||||
end
|
||||
|
||||
assert_match(/Liquid syntax error \(line 4\)/, err.message)
|
||||
end
|
||||
|
||||
def test_with_line_numbers_adds_numbers_to_parser_errors_with_whitespace_trim
|
||||
source = <<~LIQUID
|
||||
foobar
|
||||
err = assert_raises(SyntaxError) do
|
||||
Liquid::Template.parse('
|
||||
foobar
|
||||
|
||||
{%- "cat" | foobar -%}
|
||||
{%- "cat" | foobar -%}
|
||||
|
||||
bla
|
||||
LIQUID
|
||||
bla
|
||||
',
|
||||
line_numbers: true)
|
||||
end
|
||||
|
||||
assert_match_syntax_error(/Liquid syntax error \(line 3\)/, source)
|
||||
assert_match(/Liquid syntax error \(line 4\)/, err.message)
|
||||
end
|
||||
|
||||
def test_parsing_warn_with_line_numbers_adds_numbers_to_lexer_errors
|
||||
template = Liquid::Template.parse(
|
||||
'
|
||||
template = Liquid::Template.parse('
|
||||
foobar
|
||||
|
||||
{% if 1 =! 2 %}ok{% endif %}
|
||||
@@ -114,19 +122,15 @@ class ErrorHandlingTest < Minitest::Test
|
||||
bla
|
||||
',
|
||||
error_mode: :warn,
|
||||
line_numbers: true,
|
||||
)
|
||||
line_numbers: true)
|
||||
|
||||
assert_equal(
|
||||
['Liquid syntax error (line 4): Unexpected character = in "1 =! 2"'],
|
||||
template.warnings.map(&:message),
|
||||
)
|
||||
assert_equal ['Liquid syntax error (line 4): Unexpected character = in "1 =! 2"'],
|
||||
template.warnings.map(&:message)
|
||||
end
|
||||
|
||||
def test_parsing_strict_with_line_numbers_adds_numbers_to_lexer_errors
|
||||
err = assert_raises(SyntaxError) do
|
||||
Liquid::Template.parse(
|
||||
'
|
||||
Liquid::Template.parse('
|
||||
foobar
|
||||
|
||||
{% if 1 =! 2 %}ok{% endif %}
|
||||
@@ -134,60 +138,62 @@ class ErrorHandlingTest < Minitest::Test
|
||||
bla
|
||||
',
|
||||
error_mode: :strict,
|
||||
line_numbers: true,
|
||||
)
|
||||
line_numbers: true)
|
||||
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
|
||||
|
||||
def test_syntax_errors_in_nested_blocks_have_correct_line_number
|
||||
source = <<~LIQUID
|
||||
foobar
|
||||
err = assert_raises(SyntaxError) do
|
||||
Liquid::Template.parse('
|
||||
foobar
|
||||
|
||||
{% if 1 != 2 %}
|
||||
{% foo %}
|
||||
{% endif %}
|
||||
{% if 1 != 2 %}
|
||||
{% foo %}
|
||||
{% endif %}
|
||||
|
||||
bla
|
||||
LIQUID
|
||||
bla
|
||||
',
|
||||
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
|
||||
|
||||
def test_strict_error_messages
|
||||
err = assert_raises(SyntaxError) do
|
||||
Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', error_mode: :strict)
|
||||
end
|
||||
assert_equal('Liquid syntax error: Unexpected character = in "1 =! 2"', err.message)
|
||||
assert_equal 'Liquid syntax error: Unexpected character = in "1 =! 2"', err.message
|
||||
|
||||
err = assert_raises(SyntaxError) do
|
||||
Liquid::Template.parse('{{%%%}}', error_mode: :strict)
|
||||
end
|
||||
assert_equal('Liquid syntax error: Unexpected character % in "{{%%%}}"', err.message)
|
||||
assert_equal 'Liquid syntax error: Unexpected character % in "{{%%%}}"', err.message
|
||||
end
|
||||
|
||||
def test_warnings
|
||||
template = Liquid::Template.parse('{% if ~~~ %}{{%%%}}{% else %}{{ hello. }}{% endif %}', error_mode: :warn)
|
||||
assert_equal(3, template.warnings.size)
|
||||
assert_equal('Unexpected character ~ in "~~~"', template.warnings[0].to_s(false))
|
||||
assert_equal('Unexpected character % in "{{%%%}}"', template.warnings[1].to_s(false))
|
||||
assert_equal('Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].to_s(false))
|
||||
assert_equal('', template.render)
|
||||
assert_equal 3, template.warnings.size
|
||||
assert_equal 'Unexpected character ~ in "~~~"', template.warnings[0].to_s(false)
|
||||
assert_equal 'Unexpected character % in "{{%%%}}"', template.warnings[1].to_s(false)
|
||||
assert_equal 'Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].to_s(false)
|
||||
assert_equal '', template.render
|
||||
end
|
||||
|
||||
def test_warning_line_numbers
|
||||
template = Liquid::Template.parse("{% if ~~~ %}\n{{%%%}}{% else %}\n{{ hello. }}{% endif %}", error_mode: :warn, line_numbers: true)
|
||||
assert_equal('Liquid syntax error (line 1): Unexpected character ~ in "~~~"', template.warnings[0].message)
|
||||
assert_equal('Liquid syntax error (line 2): Unexpected character % in "{{%%%}}"', template.warnings[1].message)
|
||||
assert_equal('Liquid syntax error (line 3): Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].message)
|
||||
assert_equal(3, template.warnings.size)
|
||||
assert_equal([1, 2, 3], template.warnings.map(&:line_number))
|
||||
assert_equal 'Liquid syntax error (line 1): Unexpected character ~ in "~~~"', template.warnings[0].message
|
||||
assert_equal 'Liquid syntax error (line 2): Unexpected character % in "{{%%%}}"', template.warnings[1].message
|
||||
assert_equal 'Liquid syntax error (line 3): Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].message
|
||||
assert_equal 3, template.warnings.size
|
||||
assert_equal [1, 2, 3], template.warnings.map(&:line_number)
|
||||
end
|
||||
|
||||
# Liquid should not catch Exceptions that are not subclasses of StandardError, like Interrupt and NoMemoryError
|
||||
def test_exceptions_propagate
|
||||
assert_raises(Exception) do
|
||||
assert_raises Exception do
|
||||
template = Liquid::Template.parse('{{ errors.exception }}')
|
||||
template.render('errors' => ErrorDrop.new)
|
||||
end
|
||||
@@ -198,41 +204,35 @@ class ErrorHandlingTest < Minitest::Test
|
||||
|
||||
output = template.render('errors' => ErrorDrop.new)
|
||||
|
||||
assert_equal('This is a runtime error: Liquid error (line 1): internal', output)
|
||||
assert_equal([Liquid::InternalError], template.errors.map(&:class))
|
||||
assert_equal 'This is a runtime error: Liquid error (line 1): internal', output
|
||||
assert_equal [Liquid::InternalError], template.errors.map(&:class)
|
||||
end
|
||||
|
||||
def test_setting_default_exception_renderer
|
||||
old_exception_renderer = Liquid::Template.default_exception_renderer
|
||||
exceptions = []
|
||||
Liquid::Template.default_exception_renderer = ->(e) {
|
||||
exceptions << e
|
||||
''
|
||||
}
|
||||
Liquid::Template.default_exception_renderer = ->(e) { exceptions << e; '' }
|
||||
template = Liquid::Template.parse('This is a runtime error: {{ errors.argument_error }}')
|
||||
|
||||
output = template.render('errors' => ErrorDrop.new)
|
||||
|
||||
assert_equal('This is a runtime error: ', output)
|
||||
assert_equal([Liquid::ArgumentError], template.errors.map(&:class))
|
||||
assert_equal 'This is a runtime error: ', output
|
||||
assert_equal [Liquid::ArgumentError], template.errors.map(&:class)
|
||||
ensure
|
||||
Liquid::Template.default_exception_renderer = old_exception_renderer if old_exception_renderer
|
||||
end
|
||||
|
||||
def test_exception_renderer_exposing_non_liquid_error
|
||||
template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true)
|
||||
template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true)
|
||||
exceptions = []
|
||||
handler = ->(e) {
|
||||
exceptions << e
|
||||
e.cause
|
||||
}
|
||||
handler = ->(e) { exceptions << e; e.cause }
|
||||
|
||||
output = template.render({ 'errors' => ErrorDrop.new }, exception_renderer: handler)
|
||||
|
||||
assert_equal('This is a runtime error: runtime error', output)
|
||||
assert_equal([Liquid::InternalError], exceptions.map(&:class))
|
||||
assert_equal(exceptions, template.errors)
|
||||
assert_equal('#<RuntimeError: runtime error>', exceptions.first.cause.inspect)
|
||||
assert_equal 'This is a runtime error: runtime error', output
|
||||
assert_equal [Liquid::InternalError], exceptions.map(&:class)
|
||||
assert_equal exceptions, template.errors
|
||||
assert_equal '#<RuntimeError: runtime error>', exceptions.first.cause.inspect
|
||||
end
|
||||
|
||||
class TestFileSystem
|
||||
@@ -246,98 +246,12 @@ class ErrorHandlingTest < Minitest::Test
|
||||
|
||||
begin
|
||||
Liquid::Template.file_system = TestFileSystem.new
|
||||
|
||||
template = Liquid::Template.parse("Argument error:\n{% include 'product' %}", line_numbers: true)
|
||||
page = template.render('errors' => ErrorDrop.new)
|
||||
page = template.render('errors' => ErrorDrop.new)
|
||||
ensure
|
||||
Liquid::Template.file_system = old_file_system
|
||||
end
|
||||
assert_equal("Argument error:\nLiquid error (product line 1): argument error", page)
|
||||
assert_equal("product", template.errors.first.template_name)
|
||||
end
|
||||
|
||||
def test_bug_compatible_silencing_of_errors_in_blank_nodes
|
||||
output = Liquid::Template.parse("{% assign x = 0 %}{% if 1 < '2' %}not blank{% assign x = 3 %}{% endif %}{{ x }}").render
|
||||
assert_equal("Liquid error: comparison of Integer with String failed0", output)
|
||||
|
||||
output = Liquid::Template.parse("{% assign x = 0 %}{% if 1 < '2' %}{% assign x = 3 %}{% endif %}{{ x }}").render
|
||||
assert_equal("0", output)
|
||||
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),
|
||||
)
|
||||
assert_equal "Argument error:\nLiquid error (product line 1): argument error", page
|
||||
assert_equal "product", template.errors.first.template_name
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class ExpressionTest < Minitest::Test
|
||||
def test_keyword_literals
|
||||
assert_template_result("true", "{{ true }}")
|
||||
assert_expression_result(true, "true")
|
||||
end
|
||||
|
||||
def test_string
|
||||
assert_template_result("single quoted", "{{'single quoted'}}")
|
||||
assert_template_result("double quoted", '{{"double quoted"}}')
|
||||
assert_template_result("spaced", "{{ 'spaced' }}")
|
||||
assert_template_result("spaced2", "{{ 'spaced2' }}")
|
||||
end
|
||||
|
||||
def test_int
|
||||
assert_template_result("456", "{{ 456 }}")
|
||||
assert_expression_result(123, "123")
|
||||
assert_expression_result(12, "012")
|
||||
end
|
||||
|
||||
def test_float
|
||||
assert_template_result("2.5", "{{ 2.5 }}")
|
||||
assert_expression_result(1.5, "1.5")
|
||||
end
|
||||
|
||||
def test_range
|
||||
assert_template_result("3..4", "{{ ( 3 .. 4 ) }}")
|
||||
assert_expression_result(1..2, "(1..2)")
|
||||
|
||||
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
|
||||
|
||||
private
|
||||
|
||||
def assert_expression_result(expect, markup, **assigns)
|
||||
liquid = "{% if expect == #{markup} %}pass{% else %}got {{ #{markup} }}{% endif %}"
|
||||
assert_template_result("pass", liquid, { "expect" => expect, **assigns })
|
||||
end
|
||||
end
|
||||
@@ -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
|
||||
@@ -41,13 +41,13 @@ class FiltersTest < Minitest::Test
|
||||
@context['var'] = 1000
|
||||
@context.add_filters(MoneyFilter)
|
||||
|
||||
assert_equal(' 1000$ ', Template.parse("{{var | money}}").render(@context))
|
||||
assert_equal ' 1000$ ', Template.parse("{{var | money}}").render(@context)
|
||||
end
|
||||
|
||||
def test_underscore_in_filter_name
|
||||
@context['var'] = 1000
|
||||
@context.add_filters(MoneyFilter)
|
||||
assert_equal(' 1000$ ', Template.parse("{{var | money_with_underscore}}").render(@context))
|
||||
assert_equal ' 1000$ ', Template.parse("{{var | money_with_underscore}}").render(@context)
|
||||
end
|
||||
|
||||
def test_second_filter_overwrites_first
|
||||
@@ -55,115 +55,103 @@ class FiltersTest < Minitest::Test
|
||||
@context.add_filters(MoneyFilter)
|
||||
@context.add_filters(CanadianMoneyFilter)
|
||||
|
||||
assert_equal(' 1000$ CAD ', Template.parse("{{var | money}}").render(@context))
|
||||
assert_equal ' 1000$ CAD ', Template.parse("{{var | money}}").render(@context)
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
def test_sort
|
||||
assert_template_result("1 2 3 4", "{{numbers | sort | join}}", { "numbers" => [2, 1, 4, 3] })
|
||||
assert_template_result(
|
||||
"alphabetic as expected",
|
||||
"{{words | sort | join}}",
|
||||
{ "words" => ['expected', 'as', 'alphabetic'] },
|
||||
)
|
||||
assert_template_result("3", "{{value | sort}}", { "value" => 3 })
|
||||
assert_template_result('are flower', "{{arrays | sort | join}}", { 'arrays' => ['flower', 'are'] })
|
||||
assert_template_result(
|
||||
"Expected case sensitive",
|
||||
"{{case_sensitive | sort | join}}",
|
||||
{ "case_sensitive" => ["sensitive", "Expected", "case"] },
|
||||
)
|
||||
@context['value'] = 3
|
||||
@context['numbers'] = [2, 1, 4, 3]
|
||||
@context['words'] = ['expected', 'as', 'alphabetic']
|
||||
@context['arrays'] = ['flower', 'are']
|
||||
@context['case_sensitive'] = ['sensitive', 'Expected', 'case']
|
||||
|
||||
assert_equal '1 2 3 4', Template.parse("{{numbers | sort | join}}").render(@context)
|
||||
assert_equal 'alphabetic as expected', Template.parse("{{words | sort | join}}").render(@context)
|
||||
assert_equal '3', Template.parse("{{value | sort}}").render(@context)
|
||||
assert_equal 'are flower', Template.parse("{{arrays | sort | join}}").render(@context)
|
||||
assert_equal 'Expected case sensitive', Template.parse("{{case_sensitive | sort | join}}").render(@context)
|
||||
end
|
||||
|
||||
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
|
||||
assert_template_result(
|
||||
"Assert case Insensitive",
|
||||
"{{words | sort_natural | join}}",
|
||||
{ "words" => ["case", "Assert", "Insensitive"] },
|
||||
)
|
||||
assert_equal 'Assert case Insensitive', Template.parse("{{words | sort_natural | join}}").render(@context)
|
||||
|
||||
# Test hashes
|
||||
assert_template_result(
|
||||
"A b C",
|
||||
"{{hashes | sort_natural: 'a' | map: 'a' | join}}",
|
||||
{ "hashes" => [{ "a" => "A" }, { "a" => "b" }, { "a" => "C" }] },
|
||||
)
|
||||
assert_equal 'A b C', Template.parse("{{hashes | sort_natural: 'a' | map: 'a' | join}}").render(@context)
|
||||
|
||||
# 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
|
||||
|
||||
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
|
||||
assert_template_result(
|
||||
"a b c",
|
||||
"{{words | compact | join}}",
|
||||
{ "words" => ['a', nil, 'b', nil, 'c'] },
|
||||
)
|
||||
assert_equal 'a b c', Template.parse("{{words | compact | join}}").render(@context)
|
||||
|
||||
# Test hashes
|
||||
assert_template_result(
|
||||
"A C",
|
||||
"{{hashes | compact: 'a' | map: 'a' | join}}",
|
||||
{ "hashes" => [{ "a" => "A" }, { "a" => nil }, { "a" => "C" }] },
|
||||
)
|
||||
assert_equal 'A C', Template.parse("{{hashes | compact: 'a' | map: 'a' | join}}").render(@context)
|
||||
|
||||
# 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
|
||||
|
||||
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
|
||||
|
||||
def test_strip_html_ignore_comments_with_html
|
||||
assert_template_result(
|
||||
"bla blub",
|
||||
"{{ var | strip_html }}",
|
||||
{ "var" => "<!-- split and some <ul> tag --><b>bla blub</a>" },
|
||||
)
|
||||
@context['var'] = "<!-- split and some <ul> tag --><b>bla blub</a>"
|
||||
|
||||
assert_equal "bla blub", Template.parse("{{ var | strip_html }}").render(@context)
|
||||
end
|
||||
|
||||
def test_capitalize
|
||||
assert_template_result("Blub", "{{ var | capitalize }}", { "var" => "blub" })
|
||||
@context['var'] = "blub"
|
||||
|
||||
assert_equal "Blub", Template.parse("{{ var | capitalize }}").render(@context)
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def test_filter_with_keyword_arguments
|
||||
@context['surname'] = 'john'
|
||||
@context['input'] = 'hello %{first_name}, %{last_name}'
|
||||
@context['input'] = 'hello %{first_name}, %{last_name}'
|
||||
@context.add_filters(SubstituteFilter)
|
||||
output = Template.parse(%({{ input | substitute: first_name: surname, last_name: 'doe' }})).render(@context)
|
||||
assert_equal('hello john, doe', output)
|
||||
output = Template.parse(%({{ input | substitute: first_name: surname, last_name: 'doe' }})).render(@context)
|
||||
assert_equal 'hello john, doe', output
|
||||
end
|
||||
|
||||
def test_override_object_method_in_filter
|
||||
assert_equal("tap overridden", Template.parse("{{var | tap}}").render!({ 'var' => 1000 }, filters: [OverrideObjectMethodFilter]))
|
||||
assert_equal "tap overridden", Template.parse("{{var | tap}}").render!({ 'var' => 1000 }, filters: [OverrideObjectMethodFilter])
|
||||
|
||||
# tap still treated as a non-existent filter
|
||||
assert_equal("1000", Template.parse("{{var | tap}}").render!('var' => 1000))
|
||||
end
|
||||
|
||||
def test_liquid_argument_error
|
||||
source = "{{ '' | size: 'too many args' }}"
|
||||
exc = assert_raises(Liquid::ArgumentError) do
|
||||
Template.parse(source).render!
|
||||
end
|
||||
assert_match(/\ALiquid error: wrong number of arguments /, exc.message)
|
||||
assert_equal(exc.message, Template.parse(source).render)
|
||||
assert_equal "1000", Template.parse("{{var | tap}}").render!('var' => 1000)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -172,15 +160,15 @@ class FiltersInTemplate < Minitest::Test
|
||||
|
||||
def test_local_global
|
||||
with_global_filter(MoneyFilter) do
|
||||
assert_equal(" 1000$ ", Template.parse("{{1000 | money}}").render!(nil, nil))
|
||||
assert_equal(" 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, filters: CanadianMoneyFilter))
|
||||
assert_equal(" 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, filters: [CanadianMoneyFilter]))
|
||||
assert_equal " 1000$ ", Template.parse("{{1000 | money}}").render!(nil, nil)
|
||||
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, filters: CanadianMoneyFilter)
|
||||
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, filters: [CanadianMoneyFilter])
|
||||
end
|
||||
end
|
||||
|
||||
def test_local_filter_with_deprecated_syntax
|
||||
assert_equal(" 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, CanadianMoneyFilter))
|
||||
assert_equal(" 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, [CanadianMoneyFilter]))
|
||||
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, CanadianMoneyFilter)
|
||||
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, [CanadianMoneyFilter])
|
||||
end
|
||||
end # FiltersTest
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ class HashOrderingTest < Minitest::Test
|
||||
|
||||
def test_global_register_order
|
||||
with_global_filter(MoneyFilter, CanadianMoneyFilter) do
|
||||
assert_equal(" 1000$ CAD ", Template.parse("{{1000 | money}}").render(nil, nil))
|
||||
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render(nil, nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -33,88 +33,93 @@ class OutputTest < Minitest::Test
|
||||
|
||||
def setup
|
||||
@assigns = {
|
||||
'best_cars' => 'bmw',
|
||||
'car' => { 'bmw' => 'good', 'gm' => 'bad' },
|
||||
}
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def test_variable_traversing_with_two_brackets
|
||||
source = "{{ site.data.menu[include.menu][include.locale] }}"
|
||||
assert_template_result("it works!", source, {
|
||||
text = %({{ site.data.menu[include.menu][include.locale] }})
|
||||
assert_equal "it works!", Template.parse(text).render!(
|
||||
"site" => { "data" => { "menu" => { "foo" => { "bar" => "it works!" } } } },
|
||||
"include" => { "menu" => "foo", "locale" => "bar" },
|
||||
})
|
||||
"include" => { "menu" => "foo", "locale" => "bar" }
|
||||
)
|
||||
end
|
||||
|
||||
def test_variable_traversing
|
||||
source = " {{car.bmw}} {{car.gm}} {{car.bmw}} "
|
||||
assert_template_result(" good bad good ", source, @assigns)
|
||||
text = %( {{car.bmw}} {{car.gm}} {{car.bmw}} )
|
||||
|
||||
expected = %( good bad good )
|
||||
assert_equal expected, Template.parse(text).render!(@assigns)
|
||||
end
|
||||
|
||||
def test_variable_piping
|
||||
text = %( {{ car.gm | make_funny }} )
|
||||
text = %( {{ car.gm | make_funny }} )
|
||||
expected = %( LOL )
|
||||
|
||||
assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
|
||||
end
|
||||
|
||||
def test_variable_piping_with_input
|
||||
text = %( {{ car.gm | cite_funny }} )
|
||||
text = %( {{ car.gm | cite_funny }} )
|
||||
expected = %( LOL: bad )
|
||||
|
||||
assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
|
||||
end
|
||||
|
||||
def test_variable_piping_with_args
|
||||
text = %! {{ car.gm | add_smiley : ':-(' }} !
|
||||
text = %! {{ car.gm | add_smiley : ':-(' }} !
|
||||
expected = %| bad :-( |
|
||||
|
||||
assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
|
||||
end
|
||||
|
||||
def test_variable_piping_with_no_args
|
||||
text = %( {{ car.gm | add_smiley }} )
|
||||
text = %( {{ car.gm | add_smiley }} )
|
||||
expected = %| bad :-) |
|
||||
|
||||
assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
|
||||
end
|
||||
|
||||
def test_multiple_variable_piping_with_args
|
||||
text = %! {{ car.gm | add_smiley : ':-(' | add_smiley : ':-('}} !
|
||||
text = %! {{ car.gm | add_smiley : ':-(' | add_smiley : ':-('}} !
|
||||
expected = %| bad :-( :-( |
|
||||
|
||||
assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
|
||||
end
|
||||
|
||||
def test_variable_piping_with_multiple_args
|
||||
text = %( {{ car.gm | add_tag : 'span', 'bar'}} )
|
||||
text = %( {{ car.gm | add_tag : 'span', 'bar'}} )
|
||||
expected = %( <span id="bar">bad</span> )
|
||||
|
||||
assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
|
||||
end
|
||||
|
||||
def test_variable_piping_with_variable_args
|
||||
text = %( {{ car.gm | add_tag : 'span', car.bmw}} )
|
||||
text = %( {{ car.gm | add_tag : 'span', car.bmw}} )
|
||||
expected = %( <span id="good">bad</span> )
|
||||
|
||||
assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
|
||||
end
|
||||
|
||||
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> )
|
||||
|
||||
assert_equal(expected, Template.parse(text).render!(assigns, filters: [FunnyFilter]))
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
|
||||
end
|
||||
|
||||
def test_link_to
|
||||
text = %( {{ 'Typo' | link_to: 'http://typo.leetsoft.com' }} )
|
||||
text = %( {{ 'Typo' | link_to: 'http://typo.leetsoft.com' }} )
|
||||
expected = %( <a href="http://typo.leetsoft.com">Typo</a> )
|
||||
|
||||
assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
|
||||
end
|
||||
end # OutputTest
|
||||
|
||||
+33
-61
@@ -8,245 +8,217 @@ class ParseTreeVisitorTest < Minitest::Test
|
||||
def test_variable
|
||||
assert_equal(
|
||||
["test"],
|
||||
visit(%({{ test }})),
|
||||
visit(%({{ test }}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_varible_with_filter
|
||||
assert_equal(
|
||||
["test", "infilter"],
|
||||
visit(%({{ test | split: infilter }})),
|
||||
visit(%({{ test | split: infilter }}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_dynamic_variable
|
||||
assert_equal(
|
||||
["test", "inlookup"],
|
||||
visit(%({{ test[inlookup] }})),
|
||||
)
|
||||
end
|
||||
|
||||
def test_echo
|
||||
assert_equal(
|
||||
["test"],
|
||||
visit(%({% echo test %})),
|
||||
visit(%({{ test[inlookup] }}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_if_condition
|
||||
assert_equal(
|
||||
["test"],
|
||||
visit(%({% if test %}{% endif %})),
|
||||
visit(%({% if test %}{% endif %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_complex_if_condition
|
||||
assert_equal(
|
||||
["test"],
|
||||
visit(%({% if 1 == 1 and 2 == test %}{% endif %})),
|
||||
visit(%({% if 1 == 1 and 2 == test %}{% endif %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_if_body
|
||||
assert_equal(
|
||||
["test"],
|
||||
visit(%({% if 1 == 1 %}{{ test }}{% endif %})),
|
||||
visit(%({% if 1 == 1 %}{{ test }}{% endif %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_unless_condition
|
||||
assert_equal(
|
||||
["test"],
|
||||
visit(%({% unless test %}{% endunless %})),
|
||||
visit(%({% unless test %}{% endunless %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_complex_unless_condition
|
||||
assert_equal(
|
||||
["test"],
|
||||
visit(%({% unless 1 == 1 and 2 == test %}{% endunless %})),
|
||||
visit(%({% unless 1 == 1 and 2 == test %}{% endunless %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_unless_body
|
||||
assert_equal(
|
||||
["test"],
|
||||
visit(%({% unless 1 == 1 %}{{ test }}{% endunless %})),
|
||||
visit(%({% unless 1 == 1 %}{{ test }}{% endunless %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_elsif_condition
|
||||
assert_equal(
|
||||
["test"],
|
||||
visit(%({% if 1 == 1 %}{% elsif test %}{% endif %})),
|
||||
visit(%({% if 1 == 1 %}{% elsif test %}{% endif %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_complex_elsif_condition
|
||||
assert_equal(
|
||||
["test"],
|
||||
visit(%({% if 1 == 1 %}{% elsif 1 == 1 and 2 == test %}{% endif %})),
|
||||
visit(%({% if 1 == 1 %}{% elsif 1 == 1 and 2 == test %}{% endif %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_elsif_body
|
||||
assert_equal(
|
||||
["test"],
|
||||
visit(%({% if 1 == 1 %}{% elsif 2 == 2 %}{{ test }}{% endif %})),
|
||||
visit(%({% if 1 == 1 %}{% elsif 2 == 2 %}{{ test }}{% endif %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_else_body
|
||||
assert_equal(
|
||||
["test"],
|
||||
visit(%({% if 1 == 1 %}{% else %}{{ test }}{% endif %})),
|
||||
visit(%({% if 1 == 1 %}{% else %}{{ test }}{% endif %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_case_left
|
||||
assert_equal(
|
||||
["test"],
|
||||
visit(%({% case test %}{% endcase %})),
|
||||
visit(%({% case test %}{% endcase %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_case_condition
|
||||
assert_equal(
|
||||
["test"],
|
||||
visit(%({% case 1 %}{% when test %}{% endcase %})),
|
||||
visit(%({% case 1 %}{% when test %}{% endcase %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_case_when_body
|
||||
assert_equal(
|
||||
["test"],
|
||||
visit(%({% case 1 %}{% when 2 %}{{ test }}{% endcase %})),
|
||||
visit(%({% case 1 %}{% when 2 %}{{ test }}{% endcase %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_case_else_body
|
||||
assert_equal(
|
||||
["test"],
|
||||
visit(%({% case 1 %}{% else %}{{ test }}{% endcase %})),
|
||||
visit(%({% case 1 %}{% else %}{{ test }}{% endcase %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_for_in
|
||||
assert_equal(
|
||||
["test"],
|
||||
visit(%({% for x in test %}{% endfor %})),
|
||||
visit(%({% for x in test %}{% endfor %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_for_limit
|
||||
assert_equal(
|
||||
["test"],
|
||||
visit(%({% for x in (1..5) limit: test %}{% endfor %})),
|
||||
visit(%({% for x in (1..5) limit: test %}{% endfor %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_for_offset
|
||||
assert_equal(
|
||||
["test"],
|
||||
visit(%({% for x in (1..5) offset: test %}{% endfor %})),
|
||||
visit(%({% for x in (1..5) offset: test %}{% endfor %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_for_body
|
||||
assert_equal(
|
||||
["test"],
|
||||
visit(%({% for x in (1..5) %}{{ test }}{% endfor %})),
|
||||
)
|
||||
end
|
||||
|
||||
def test_for_range
|
||||
assert_equal(
|
||||
["test"],
|
||||
visit(%({% for x in (1..test) %}{% endfor %})),
|
||||
visit(%({% for x in (1..5) %}{{ test }}{% endfor %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_tablerow_in
|
||||
assert_equal(
|
||||
["test"],
|
||||
visit(%({% tablerow x in test %}{% endtablerow %})),
|
||||
visit(%({% tablerow x in test %}{% endtablerow %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_tablerow_limit
|
||||
assert_equal(
|
||||
["test"],
|
||||
visit(%({% tablerow x in (1..5) limit: test %}{% endtablerow %})),
|
||||
visit(%({% tablerow x in (1..5) limit: test %}{% endtablerow %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_tablerow_offset
|
||||
assert_equal(
|
||||
["test"],
|
||||
visit(%({% tablerow x in (1..5) offset: test %}{% endtablerow %})),
|
||||
visit(%({% tablerow x in (1..5) offset: test %}{% endtablerow %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_tablerow_body
|
||||
assert_equal(
|
||||
["test"],
|
||||
visit(%({% tablerow x in (1..5) %}{{ test }}{% endtablerow %})),
|
||||
visit(%({% tablerow x in (1..5) %}{{ test }}{% endtablerow %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_cycle
|
||||
assert_equal(
|
||||
["test"],
|
||||
visit(%({% cycle test %})),
|
||||
visit(%({% cycle test %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_assign
|
||||
assert_equal(
|
||||
["test"],
|
||||
visit(%({% assign x = test %})),
|
||||
visit(%({% assign x = test %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_capture
|
||||
assert_equal(
|
||||
["test"],
|
||||
visit(%({% capture x %}{{ test }}{% endcapture %})),
|
||||
visit(%({% capture x %}{{ test }}{% endcapture %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_include
|
||||
assert_equal(
|
||||
["test"],
|
||||
visit(%({% include test %})),
|
||||
visit(%({% include test %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_include_with
|
||||
assert_equal(
|
||||
["test"],
|
||||
visit(%({% include "hai" with test %})),
|
||||
visit(%({% include "hai" with test %}))
|
||||
)
|
||||
end
|
||||
|
||||
def test_include_for
|
||||
assert_equal(
|
||||
["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 %})),
|
||||
visit(%({% include "hai" for test %}))
|
||||
)
|
||||
end
|
||||
|
||||
@@ -257,7 +229,7 @@ class ParseTreeVisitorTest < Minitest::Test
|
||||
["test", []],
|
||||
["xs", []],
|
||||
]]],
|
||||
traversal(%({% for x in xs offset: test %}{{ other }}{% endfor %})).visit,
|
||||
traversal(%({% for x in xs offset: test %}{{ other }}{% endfor %})).visit
|
||||
)
|
||||
end
|
||||
|
||||
@@ -266,7 +238,7 @@ class ParseTreeVisitorTest < Minitest::Test
|
||||
def traversal(template)
|
||||
ParseTreeVisitor
|
||||
.for(Template.parse(template).root)
|
||||
.add_callback_for(VariableLookup) { |node| node.name } # rubocop:disable Style/SymbolProc
|
||||
.add_callback_for(VariableLookup, &:name)
|
||||
end
|
||||
|
||||
def visit(template)
|
||||
@@ -7,7 +7,7 @@ class ParsingQuirksTest < Minitest::Test
|
||||
|
||||
def test_parsing_css
|
||||
text = " div { font-weight: bold; } "
|
||||
assert_equal(text, Template.parse(text).render!)
|
||||
assert_equal text, Template.parse(text).render!
|
||||
end
|
||||
|
||||
def test_raise_on_single_close_bracet
|
||||
@@ -29,10 +29,10 @@ class ParsingQuirksTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_error_on_empty_filter
|
||||
assert(Template.parse("{{test}}"))
|
||||
assert Template.parse("{{test}}")
|
||||
|
||||
with_error_mode(:lax) do
|
||||
assert(Template.parse("{{|test}}"))
|
||||
assert Template.parse("{{|test}}")
|
||||
end
|
||||
|
||||
with_error_mode(:strict) do
|
||||
@@ -64,15 +64,15 @@ class ParsingQuirksTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_no_error_on_lax_empty_filter
|
||||
assert(Template.parse("{{test |a|b|}}", error_mode: :lax))
|
||||
assert(Template.parse("{{test}}", error_mode: :lax))
|
||||
assert(Template.parse("{{|test|}}", error_mode: :lax))
|
||||
assert Template.parse("{{test |a|b|}}", error_mode: :lax)
|
||||
assert Template.parse("{{test}}", error_mode: :lax)
|
||||
assert Template.parse("{{|test|}}", error_mode: :lax)
|
||||
end
|
||||
|
||||
def test_meaningless_parens_lax
|
||||
with_error_mode(:lax) do
|
||||
assigns = { 'b' => 'bar', 'c' => 'baz' }
|
||||
markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false"
|
||||
markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false"
|
||||
assert_template_result(' YES ', "{% if #{markup} %} YES {% endif %}", assigns)
|
||||
end
|
||||
end
|
||||
@@ -118,17 +118,7 @@ class ParsingQuirksTest < Minitest::Test
|
||||
end
|
||||
end
|
||||
|
||||
def test_blank_variable_markup
|
||||
assert_template_result('', "{{}}")
|
||||
end
|
||||
|
||||
def test_lookup_on_var_with_literal_name
|
||||
assigns = { "blank" => { "x" => "result" } }
|
||||
assert_template_result('result', "{{ blank.x }}", assigns)
|
||||
assert_template_result('result', "{{ blank['x'] }}", assigns)
|
||||
end
|
||||
|
||||
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 # ParsingQuirksTest
|
||||
|
||||
@@ -1,240 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class ProfilerTest < Minitest::Test
|
||||
class TestDrop < Liquid::Drop
|
||||
def initialize(value)
|
||||
super()
|
||||
@value = value
|
||||
end
|
||||
|
||||
def to_s
|
||||
artificial_execution_time
|
||||
|
||||
@value
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Monotonic clock precision fluctuate based on the operating system
|
||||
# By introducing a small sleep we ensure ourselves to register a non zero unit of time
|
||||
def artificial_execution_time
|
||||
sleep(Process.clock_getres(Process::CLOCK_MONOTONIC))
|
||||
end
|
||||
end
|
||||
|
||||
include Liquid
|
||||
|
||||
class ProfilingFileSystem
|
||||
def read_template_file(template_path)
|
||||
"Rendering template {% assign template_name = '#{template_path}'%}\n{{ template_name }}"
|
||||
end
|
||||
end
|
||||
|
||||
def setup
|
||||
Liquid::Template.file_system = ProfilingFileSystem.new
|
||||
end
|
||||
|
||||
def test_template_allows_flagging_profiling
|
||||
t = Template.parse("{{ 'a string' | upcase }}")
|
||||
t.render!
|
||||
|
||||
assert_nil(t.profiler)
|
||||
end
|
||||
|
||||
def test_parse_makes_available_simple_profiling
|
||||
t = Template.parse("{{ 'a string' | upcase }}", profile: true)
|
||||
t.render!
|
||||
|
||||
assert_equal(1, t.profiler.length)
|
||||
|
||||
node = t.profiler[0]
|
||||
assert_equal(" 'a string' | upcase ", node.code)
|
||||
end
|
||||
|
||||
def test_render_ignores_raw_strings_when_profiling
|
||||
t = Template.parse("This is raw string\nstuff\nNewline", profile: true)
|
||||
t.render!
|
||||
|
||||
assert_equal(0, t.profiler.length)
|
||||
end
|
||||
|
||||
def test_profiling_includes_line_numbers_of_liquid_nodes
|
||||
t = Template.parse("{{ 'a string' | upcase }}\n{% increment test %}", profile: true)
|
||||
t.render!
|
||||
assert_equal(2, t.profiler.length)
|
||||
|
||||
# {{ 'a string' | upcase }}
|
||||
assert_equal(1, t.profiler[0].line_number)
|
||||
# {{ increment test }}
|
||||
assert_equal(2, t.profiler[1].line_number)
|
||||
end
|
||||
|
||||
def test_profiling_includes_line_numbers_of_included_partials
|
||||
t = Template.parse("{% include 'a_template' %}", profile: true)
|
||||
t.render!
|
||||
|
||||
included_children = t.profiler[0].children
|
||||
|
||||
# {% assign template_name = 'a_template' %}
|
||||
assert_equal(1, included_children[0].line_number)
|
||||
# {{ template_name }}
|
||||
assert_equal(2, included_children[1].line_number)
|
||||
end
|
||||
|
||||
def test_profiling_render_tag
|
||||
t = Template.parse("{% render 'a_template' %}", profile: true)
|
||||
t.render!
|
||||
|
||||
render_children = t.profiler[0].children
|
||||
render_children.each do |timing|
|
||||
assert_equal('a_template', timing.partial)
|
||||
end
|
||||
assert_equal([1, 2], render_children.map(&:line_number))
|
||||
end
|
||||
|
||||
def test_profiling_times_the_rendering_of_tokens
|
||||
t = Template.parse("{% include 'a_template' %}", profile: true)
|
||||
t.render!
|
||||
|
||||
node = t.profiler[0]
|
||||
refute_nil(node.render_time)
|
||||
end
|
||||
|
||||
def test_profiling_times_the_entire_render
|
||||
t = Template.parse("{% include 'a_template' %}", profile: true)
|
||||
t.render!
|
||||
|
||||
assert(t.profiler.total_render_time >= 0, "Total render time was not calculated")
|
||||
end
|
||||
|
||||
class SleepTag < Liquid::Tag
|
||||
def initialize(tag_name, markup, parse_context)
|
||||
super
|
||||
@duration = Float(markup)
|
||||
end
|
||||
|
||||
def render_to_output_buffer(_context, _output)
|
||||
sleep(@duration)
|
||||
end
|
||||
end
|
||||
|
||||
def test_profiling_multiple_renders
|
||||
with_custom_tag('sleep', SleepTag) do
|
||||
context = Liquid::Context.new
|
||||
t = Liquid::Template.parse("{% sleep 0.001 %}", profile: true)
|
||||
context.template_name = 'index'
|
||||
t.render!(context)
|
||||
context.template_name = 'layout'
|
||||
first_render_time = context.profiler.total_time
|
||||
t.render!(context)
|
||||
|
||||
profiler = context.profiler
|
||||
children = profiler.children
|
||||
assert_operator(first_render_time, :>=, 0.001)
|
||||
assert_operator(profiler.total_time, :>=, 0.001 + first_render_time)
|
||||
assert_equal(["index", "layout"], children.map(&:template_name))
|
||||
assert_equal([nil, nil], children.map(&:code))
|
||||
assert_equal(profiler.total_time, children.map(&:total_time).reduce(&:+))
|
||||
end
|
||||
end
|
||||
|
||||
def test_profiling_uses_include_to_mark_children
|
||||
t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}", profile: true)
|
||||
t.render!
|
||||
|
||||
include_node = t.profiler[1]
|
||||
assert_equal(2, include_node.children.length)
|
||||
end
|
||||
|
||||
def test_profiling_marks_children_with_the_name_of_included_partial
|
||||
t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}", profile: true)
|
||||
t.render!
|
||||
|
||||
include_node = t.profiler[1]
|
||||
include_node.children.each do |child|
|
||||
assert_equal("a_template", child.partial)
|
||||
end
|
||||
end
|
||||
|
||||
def test_profiling_supports_multiple_templates
|
||||
t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}\n{% include 'b_template' %}", profile: true)
|
||||
t.render!
|
||||
|
||||
a_template = t.profiler[1]
|
||||
a_template.children.each do |child|
|
||||
assert_equal("a_template", child.partial)
|
||||
end
|
||||
|
||||
b_template = t.profiler[2]
|
||||
b_template.children.each do |child|
|
||||
assert_equal("b_template", child.partial)
|
||||
end
|
||||
end
|
||||
|
||||
def test_profiling_supports_rendering_the_same_partial_multiple_times
|
||||
t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}\n{% include 'a_template' %}", profile: true)
|
||||
t.render!
|
||||
|
||||
a_template1 = t.profiler[1]
|
||||
a_template1.children.each do |child|
|
||||
assert_equal("a_template", child.partial)
|
||||
end
|
||||
|
||||
a_template2 = t.profiler[2]
|
||||
a_template2.children.each do |child|
|
||||
assert_equal("a_template", child.partial)
|
||||
end
|
||||
end
|
||||
|
||||
def test_can_iterate_over_each_profiling_entry
|
||||
t = Template.parse("{{ 'a string' | upcase }}\n{% increment test %}", profile: true)
|
||||
t.render!
|
||||
|
||||
timing_count = 0
|
||||
t.profiler.each do |_timing|
|
||||
timing_count += 1
|
||||
end
|
||||
|
||||
assert_equal(2, timing_count)
|
||||
end
|
||||
|
||||
def test_profiling_marks_children_of_if_blocks
|
||||
t = Template.parse("{% if true %} {% increment test %} {{ test }} {% endif %}", profile: true)
|
||||
t.render!
|
||||
|
||||
assert_equal(1, t.profiler.length)
|
||||
assert_equal(2, t.profiler[0].children.length)
|
||||
end
|
||||
|
||||
def test_profiling_marks_children_of_for_blocks
|
||||
t = Template.parse("{% for item in collection %} {{ item }} {% endfor %}", profile: true)
|
||||
t.render!("collection" => ["one", "two"])
|
||||
|
||||
assert_equal(1, t.profiler.length)
|
||||
# Will profile each invocation of the for block
|
||||
assert_equal(2, t.profiler[0].children.length)
|
||||
end
|
||||
|
||||
def test_profiling_supports_self_time
|
||||
t = Template.parse("{% for item in collection %} {{ item }} {% endfor %}", profile: true)
|
||||
collection = [
|
||||
TestDrop.new("one"),
|
||||
TestDrop.new("two"),
|
||||
]
|
||||
output = t.render!("collection" => collection)
|
||||
assert_equal(" one two ", output)
|
||||
|
||||
leaf = t.profiler[0].children[0]
|
||||
assert_operator(leaf.self_time, :>, 0.0)
|
||||
end
|
||||
|
||||
def test_profiling_supports_total_time
|
||||
t = Template.parse("{% if true %} {{ test }} {% endif %}", profile: true)
|
||||
output = t.render!("test" => TestDrop.new("one"))
|
||||
assert_equal(" one ", output)
|
||||
|
||||
assert_operator(t.profiler[0].total_time, :>, 0.0)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,156 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class RenderProfilingTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
class ProfilingFileSystem
|
||||
def read_template_file(template_path)
|
||||
"Rendering template {% assign template_name = '#{template_path}'%}\n{{ template_name }}"
|
||||
end
|
||||
end
|
||||
|
||||
def setup
|
||||
Liquid::Template.file_system = ProfilingFileSystem.new
|
||||
end
|
||||
|
||||
def test_template_allows_flagging_profiling
|
||||
t = Template.parse("{{ 'a string' | upcase }}")
|
||||
t.render!
|
||||
|
||||
assert_nil t.profiler
|
||||
end
|
||||
|
||||
def test_parse_makes_available_simple_profiling
|
||||
t = Template.parse("{{ 'a string' | upcase }}", profile: true)
|
||||
t.render!
|
||||
|
||||
assert_equal 1, t.profiler.length
|
||||
|
||||
node = t.profiler[0]
|
||||
assert_equal " 'a string' | upcase ", node.code
|
||||
end
|
||||
|
||||
def test_render_ignores_raw_strings_when_profiling
|
||||
t = Template.parse("This is raw string\nstuff\nNewline", profile: true)
|
||||
t.render!
|
||||
|
||||
assert_equal 0, t.profiler.length
|
||||
end
|
||||
|
||||
def test_profiling_includes_line_numbers_of_liquid_nodes
|
||||
t = Template.parse("{{ 'a string' | upcase }}\n{% increment test %}", profile: true)
|
||||
t.render!
|
||||
assert_equal 2, t.profiler.length
|
||||
|
||||
# {{ 'a string' | upcase }}
|
||||
assert_equal 1, t.profiler[0].line_number
|
||||
# {{ increment test }}
|
||||
assert_equal 2, t.profiler[1].line_number
|
||||
end
|
||||
|
||||
def test_profiling_includes_line_numbers_of_included_partials
|
||||
t = Template.parse("{% include 'a_template' %}", profile: true)
|
||||
t.render!
|
||||
|
||||
included_children = t.profiler[0].children
|
||||
|
||||
# {% assign template_name = 'a_template' %}
|
||||
assert_equal 1, included_children[0].line_number
|
||||
# {{ template_name }}
|
||||
assert_equal 2, included_children[1].line_number
|
||||
end
|
||||
|
||||
def test_profiling_times_the_rendering_of_tokens
|
||||
t = Template.parse("{% include 'a_template' %}", profile: true)
|
||||
t.render!
|
||||
|
||||
node = t.profiler[0]
|
||||
refute_nil node.render_time
|
||||
end
|
||||
|
||||
def test_profiling_times_the_entire_render
|
||||
t = Template.parse("{% include 'a_template' %}", profile: true)
|
||||
t.render!
|
||||
|
||||
assert t.profiler.total_render_time >= 0, "Total render time was not calculated"
|
||||
end
|
||||
|
||||
def test_profiling_uses_include_to_mark_children
|
||||
t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}", profile: true)
|
||||
t.render!
|
||||
|
||||
include_node = t.profiler[1]
|
||||
assert_equal 2, include_node.children.length
|
||||
end
|
||||
|
||||
def test_profiling_marks_children_with_the_name_of_included_partial
|
||||
t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}", profile: true)
|
||||
t.render!
|
||||
|
||||
include_node = t.profiler[1]
|
||||
include_node.children.each do |child|
|
||||
assert_equal "a_template", child.partial
|
||||
end
|
||||
end
|
||||
|
||||
def test_profiling_supports_multiple_templates
|
||||
t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}\n{% include 'b_template' %}", profile: true)
|
||||
t.render!
|
||||
|
||||
a_template = t.profiler[1]
|
||||
a_template.children.each do |child|
|
||||
assert_equal "a_template", child.partial
|
||||
end
|
||||
|
||||
b_template = t.profiler[2]
|
||||
b_template.children.each do |child|
|
||||
assert_equal "b_template", child.partial
|
||||
end
|
||||
end
|
||||
|
||||
def test_profiling_supports_rendering_the_same_partial_multiple_times
|
||||
t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}\n{% include 'a_template' %}", profile: true)
|
||||
t.render!
|
||||
|
||||
a_template1 = t.profiler[1]
|
||||
a_template1.children.each do |child|
|
||||
assert_equal "a_template", child.partial
|
||||
end
|
||||
|
||||
a_template2 = t.profiler[2]
|
||||
a_template2.children.each do |child|
|
||||
assert_equal "a_template", child.partial
|
||||
end
|
||||
end
|
||||
|
||||
def test_can_iterate_over_each_profiling_entry
|
||||
t = Template.parse("{{ 'a string' | upcase }}\n{% increment test %}", profile: true)
|
||||
t.render!
|
||||
|
||||
timing_count = 0
|
||||
t.profiler.each do |_timing|
|
||||
timing_count += 1
|
||||
end
|
||||
|
||||
assert_equal 2, timing_count
|
||||
end
|
||||
|
||||
def test_profiling_marks_children_of_if_blocks
|
||||
t = Template.parse("{% if true %} {% increment test %} {{ test }} {% endif %}", profile: true)
|
||||
t.render!
|
||||
|
||||
assert_equal 1, t.profiler.length
|
||||
assert_equal 2, t.profiler[0].children.length
|
||||
end
|
||||
|
||||
def test_profiling_marks_children_of_for_blocks
|
||||
t = Template.parse("{% for item in collection %} {{ item }} {% endfor %}", profile: true)
|
||||
t.render!("collection" => ["one", "two"])
|
||||
|
||||
assert_equal 1, t.profiler.length
|
||||
# Will profile each invocation of the for block
|
||||
assert_equal 2, t.profiler[0].children.length
|
||||
end
|
||||
end
|
||||
@@ -16,72 +16,65 @@ class SecurityTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_no_instance_eval
|
||||
text = %( {{ '1+1' | instance_eval }} )
|
||||
text = %( {{ '1+1' | instance_eval }} )
|
||||
expected = %( 1+1 )
|
||||
|
||||
assert_equal(expected, Template.parse(text).render!(@assigns))
|
||||
assert_equal expected, Template.parse(text).render!(@assigns)
|
||||
end
|
||||
|
||||
def test_no_existing_instance_eval
|
||||
text = %( {{ '1+1' | __instance_eval__ }} )
|
||||
text = %( {{ '1+1' | __instance_eval__ }} )
|
||||
expected = %( 1+1 )
|
||||
|
||||
assert_equal(expected, Template.parse(text).render!(@assigns))
|
||||
assert_equal expected, Template.parse(text).render!(@assigns)
|
||||
end
|
||||
|
||||
def test_no_instance_eval_after_mixing_in_new_filter
|
||||
text = %( {{ '1+1' | instance_eval }} )
|
||||
text = %( {{ '1+1' | instance_eval }} )
|
||||
expected = %( 1+1 )
|
||||
|
||||
assert_equal(expected, Template.parse(text).render!(@assigns))
|
||||
assert_equal expected, Template.parse(text).render!(@assigns)
|
||||
end
|
||||
|
||||
def test_no_instance_eval_later_in_chain
|
||||
text = %( {{ '1+1' | add_one | instance_eval }} )
|
||||
text = %( {{ '1+1' | add_one | instance_eval }} )
|
||||
expected = %( 1+1 + 1 )
|
||||
|
||||
assert_equal(expected, Template.parse(text).render!(@assigns, filters: SecurityFilter))
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: SecurityFilter)
|
||||
end
|
||||
|
||||
def test_does_not_permanently_add_filters_to_symbol_table
|
||||
def test_does_not_add_filters_to_symbol_table
|
||||
current_symbols = Symbol.all_symbols
|
||||
|
||||
# MRI imprecisely marks objects found on the C stack, which can result
|
||||
# in uninitialized memory being marked. This can even result in the test failing
|
||||
# deterministically for a given compilation of ruby. Using a separate thread will
|
||||
# keep these writes of the symbol pointer on a separate stack that will be garbage
|
||||
# collected after Thread#join.
|
||||
Thread.new do
|
||||
test = %( {{ "some_string" | a_bad_filter }} )
|
||||
Template.parse(test).render!
|
||||
nil
|
||||
end.join
|
||||
test = %( {{ "some_string" | a_bad_filter }} )
|
||||
|
||||
GC.start
|
||||
template = Template.parse(test)
|
||||
assert_equal [], (Symbol.all_symbols - current_symbols)
|
||||
|
||||
assert_equal([], (Symbol.all_symbols - current_symbols))
|
||||
template.render!
|
||||
assert_equal [], (Symbol.all_symbols - current_symbols)
|
||||
end
|
||||
|
||||
def test_does_not_add_drop_methods_to_symbol_table
|
||||
current_symbols = Symbol.all_symbols
|
||||
|
||||
assigns = { 'drop' => Drop.new }
|
||||
assert_equal("", Template.parse("{{ drop.custom_method_1 }}", assigns).render!)
|
||||
assert_equal("", Template.parse("{{ drop.custom_method_2 }}", assigns).render!)
|
||||
assert_equal("", Template.parse("{{ drop.custom_method_3 }}", assigns).render!)
|
||||
assert_equal "", Template.parse("{{ drop.custom_method_1 }}", assigns).render!
|
||||
assert_equal "", Template.parse("{{ drop.custom_method_2 }}", assigns).render!
|
||||
assert_equal "", Template.parse("{{ drop.custom_method_3 }}", assigns).render!
|
||||
|
||||
assert_equal([], (Symbol.all_symbols - current_symbols))
|
||||
assert_equal [], (Symbol.all_symbols - current_symbols)
|
||||
end
|
||||
|
||||
def test_max_depth_nested_blocks_does_not_raise_exception
|
||||
depth = Liquid::Block::MAX_DEPTH
|
||||
code = "{% if true %}" * depth + "rendered" + "{% endif %}" * depth
|
||||
assert_equal("rendered", Template.parse(code).render!)
|
||||
code = "{% if true %}" * depth + "rendered" + "{% endif %}" * depth
|
||||
assert_equal "rendered", Template.parse(code).render!
|
||||
end
|
||||
|
||||
def test_more_than_max_depth_nested_blocks_raises_exception
|
||||
depth = Liquid::Block::MAX_DEPTH + 1
|
||||
code = "{% if true %}" * depth + "rendered" + "{% endif %}" * depth
|
||||
code = "{% if true %}" * depth + "rendered" + "{% endif %}" * depth
|
||||
assert_raises(Liquid::StackLevelError) do
|
||||
Template.parse(code).render!
|
||||
end
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,59 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class TagDisableableTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
module RenderTagName
|
||||
def render(_context)
|
||||
tag_name
|
||||
end
|
||||
end
|
||||
|
||||
class Custom < Tag
|
||||
prepend Liquid::Tag::Disableable
|
||||
include RenderTagName
|
||||
end
|
||||
|
||||
class Custom2 < Tag
|
||||
prepend Liquid::Tag::Disableable
|
||||
include RenderTagName
|
||||
end
|
||||
|
||||
class DisableCustom < Block
|
||||
disable_tags "custom"
|
||||
end
|
||||
|
||||
class DisableBoth < Block
|
||||
disable_tags "custom", "custom2"
|
||||
end
|
||||
|
||||
def test_block_tag_disabling_nested_tag
|
||||
with_disableable_tags do
|
||||
with_custom_tag('disable', DisableCustom) do
|
||||
output = Template.parse('{% disable %}{% custom %};{% custom2 %}{% enddisable %}').render
|
||||
assert_equal('Liquid error: custom usage is not allowed in this context;custom2', output)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_block_tag_disabling_multiple_nested_tags
|
||||
with_disableable_tags do
|
||||
with_custom_tag('disable', DisableBoth) do
|
||||
output = Template.parse('{% disable %}{% custom %};{% custom2 %}{% enddisable %}').render
|
||||
assert_equal('Liquid error: custom usage is not allowed in this context;Liquid error: custom2 usage is not allowed in this context', output)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def with_disableable_tags
|
||||
with_custom_tag('custom', Custom) do
|
||||
with_custom_tag('custom2', Custom2) do
|
||||
yield
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user