mirror of
https://github.com/Shopify/liquid.git
synced 2026-10-02 08:35:14 -07:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b6344f407 | ||
|
|
a372baa9cf | ||
|
|
60075ddda2 |
@@ -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,37 +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: 3.0, allowed-failure: false } # minimum supported
|
|
||||||
- { ruby: 3.2, allowed-failure: false }
|
|
||||||
- { ruby: 3.3, allowed-failure: false }
|
|
||||||
- { ruby: 3.4, allowed-failure: false } # latest
|
|
||||||
- { ruby: ruby-head, allowed-failure: false }
|
|
||||||
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
|
|
||||||
bundler: latest
|
|
||||||
- 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:
|
|
||||||
bundler-cache: true
|
|
||||||
- run: bundle exec rake memory_profile:run
|
|
||||||
+2
-1
@@ -4,6 +4,7 @@
|
|||||||
pkg
|
pkg
|
||||||
*.rbc
|
*.rbc
|
||||||
.rvmrc
|
.rvmrc
|
||||||
|
.ruby-version
|
||||||
|
Gemfile.lock
|
||||||
.bundle
|
.bundle
|
||||||
.byebug_history
|
.byebug_history
|
||||||
Gemfile.lock
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+2
-13
@@ -1,7 +1,5 @@
|
|||||||
inherit_gem:
|
|
||||||
rubocop-shopify: rubocop.yml
|
|
||||||
|
|
||||||
inherit_from:
|
inherit_from:
|
||||||
|
- 'https://shopify.github.io/ruby-style-guide/rubocop.yml'
|
||||||
- .rubocop_todo.yml
|
- .rubocop_todo.yml
|
||||||
|
|
||||||
require: rubocop-performance
|
require: rubocop-performance
|
||||||
@@ -10,19 +8,10 @@ Performance:
|
|||||||
Enabled: true
|
Enabled: true
|
||||||
|
|
||||||
AllCops:
|
AllCops:
|
||||||
NewCops: disable
|
TargetRubyVersion: 2.4
|
||||||
SuggestExtensions: false
|
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'vendor/bundle/**/*'
|
- 'vendor/bundle/**/*'
|
||||||
|
|
||||||
Naming/MethodName:
|
Naming/MethodName:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'example/server/liquid_servlet.rb'
|
- 'example/server/liquid_servlet.rb'
|
||||||
|
|
||||||
Style/ClassMethodsDefinitions:
|
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
# liquid filter calls were being mistaken to be calls on arrays
|
|
||||||
Style/ConcatArrayLiterals:
|
|
||||||
Exclude:
|
|
||||||
- 'test/integration/standard_filter_test.rb'
|
|
||||||
|
|||||||
+17
-149
@@ -1,84 +1,25 @@
|
|||||||
# This configuration was generated by
|
# This configuration was generated by
|
||||||
# `rubocop --auto-gen-config`
|
# `rubocop --auto-gen-config`
|
||||||
# on 2022-05-18 19:25:47 UTC using RuboCop version 1.29.1.
|
# on 2019-09-11 06:34:25 +1000 using RuboCop version 0.74.0.
|
||||||
# The point is for the user to remove these configuration records
|
# The point is for the user to remove these configuration records
|
||||||
# one by one as the offenses are removed from the code base.
|
# one by one as the offenses are removed from the code base.
|
||||||
# Note that changes in the inspected code, or installation of new
|
# Note that changes in the inspected code, or installation of new
|
||||||
# versions of RuboCop, may require this file to be generated again.
|
# versions of RuboCop, may require this file to be generated again.
|
||||||
|
|
||||||
# Offense count: 1
|
# Offense count: 2
|
||||||
# This cop supports safe auto-correction (--auto-correct).
|
# Cop supports --auto-correct.
|
||||||
# Configuration parameters: TreatCommentsAsGroupSeparators, ConsiderPunctuation, Include.
|
# Configuration parameters: EnforcedStyle.
|
||||||
# Include: **/*.gemspec
|
# SupportedStyles: runtime_error, standard_error
|
||||||
Gemspec/OrderedDependencies:
|
Lint/InheritException:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'liquid.gemspec'
|
- 'lib/liquid/interrupts.rb'
|
||||||
|
|
||||||
# Offense count: 6
|
# Offense count: 98
|
||||||
# This cop supports safe auto-correction (--auto-correct).
|
# Cop supports --auto-correct.
|
||||||
Layout/ClosingHeredocIndentation:
|
# Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
|
||||||
Exclude:
|
# URISchemes: http, https
|
||||||
- 'test/integration/tags/for_tag_test.rb'
|
Layout/LineLength:
|
||||||
|
Max: 294
|
||||||
# Offense count: 34
|
|
||||||
# This cop supports safe auto-correction (--auto-correct).
|
|
||||||
Layout/EmptyLineAfterGuardClause:
|
|
||||||
Exclude:
|
|
||||||
- 'lib/liquid/block.rb'
|
|
||||||
- 'lib/liquid/block_body.rb'
|
|
||||||
- 'lib/liquid/context.rb'
|
|
||||||
- 'lib/liquid/drop.rb'
|
|
||||||
- 'lib/liquid/lexer.rb'
|
|
||||||
- 'lib/liquid/parser.rb'
|
|
||||||
- 'lib/liquid/profiler/hooks.rb'
|
|
||||||
- 'lib/liquid/standardfilters.rb'
|
|
||||||
- 'lib/liquid/tags/for.rb'
|
|
||||||
- 'lib/liquid/tags/if.rb'
|
|
||||||
- 'lib/liquid/utils.rb'
|
|
||||||
- 'lib/liquid/variable.rb'
|
|
||||||
- 'lib/liquid/variable_lookup.rb'
|
|
||||||
- 'performance/shopify/money_filter.rb'
|
|
||||||
- 'performance/shopify/paginate.rb'
|
|
||||||
|
|
||||||
# Offense count: 8
|
|
||||||
# This cop supports safe auto-correction (--auto-correct).
|
|
||||||
# Configuration parameters: AllowAliasSyntax, AllowedMethods.
|
|
||||||
# AllowedMethods: alias_method, public, protected, private
|
|
||||||
Layout/EmptyLinesAroundAttributeAccessor:
|
|
||||||
Exclude:
|
|
||||||
- 'lib/liquid/template.rb'
|
|
||||||
- 'test/integration/filter_test.rb'
|
|
||||||
- 'test/integration/tags/include_tag_test.rb'
|
|
||||||
- 'test/unit/strainer_template_unit_test.rb'
|
|
||||||
|
|
||||||
# Offense count: 17
|
|
||||||
# This cop supports safe auto-correction (--auto-correct).
|
|
||||||
# Configuration parameters: EnforcedStyle, IndentationWidth.
|
|
||||||
# SupportedStyles: aligned, indented
|
|
||||||
Layout/LineEndStringConcatenationIndentation:
|
|
||||||
Exclude:
|
|
||||||
- 'test/integration/tags/for_tag_test.rb'
|
|
||||||
- 'test/integration/tags/increment_tag_test.rb'
|
|
||||||
|
|
||||||
# Offense count: 1
|
|
||||||
# This cop supports safe auto-correction (--auto-correct).
|
|
||||||
# Configuration parameters: EnforcedStyle, IndentationWidth.
|
|
||||||
# SupportedStyles: aligned, indented
|
|
||||||
Layout/MultilineOperationIndentation:
|
|
||||||
Exclude:
|
|
||||||
- 'lib/liquid/expression.rb'
|
|
||||||
|
|
||||||
# Offense count: 9
|
|
||||||
Lint/MissingSuper:
|
|
||||||
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: 44
|
# Offense count: 44
|
||||||
Naming/ConstantName:
|
Naming/ConstantName:
|
||||||
@@ -91,6 +32,7 @@ Naming/ConstantName:
|
|||||||
- 'lib/liquid/tags/cycle.rb'
|
- 'lib/liquid/tags/cycle.rb'
|
||||||
- 'lib/liquid/tags/for.rb'
|
- 'lib/liquid/tags/for.rb'
|
||||||
- 'lib/liquid/tags/if.rb'
|
- 'lib/liquid/tags/if.rb'
|
||||||
|
- 'lib/liquid/tags/include.rb'
|
||||||
- 'lib/liquid/tags/raw.rb'
|
- 'lib/liquid/tags/raw.rb'
|
||||||
- 'lib/liquid/tags/table_row.rb'
|
- 'lib/liquid/tags/table_row.rb'
|
||||||
- 'lib/liquid/variable.rb'
|
- 'lib/liquid/variable.rb'
|
||||||
@@ -98,83 +40,9 @@ Naming/ConstantName:
|
|||||||
- 'performance/shopify/paginate.rb'
|
- 'performance/shopify/paginate.rb'
|
||||||
- 'test/integration/tags/include_tag_test.rb'
|
- 'test/integration/tags/include_tag_test.rb'
|
||||||
|
|
||||||
# Offense count: 9
|
# Offense count: 5
|
||||||
# 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
|
|
||||||
Style/ClassVars:
|
Style/ClassVars:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'lib/liquid/condition.rb'
|
- 'lib/liquid/condition.rb'
|
||||||
|
- 'lib/liquid/strainer.rb'
|
||||||
# Offense count: 3
|
- 'lib/liquid/template.rb'
|
||||||
# This cop supports safe auto-correction (--auto-correct).
|
|
||||||
Style/ExplicitBlockArgument:
|
|
||||||
Exclude:
|
|
||||||
- 'test/integration/context_test.rb'
|
|
||||||
- 'test/integration/tag/disableable_test.rb'
|
|
||||||
- 'test/integration/tags/for_tag_test.rb'
|
|
||||||
|
|
||||||
# Offense count: 2982
|
|
||||||
# This cop supports safe auto-correction (--auto-correct).
|
|
||||||
# Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline.
|
|
||||||
# SupportedStyles: single_quotes, double_quotes
|
|
||||||
Style/StringLiterals:
|
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
# Offense count: 20
|
|
||||||
# This cop supports safe auto-correction (--auto-correct).
|
|
||||||
# Configuration parameters: EnforcedStyle.
|
|
||||||
# SupportedStyles: single_quotes, double_quotes
|
|
||||||
Style/StringLiteralsInInterpolation:
|
|
||||||
Exclude:
|
|
||||||
- 'lib/liquid/condition.rb'
|
|
||||||
- 'lib/liquid/strainer_template.rb'
|
|
||||||
- 'lib/liquid/tag/disableable.rb'
|
|
||||||
- 'performance/shopify/shop_filter.rb'
|
|
||||||
- 'performance/shopify/tag_filter.rb'
|
|
||||||
|
|
||||||
# Offense count: 6
|
|
||||||
# This cop supports safe auto-correction (--auto-correct).
|
|
||||||
# Configuration parameters: EnforcedStyleForMultiline.
|
|
||||||
# SupportedStylesForMultiline: comma, consistent_comma, no_comma
|
|
||||||
Style/TrailingCommaInArrayLiteral:
|
|
||||||
Exclude:
|
|
||||||
- 'example/server/example_servlet.rb'
|
|
||||||
- 'lib/liquid/condition.rb'
|
|
||||||
- 'test/integration/context_test.rb'
|
|
||||||
- 'test/integration/standard_filter_test.rb'
|
|
||||||
- 'test/unit/parse_tree_visitor_test.rb'
|
|
||||||
|
|
||||||
# Offense count: 1
|
|
||||||
# This cop supports safe auto-correction (--auto-correct).
|
|
||||||
# Configuration parameters: EnforcedStyleForMultiline.
|
|
||||||
# SupportedStylesForMultiline: comma, consistent_comma, no_comma
|
|
||||||
Style/TrailingCommaInHashLiteral:
|
|
||||||
Exclude:
|
|
||||||
- 'lib/liquid/expression.rb'
|
|
||||||
|
|
||||||
# Offense count: 19
|
|
||||||
# This cop supports safe auto-correction (--auto-correct).
|
|
||||||
# Configuration parameters: EnforcedStyle, MinSize, WordRegex.
|
|
||||||
# SupportedStyles: percent, brackets
|
|
||||||
Style/WordArray:
|
|
||||||
Exclude:
|
|
||||||
- 'lib/liquid/tags/if.rb'
|
|
||||||
- 'liquid.gemspec'
|
|
||||||
- 'test/integration/assign_test.rb'
|
|
||||||
- 'test/integration/context_test.rb'
|
|
||||||
- 'test/integration/drop_test.rb'
|
|
||||||
- 'test/integration/standard_filter_test.rb'
|
|
||||||
|
|
||||||
# Offense count: 117
|
|
||||||
# This cop supports safe auto-correction (--auto-correct).
|
|
||||||
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns, IgnoredPatterns.
|
|
||||||
# URISchemes: http, https
|
|
||||||
Layout/LineLength:
|
|
||||||
Max: 260
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
3.3.6
|
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
language: ruby
|
||||||
|
cache: bundler
|
||||||
|
|
||||||
|
rvm:
|
||||||
|
- 2.4
|
||||||
|
- 2.5
|
||||||
|
- 2.6
|
||||||
|
- &latest_ruby 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
|
||||||
@@ -5,7 +5,6 @@
|
|||||||
* Bugfixes
|
* Bugfixes
|
||||||
* Performance improvements
|
* Performance improvements
|
||||||
* Features that are likely to be useful to the majority of Liquid users
|
* Features that are likely to be useful to the majority of Liquid users
|
||||||
* Documentation updates that are concise and likely to be useful to the majority of Liquid users
|
|
||||||
|
|
||||||
## Things we won't merge
|
## Things we won't merge
|
||||||
|
|
||||||
@@ -15,22 +14,12 @@
|
|||||||
* Features that can easily be implemented on top of Liquid (for example as a custom filter or custom filesystem)
|
* Features that can easily be implemented on top of Liquid (for example as a custom filter or custom filesystem)
|
||||||
* Code that does not include tests
|
* Code that does not include tests
|
||||||
* Code that breaks existing tests
|
* Code that breaks existing tests
|
||||||
* Documentation changes that are verbose, incorrect or not important to most people (we want to keep it simple and easy to understand)
|
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
* [Sign the CLA](https://cla.shopify.com/) if you haven't already
|
|
||||||
* Fork the Liquid repository
|
* Fork the Liquid repository
|
||||||
* Create a new branch in your fork
|
* Create a new branch in your fork
|
||||||
* For updating [Liquid documentation](https://shopify.github.io/liquid/), create it from `gh-pages` branch. (You can skip tests.)
|
|
||||||
* If it makes sense, add tests for your code and/or run a performance benchmark
|
* If it makes sense, add tests for your code and/or run a performance benchmark
|
||||||
* Make sure all tests pass (`bundle exec rake`)
|
* Make sure all tests pass (`bundle exec rake`)
|
||||||
* Create a pull request
|
* Create a pull request
|
||||||
|
|
||||||
## Releasing
|
|
||||||
|
|
||||||
* Bump the version in `lib/liquid/version.rb`
|
|
||||||
* Update the `History.md` file
|
|
||||||
* Open a PR like [this one](https://github.com/Shopify/liquid/pull/1894) and merge it to `main`
|
|
||||||
* Create a new release using the [GitHub UI](https://github.com/Shopify/liquid/releases/new)
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,13 +7,10 @@ end
|
|||||||
|
|
||||||
gemspec
|
gemspec
|
||||||
|
|
||||||
gem "base64"
|
|
||||||
|
|
||||||
group :benchmark, :test do
|
group :benchmark, :test do
|
||||||
gem 'benchmark-ips'
|
gem 'benchmark-ips'
|
||||||
gem 'memory_profiler'
|
gem 'memory_profiler'
|
||||||
gem 'terminal-table'
|
gem 'terminal-table'
|
||||||
gem "lru_redux"
|
|
||||||
|
|
||||||
install_if -> { RUBY_PLATFORM !~ /mingw|mswin|java/ && RUBY_ENGINE != 'truffleruby' } do
|
install_if -> { RUBY_PLATFORM !~ /mingw|mswin|java/ && RUBY_ENGINE != 'truffleruby' } do
|
||||||
gem 'stackprof'
|
gem 'stackprof'
|
||||||
@@ -21,7 +18,10 @@ group :benchmark, :test do
|
|||||||
end
|
end
|
||||||
|
|
||||||
group :test do
|
group :test do
|
||||||
gem 'rubocop', '~> 1.61.0'
|
gem 'rubocop', '~> 0.78.0', require: false
|
||||||
gem 'rubocop-shopify', '~> 2.12.0', require: false
|
|
||||||
gem 'rubocop-performance', require: false
|
gem 'rubocop-performance', require: false
|
||||||
|
|
||||||
|
platform :mri, :truffleruby do
|
||||||
|
gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'master'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+3
-139
@@ -1,146 +1,10 @@
|
|||||||
# Liquid Change Log
|
# Liquid Change Log
|
||||||
|
|
||||||
## 5.8.0 (unreleased)
|
### Unreleased
|
||||||
|
|
||||||
## 5.7.0 2025-01-16
|
* Split Strainer class as a factory and a template (#1208) [Thierry Joyal]
|
||||||
|
|
||||||
### Features
|
|
||||||
* Add `find`, `find_index`, `has`, and `reject` filters to arrays
|
|
||||||
* Compatibility with Ruby 3.4
|
|
||||||
|
|
||||||
## 5.6.4 2025-01-14
|
|
||||||
|
|
||||||
### Fixes
|
|
||||||
* Add a default `string_scanner` to avoid errors with `Liquid::VariableLookup.parse("foo.bar")` [Ian Ker-Seymer]
|
|
||||||
|
|
||||||
## 5.6.3 2025-01-13
|
|
||||||
* Remove `lru_redux` dependency [Michael Go]
|
|
||||||
|
|
||||||
## 5.6.2 2025-01-13
|
|
||||||
|
|
||||||
### Fixes
|
|
||||||
* Preserve the old behavior of requiring floats to start with a digit [Michael Go]
|
|
||||||
|
|
||||||
## 5.6.1 2025-01-13
|
|
||||||
|
|
||||||
### Performance improvements
|
|
||||||
* Faster Expression parser / Tokenizer with StringScanner [Michael Go]
|
|
||||||
|
|
||||||
## 5.6.0 2024-12-19
|
|
||||||
|
|
||||||
### Architectural changes
|
|
||||||
* Added new `Environment` class to manage configuration and state that was previously stored in `Template` [Ian Ker-Seymer]
|
|
||||||
* Moved tag registration from `Template` to `Environment` [Ian Ker-Seymer]
|
|
||||||
* Removed `StrainerFactory` in favor of `Environment`-based strainer creation [Ian Ker-Seymer]
|
|
||||||
* Consolidated standard tags into a new `Tags` module with `STANDARD_TAGS` constant [Ian Ker-Seymer]
|
|
||||||
|
|
||||||
### Performance improvements
|
|
||||||
* Optimized `Lexer` with a new `Lexer2` implementation using jump tables for faster tokenization, requires Ruby 3.4 [Ian Ker-Seymer]
|
|
||||||
* Improved variable rendering with specialized handling for different types [Michael Go]
|
|
||||||
* Reduced array allocations by using frozen empty constants [Michael Go]
|
|
||||||
|
|
||||||
### API changes
|
|
||||||
* Deprecated several `Template` class methods in favor of `Environment` methods [Ian Ker-Seymer]
|
|
||||||
* Added deprecation warnings system [Ian Ker-Seymer]
|
|
||||||
* Changed how filters and tags are registered to use Environment [Ian Ker-Seymer]
|
|
||||||
|
|
||||||
### Fixes
|
|
||||||
* Fixed table row handling of break interrupts [Alex Coco]
|
|
||||||
* Improved variable output handling for arrays [Ian Ker-Seymer]
|
|
||||||
* Fix Tokenizer to handle null source value (#1873) [Bahar Pourazar]
|
|
||||||
|
|
||||||
## 5.5.0 2024-03-21
|
|
||||||
|
|
||||||
Please reference the GitHub release for more information.
|
|
||||||
|
|
||||||
## 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]
|
* Remove handling of a nil context in the Strainer class (#1218) [Thierry Joyal]
|
||||||
* Handle `BlockBody#blank?` at parse time (#1287) [Dylan Thacker-Smith]
|
* StaticRegisters#fetch to raise on missing key (#1250) [Thierry Joyal]
|
||||||
* 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
|
## 4.0.3 / 2019-03-12
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
[](http://inch-ci.org/github/Shopify/liquid)
|
||||||
|
|
||||||
# Liquid template engine
|
# Liquid template engine
|
||||||
|
|
||||||
* [Contributing guidelines](CONTRIBUTING.md)
|
* [Contributing guidelines](CONTRIBUTING.md)
|
||||||
* [Version history](History.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)
|
* [Liquid Wiki at GitHub](https://github.com/Shopify/liquid/wiki)
|
||||||
* [Website](http://liquidmarkup.org/)
|
* [Website](http://liquidmarkup.org/)
|
||||||
|
|
||||||
@@ -52,66 +52,24 @@ For standard use you can just pass it the content of a file and call render with
|
|||||||
@template.render('name' => 'tobi') # => "hi tobi"
|
@template.render('name' => 'tobi') # => "hi tobi"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Concept of Environments
|
|
||||||
|
|
||||||
In Liquid, a "Environment" is a scoped environment that encapsulates custom tags, filters, and other configurations. This allows you to define and isolate different sets of functionality for different contexts, avoiding global overrides that can lead to conflicts and unexpected behavior.
|
|
||||||
|
|
||||||
By using environments, you can:
|
|
||||||
|
|
||||||
1. **Encapsulate Logic**: Keep the logic for different parts of your application separate.
|
|
||||||
2. **Avoid Conflicts**: Prevent custom tags and filters from clashing with each other.
|
|
||||||
3. **Improve Maintainability**: Make it easier to manage and understand the scope of customizations.
|
|
||||||
4. **Enhance Security**: Limit the availability of certain tags and filters to specific contexts.
|
|
||||||
|
|
||||||
We encourage the use of Environments over globally overriding things because it promotes better software design principles such as modularity, encapsulation, and separation of concerns.
|
|
||||||
|
|
||||||
Here's an example of how you can define and use Environments in Liquid:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
user_environment = Liquid::Environment.build do |environment|
|
|
||||||
environment.register_tag("renderobj", RenderObjTag)
|
|
||||||
end
|
|
||||||
|
|
||||||
Liquid::Template.parse(<<~LIQUID, environment: user_environment)
|
|
||||||
{% renderobj src: "path/to/model.obj" %}
|
|
||||||
LIQUID
|
|
||||||
```
|
|
||||||
|
|
||||||
In this example, `RenderObjTag` is a custom tag that is only available within the `user_environment`.
|
|
||||||
|
|
||||||
Similarly, you can define another environment for a different context, such as email templates:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
email_environment = Liquid::Environment.build do |environment|
|
|
||||||
environment.register_tag("unsubscribe_footer", UnsubscribeFooter)
|
|
||||||
end
|
|
||||||
|
|
||||||
Liquid::Template.parse(<<~LIQUID, environment: email_environment)
|
|
||||||
{% unsubscribe_footer %}
|
|
||||||
LIQUID
|
|
||||||
```
|
|
||||||
|
|
||||||
By using Environments, you ensure that custom tags and filters are only available in the contexts where they are needed, making your Liquid templates more robust and easier to manage. For smaller projects, a global environment is available via `Liquid::Environment.default`.
|
|
||||||
|
|
||||||
### Error Modes
|
### Error Modes
|
||||||
|
|
||||||
Setting the error mode of Liquid lets you specify how strictly you want your templates to be interpreted.
|
Setting the error mode of Liquid lets you specify how strictly you want your templates to be interpreted.
|
||||||
Normally the parser is very lax and will accept almost anything without error. Unfortunately this can make
|
Normally the parser is very lax and will accept almost anything without error. Unfortunately this can make
|
||||||
it very hard to debug and can lead to unexpected behaviour.
|
it very hard to debug and can lead to unexpected behaviour.
|
||||||
|
|
||||||
Liquid also comes with a stricter parser that can be used when editing templates to give better error messages
|
Liquid also comes with a stricter parser that can be used when editing templates to give better error messages
|
||||||
when templates are invalid. You can enable this new parser like this:
|
when templates are invalid. You can enable this new parser like this:
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
Liquid::Environment.default.error_mode = :strict
|
Liquid::Template.error_mode = :strict # Raises a SyntaxError when invalid syntax is used
|
||||||
Liquid::Environment.default.error_mode = :strict # Raises a SyntaxError when invalid syntax is used
|
Liquid::Template.error_mode = :warn # Adds errors to template.errors but continues as normal
|
||||||
Liquid::Environment.default.error_mode = :warn # Adds strict errors to template.errors but continues as normal
|
Liquid::Template.error_mode = :lax # The default mode, accepts almost anything.
|
||||||
Liquid::Environment.default.error_mode = :lax # The default mode, accepts almost anything.
|
|
||||||
```
|
```
|
||||||
|
|
||||||
If you want to set the error mode only on specific templates you can pass `:error_mode` as an option to `parse`:
|
If you want to set the error mode only on specific templates you can pass `:error_mode` as an option to `parse`:
|
||||||
```ruby
|
```ruby
|
||||||
Liquid::Template.parse(source, error_mode: :strict)
|
Liquid::Template.parse(source, :error_mode => :strict)
|
||||||
```
|
```
|
||||||
This is useful for doing things like enabling strict mode only in the theme editor.
|
This is useful for doing things like enabling strict mode only in the theme editor.
|
||||||
|
|
||||||
@@ -153,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.
|
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,17 +9,11 @@ task(default: [:test, :rubocop])
|
|||||||
|
|
||||||
desc('run test suite with default parser')
|
desc('run test suite with default parser')
|
||||||
Rake::TestTask.new(:base_test) do |t|
|
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.test_files = FileList['test/{integration,unit}/**/*_test.rb']
|
||||||
t.verbose = false
|
t.verbose = false
|
||||||
end
|
end
|
||||||
|
|
||||||
Rake::TestTask.new(:integration_test) do |t|
|
|
||||||
t.libs << 'lib' << 'test'
|
|
||||||
t.test_files = FileList['test/integration/**/*_test.rb']
|
|
||||||
t.verbose = false
|
|
||||||
end
|
|
||||||
|
|
||||||
desc('run test suite with warn error mode')
|
desc('run test suite with warn error mode')
|
||||||
task :warn_test do
|
task :warn_test do
|
||||||
ENV['LIQUID_PARSER_MODE'] = 'warn'
|
ENV['LIQUID_PARSER_MODE'] = 'warn'
|
||||||
@@ -43,13 +37,15 @@ task :test do
|
|||||||
Rake::Task['base_test'].invoke
|
Rake::Task['base_test'].invoke
|
||||||
|
|
||||||
if RUBY_ENGINE == 'ruby' || RUBY_ENGINE == 'truffleruby'
|
if RUBY_ENGINE == 'ruby' || RUBY_ENGINE == 'truffleruby'
|
||||||
|
ENV['LIQUID_C'] = '1'
|
||||||
|
|
||||||
ENV['LIQUID_PARSER_MODE'] = 'lax'
|
ENV['LIQUID_PARSER_MODE'] = 'lax'
|
||||||
Rake::Task['integration_test'].reenable
|
Rake::Task['base_test'].reenable
|
||||||
Rake::Task['integration_test'].invoke
|
Rake::Task['base_test'].invoke
|
||||||
|
|
||||||
ENV['LIQUID_PARSER_MODE'] = 'strict'
|
ENV['LIQUID_PARSER_MODE'] = 'strict'
|
||||||
Rake::Task['integration_test'].reenable
|
Rake::Task['base_test'].reenable
|
||||||
Rake::Task['integration_test'].invoke
|
Rake::Task['base_test'].invoke
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -71,7 +67,7 @@ end
|
|||||||
|
|
||||||
namespace :benchmark do
|
namespace :benchmark do
|
||||||
desc "Run the liquid benchmark with lax parsing"
|
desc "Run the liquid benchmark with lax parsing"
|
||||||
task :lax do
|
task :run do
|
||||||
ruby "./performance/benchmark.rb lax"
|
ruby "./performance/benchmark.rb lax"
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -79,33 +75,6 @@ namespace :benchmark do
|
|||||||
task :strict do
|
task :strict do
|
||||||
ruby "./performance/benchmark.rb strict"
|
ruby "./performance/benchmark.rb strict"
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "Run the liquid benchmark with both lax and strict parsing"
|
|
||||||
task run: [:lax, :strict]
|
|
||||||
|
|
||||||
desc "Run unit benchmarks"
|
|
||||||
namespace :unit do
|
|
||||||
task :all do
|
|
||||||
Dir["./performance/unit/*_benchmark.rb"].each do |file|
|
|
||||||
puts "🧪 Running #{file}"
|
|
||||||
ruby file
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
task :lexer do
|
|
||||||
Dir["./performance/unit/lexer_benchmark.rb"].each do |file|
|
|
||||||
puts "🧪 Running #{file}"
|
|
||||||
ruby file
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
task :expression do
|
|
||||||
Dir["./performance/unit/expression_benchmark.rb"].each do |file|
|
|
||||||
puts "🧪 Running #{file}"
|
|
||||||
ruby file
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
namespace :profile do
|
namespace :profile do
|
||||||
|
|||||||
@@ -30,18 +30,14 @@ class Servlet < LiquidServlet
|
|||||||
private
|
private
|
||||||
|
|
||||||
def products_list
|
def products_list
|
||||||
[
|
[{ 'name' => 'Arbor Draft', 'price' => 39900, 'description' => 'the *arbor draft* is a excellent product' },
|
||||||
{ 'name' => 'Arbor Draft', 'price' => 39900, 'description' => 'the *arbor draft* is a excellent product' },
|
{ 'name' => 'Arbor Element', 'price' => 40000, 'description' => 'the *arbor element* rocks for freestyling' },
|
||||||
{ 'name' => 'Arbor Element', 'price' => 40000, 'description' => 'the *arbor element* rocks for freestyling' },
|
{ 'name' => 'Arbor Diamond', 'price' => 59900, 'description' => 'the *arbor diamond* is a made up product because im obsessed with arbor and have no creativity' }]
|
||||||
{ 'name' => 'Arbor Diamond', 'price' => 59900, 'description' => 'the *arbor diamond* is a made up product because im obsessed with arbor and have no creativity' }
|
|
||||||
]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def more_products_list
|
def more_products_list
|
||||||
[
|
[{ 'name' => 'Arbor Catalyst', 'price' => 39900, 'description' => 'the *arbor catalyst* is an advanced drop-through for freestyle and flatground performance and versatility' },
|
||||||
{ 'name' => 'Arbor Catalyst', 'price' => 39900, 'description' => 'the *arbor catalyst* is an advanced drop-through for freestyle and flatground performance and versatility' },
|
{ 'name' => 'Arbor Fish', 'price' => 40000, 'description' => 'the *arbor fish* is a compact pin that features an extended wheelbase and time-honored teardrop shape' }]
|
||||||
{ 'name' => 'Arbor Fish', 'price' => 40000, 'description' => 'the *arbor fish* is a compact pin that features an extended wheelbase and time-honored teardrop shape' }
|
|
||||||
]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def description
|
def description
|
||||||
|
|||||||
+18
-19
@@ -21,8 +21,6 @@
|
|||||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
require "strscan"
|
|
||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
FilterSeparator = /\|/
|
FilterSeparator = /\|/
|
||||||
ArgumentSeparator = ','
|
ArgumentSeparator = ','
|
||||||
@@ -31,7 +29,6 @@ module Liquid
|
|||||||
WhitespaceControl = '-'
|
WhitespaceControl = '-'
|
||||||
TagStart = /\{\%/
|
TagStart = /\{\%/
|
||||||
TagEnd = /\%\}/
|
TagEnd = /\%\}/
|
||||||
TagName = /#|\w+/
|
|
||||||
VariableSignature = /\(?[\w\-\.\[\]]\)?/
|
VariableSignature = /\(?[\w\-\.\[\]]\)?/
|
||||||
VariableSegment = /[\w\-]/
|
VariableSegment = /[\w\-]/
|
||||||
VariableStart = /\{\{/
|
VariableStart = /\{\{/
|
||||||
@@ -39,28 +36,18 @@ module Liquid
|
|||||||
VariableIncompleteEnd = /\}\}?/
|
VariableIncompleteEnd = /\}\}?/
|
||||||
QuotedString = /"[^"]*"|'[^']*'/
|
QuotedString = /"[^"]*"|'[^']*'/
|
||||||
QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o
|
QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o
|
||||||
TagAttributes = /(\w[\w-]*)\s*\:\s*(#{QuotedFragment})/o
|
TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/o
|
||||||
AnyStartingTag = /#{TagStart}|#{VariableStart}/o
|
AnyStartingTag = /#{TagStart}|#{VariableStart}/o
|
||||||
PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om
|
PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om
|
||||||
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om
|
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om
|
||||||
VariableParser = /\[(?>[^\[\]]+|\g<0>)*\]|#{VariableSegment}+\??/o
|
VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o
|
||||||
|
|
||||||
RAISE_EXCEPTION_LAMBDA = ->(_e) { raise }
|
singleton_class.send(:attr_accessor, :cache_classes)
|
||||||
HAS_STRING_SCANNER_SCAN_BYTE = StringScanner.instance_methods.include?(:scan_byte)
|
self.cache_classes = true
|
||||||
end
|
end
|
||||||
|
|
||||||
require "liquid/version"
|
require "liquid/version"
|
||||||
require "liquid/deprecations"
|
|
||||||
require "liquid/const"
|
|
||||||
require 'liquid/standardfilters'
|
|
||||||
require 'liquid/file_system'
|
|
||||||
require 'liquid/parser_switching'
|
|
||||||
require 'liquid/tag'
|
|
||||||
require 'liquid/block'
|
|
||||||
require 'liquid/parse_tree_visitor'
|
require 'liquid/parse_tree_visitor'
|
||||||
require 'liquid/interrupts'
|
|
||||||
require 'liquid/tags'
|
|
||||||
require "liquid/environment"
|
|
||||||
require 'liquid/lexer'
|
require 'liquid/lexer'
|
||||||
require 'liquid/parser'
|
require 'liquid/parser'
|
||||||
require 'liquid/i18n'
|
require 'liquid/i18n'
|
||||||
@@ -70,22 +57,34 @@ require 'liquid/forloop_drop'
|
|||||||
require 'liquid/extensions'
|
require 'liquid/extensions'
|
||||||
require 'liquid/errors'
|
require 'liquid/errors'
|
||||||
require 'liquid/interrupts'
|
require 'liquid/interrupts'
|
||||||
|
require 'liquid/strainer_factory'
|
||||||
require 'liquid/strainer_template'
|
require 'liquid/strainer_template'
|
||||||
|
require 'liquid/expression'
|
||||||
require 'liquid/context'
|
require 'liquid/context'
|
||||||
|
require 'liquid/parser_switching'
|
||||||
require 'liquid/tag'
|
require 'liquid/tag'
|
||||||
|
require 'liquid/tag/disabler'
|
||||||
|
require 'liquid/tag/disableable'
|
||||||
|
require 'liquid/block'
|
||||||
require 'liquid/block_body'
|
require 'liquid/block_body'
|
||||||
require 'liquid/document'
|
require 'liquid/document'
|
||||||
require 'liquid/variable'
|
require 'liquid/variable'
|
||||||
require 'liquid/variable_lookup'
|
require 'liquid/variable_lookup'
|
||||||
require 'liquid/range_lookup'
|
require 'liquid/range_lookup'
|
||||||
|
require 'liquid/file_system'
|
||||||
require 'liquid/resource_limits'
|
require 'liquid/resource_limits'
|
||||||
require 'liquid/expression'
|
|
||||||
require 'liquid/template'
|
require 'liquid/template'
|
||||||
|
require 'liquid/standardfilters'
|
||||||
require 'liquid/condition'
|
require 'liquid/condition'
|
||||||
require 'liquid/utils'
|
require 'liquid/utils'
|
||||||
require 'liquid/tokenizer'
|
require 'liquid/tokenizer'
|
||||||
require 'liquid/parse_context'
|
require 'liquid/parse_context'
|
||||||
require 'liquid/partial_cache'
|
require 'liquid/partial_cache'
|
||||||
require 'liquid/usage'
|
require 'liquid/usage'
|
||||||
require 'liquid/registers'
|
require 'liquid/register'
|
||||||
|
require 'liquid/static_registers'
|
||||||
require 'liquid/template_factory'
|
require 'liquid/template_factory'
|
||||||
|
|
||||||
|
# Load all the tags of the standard library
|
||||||
|
#
|
||||||
|
Dir["#{__dir__}/liquid/tags/*.rb"].each { |f| require f }
|
||||||
|
|||||||
+8
-15
@@ -13,7 +13,6 @@ module Liquid
|
|||||||
@body = new_body
|
@body = new_body
|
||||||
while parse_body(@body, tokens)
|
while parse_body(@body, tokens)
|
||||||
end
|
end
|
||||||
@body.freeze
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# For backwards compatibility
|
# For backwards compatibility
|
||||||
@@ -36,26 +35,18 @@ module Liquid
|
|||||||
# @api private
|
# @api private
|
||||||
def self.raise_unknown_tag(tag, block_name, block_delimiter, parse_context)
|
def self.raise_unknown_tag(tag, block_name, block_delimiter, parse_context)
|
||||||
if tag == 'else'
|
if tag == 'else'
|
||||||
raise SyntaxError, parse_context.locale.t(
|
raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_else",
|
||||||
"errors.syntax.unexpected_else",
|
block_name: block_name)
|
||||||
block_name: block_name,
|
|
||||||
)
|
|
||||||
elsif tag.start_with?('end')
|
elsif tag.start_with?('end')
|
||||||
raise SyntaxError, parse_context.locale.t(
|
raise SyntaxError, parse_context.locale.t("errors.syntax.invalid_delimiter",
|
||||||
"errors.syntax.invalid_delimiter",
|
|
||||||
tag: tag,
|
tag: tag,
|
||||||
block_name: block_name,
|
block_name: block_name,
|
||||||
block_delimiter: block_delimiter,
|
block_delimiter: block_delimiter)
|
||||||
)
|
|
||||||
else
|
else
|
||||||
raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag", tag: tag)
|
raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag", tag: tag)
|
||||||
end
|
end
|
||||||
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
|
def block_name
|
||||||
@tag_name
|
@tag_name
|
||||||
end
|
end
|
||||||
@@ -68,7 +59,7 @@ module Liquid
|
|||||||
|
|
||||||
# @api public
|
# @api public
|
||||||
def new_body
|
def new_body
|
||||||
parse_context.new_block_body
|
BlockBody.new
|
||||||
end
|
end
|
||||||
|
|
||||||
# @api public
|
# @api public
|
||||||
@@ -82,7 +73,9 @@ module Liquid
|
|||||||
@blank &&= body.blank?
|
@blank &&= body.blank?
|
||||||
|
|
||||||
return false if end_tag_name == block_delimiter
|
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
|
# this tag is not registered with the system
|
||||||
# pass it to the current block for special handling or error reporting
|
# pass it to the current block for special handling or error reporting
|
||||||
|
|||||||
+22
-62
@@ -4,9 +4,8 @@ require 'English'
|
|||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
class BlockBody
|
class BlockBody
|
||||||
LiquidTagToken = /\A\s*(#{TagName})\s*(.*?)\z/o
|
LiquidTagToken = /\A\s*(\w+)\s*(.*?)\z/o
|
||||||
FullToken = /\A#{TagStart}#{WhitespaceControl}?(\s*)(#{TagName})(\s*)(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
|
FullToken = /\A#{TagStart}#{WhitespaceControl}?(\s*)(\w+)(\s*)(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
|
||||||
FullTokenPossiblyInvalid = /\A(.*)#{TagStart}#{WhitespaceControl}?\s*(\w+)\s*(.*)?#{WhitespaceControl}?#{TagEnd}\z/om
|
|
||||||
ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
|
ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
|
||||||
WhitespaceOrNothing = /\A\s*\z/
|
WhitespaceOrNothing = /\A\s*\z/
|
||||||
TAGSTART = "{%"
|
TAGSTART = "{%"
|
||||||
@@ -20,8 +19,6 @@ module Liquid
|
|||||||
end
|
end
|
||||||
|
|
||||||
def parse(tokenizer, parse_context, &block)
|
def parse(tokenizer, parse_context, &block)
|
||||||
raise FrozenError, "can't modify frozen Liquid::BlockBody" if frozen?
|
|
||||||
|
|
||||||
parse_context.line_number = tokenizer.line_number
|
parse_context.line_number = tokenizer.line_number
|
||||||
|
|
||||||
if tokenizer.for_liquid_tag
|
if tokenizer.for_liquid_tag
|
||||||
@@ -31,14 +28,9 @@ module Liquid
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def freeze
|
|
||||||
@nodelist.freeze
|
|
||||||
super
|
|
||||||
end
|
|
||||||
|
|
||||||
private def parse_for_liquid_tag(tokenizer, parse_context)
|
private def parse_for_liquid_tag(tokenizer, parse_context)
|
||||||
while (token = tokenizer.shift)
|
while (token = tokenizer.shift)
|
||||||
unless token.empty? || token.match?(WhitespaceOrNothing)
|
unless token.empty? || token =~ WhitespaceOrNothing
|
||||||
unless token =~ LiquidTagToken
|
unless token =~ LiquidTagToken
|
||||||
# line isn't empty but didn't match tag syntax, yield and let the
|
# line isn't empty but didn't match tag syntax, yield and let the
|
||||||
# caller raise a syntax error
|
# caller raise a syntax error
|
||||||
@@ -46,13 +38,7 @@ module Liquid
|
|||||||
end
|
end
|
||||||
tag_name = Regexp.last_match(1)
|
tag_name = Regexp.last_match(1)
|
||||||
markup = Regexp.last_match(2)
|
markup = Regexp.last_match(2)
|
||||||
|
unless (tag = registered_tags[tag_name])
|
||||||
if tag_name == 'liquid'
|
|
||||||
parse_context.line_number -= 1
|
|
||||||
next parse_liquid_tag(markup, parse_context)
|
|
||||||
end
|
|
||||||
|
|
||||||
unless (tag = parse_context.environment.tag_for_name(tag_name))
|
|
||||||
# end parsing if we reach an unknown tag and let the caller decide
|
# end parsing if we reach an unknown tag and let the caller decide
|
||||||
# determine how to proceed
|
# determine how to proceed
|
||||||
return yield tag_name, markup
|
return yield tag_name, markup
|
||||||
@@ -85,30 +71,17 @@ module Liquid
|
|||||||
# @api private
|
# @api private
|
||||||
def self.render_node(context, output, node)
|
def self.render_node(context, output, node)
|
||||||
node.render_to_output_buffer(context, output)
|
node.render_to_output_buffer(context, output)
|
||||||
rescue => exc
|
rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
|
||||||
blank_tag = !node.instance_of?(Variable) && node.blank?
|
context.handle_error(e, node.line_number)
|
||||||
rescue_render_node(context, output, node.line_number, exc, blank_tag)
|
rescue MemoryError
|
||||||
end
|
raise
|
||||||
|
rescue ::StandardError => e
|
||||||
# @api private
|
line_number = node.is_a?(String) ? nil : node.line_number
|
||||||
def self.rescue_render_node(context, output, line_number, exc, blank_tag)
|
output << context.handle_error(e, line_number)
|
||||||
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
|
end
|
||||||
|
|
||||||
private def parse_liquid_tag(markup, parse_context)
|
private def parse_liquid_tag(markup, parse_context)
|
||||||
liquid_tag_tokenizer = parse_context.new_tokenizer(
|
liquid_tag_tokenizer = Tokenizer.new(markup, line_number: parse_context.line_number, for_liquid_tag: true)
|
||||||
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|
|
parse_for_liquid_tag(liquid_tag_tokenizer, parse_context) do |end_tag_name, _end_tag_markup|
|
||||||
if end_tag_name
|
if end_tag_name
|
||||||
BlockBody.unknown_tag_in_liquid_tag(end_tag_name, parse_context)
|
BlockBody.unknown_tag_in_liquid_tag(end_tag_name, parse_context)
|
||||||
@@ -116,22 +89,14 @@ module Liquid
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private def handle_invalid_tag_token(token, parse_context)
|
private def parse_for_document(tokenizer, parse_context)
|
||||||
if token.end_with?('%}')
|
|
||||||
yield token, token
|
|
||||||
else
|
|
||||||
BlockBody.raise_missing_tag_terminator(token, parse_context)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private def parse_for_document(tokenizer, parse_context, &block)
|
|
||||||
while (token = tokenizer.shift)
|
while (token = tokenizer.shift)
|
||||||
next if token.empty?
|
next if token.empty?
|
||||||
case
|
case
|
||||||
when token.start_with?(TAGSTART)
|
when token.start_with?(TAGSTART)
|
||||||
whitespace_handler(token, parse_context)
|
whitespace_handler(token, parse_context)
|
||||||
unless token =~ FullToken
|
unless token =~ FullToken
|
||||||
return handle_invalid_tag_token(token, parse_context, &block)
|
BlockBody.raise_missing_tag_terminator(token, parse_context)
|
||||||
end
|
end
|
||||||
tag_name = Regexp.last_match(2)
|
tag_name = Regexp.last_match(2)
|
||||||
markup = Regexp.last_match(4)
|
markup = Regexp.last_match(4)
|
||||||
@@ -147,7 +112,7 @@ module Liquid
|
|||||||
next
|
next
|
||||||
end
|
end
|
||||||
|
|
||||||
unless (tag = parse_context.environment.tag_for_name(tag_name))
|
unless (tag = registered_tags[tag_name])
|
||||||
# end parsing if we reach an unknown tag and let the caller decide
|
# end parsing if we reach an unknown tag and let the caller decide
|
||||||
# determine how to proceed
|
# determine how to proceed
|
||||||
return yield tag_name, markup
|
return yield tag_name, markup
|
||||||
@@ -165,7 +130,7 @@ module Liquid
|
|||||||
end
|
end
|
||||||
parse_context.trim_whitespace = false
|
parse_context.trim_whitespace = false
|
||||||
@nodelist << token
|
@nodelist << token
|
||||||
@blank &&= token.match?(WhitespaceOrNothing)
|
@blank &&= !!(token =~ WhitespaceOrNothing)
|
||||||
end
|
end
|
||||||
parse_context.line_number = tokenizer.line_number
|
parse_context.line_number = tokenizer.line_number
|
||||||
end
|
end
|
||||||
@@ -216,8 +181,6 @@ module Liquid
|
|||||||
end
|
end
|
||||||
|
|
||||||
def render_to_output_buffer(context, output)
|
def render_to_output_buffer(context, output)
|
||||||
freeze unless frozen?
|
|
||||||
|
|
||||||
context.resource_limits.increment_render_score(@nodelist.length)
|
context.resource_limits.increment_render_score(@nodelist.length)
|
||||||
|
|
||||||
idx = 0
|
idx = 0
|
||||||
@@ -246,17 +209,10 @@ module Liquid
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create_variable(token, parse_context)
|
def create_variable(token, parse_context)
|
||||||
if token.end_with?("}}")
|
token.scan(ContentOfVariable) do |content|
|
||||||
i = 2
|
markup = content.first
|
||||||
i = 3 if token[i] == "-"
|
|
||||||
parse_end = token.length - 3
|
|
||||||
parse_end -= 1 if token[parse_end] == "-"
|
|
||||||
markup_end = parse_end - i + 1
|
|
||||||
markup = markup_end <= 0 ? "" : token.slice(i, markup_end)
|
|
||||||
|
|
||||||
return Variable.new(markup, parse_context)
|
return Variable.new(markup, parse_context)
|
||||||
end
|
end
|
||||||
|
|
||||||
BlockBody.raise_missing_variable_terminator(token, parse_context)
|
BlockBody.raise_missing_variable_terminator(token, parse_context)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -269,5 +225,9 @@ module Liquid
|
|||||||
def raise_missing_variable_terminator(token, parse_context)
|
def raise_missing_variable_terminator(token, parse_context)
|
||||||
BlockBody.raise_missing_variable_terminator(token, parse_context)
|
BlockBody.raise_missing_variable_terminator(token, parse_context)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def registered_tags
|
||||||
|
Template.tags
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+10
-39
@@ -8,7 +8,7 @@ module Liquid
|
|||||||
# c = Condition.new(1, '==', 1)
|
# c = Condition.new(1, '==', 1)
|
||||||
# c.evaluate #=> true
|
# c.evaluate #=> true
|
||||||
#
|
#
|
||||||
class Condition # :nodoc:
|
class Condition #:nodoc:
|
||||||
@@operators = {
|
@@operators = {
|
||||||
'==' => ->(cond, left, right) { cond.send(:equal_variables, left, right) },
|
'==' => ->(cond, left, right) { cond.send(:equal_variables, left, right) },
|
||||||
'!=' => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
|
'!=' => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
|
||||||
@@ -24,34 +24,13 @@ module Liquid
|
|||||||
else
|
else
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
rescue Encoding::CompatibilityError
|
|
||||||
# "✅".b.include?("✅") raises Encoding::CompatibilityError despite being materially equal
|
|
||||||
left.b.include?(right.b)
|
|
||||||
end,
|
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
|
def self.operators
|
||||||
@@operators
|
@@operators
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.parse_expression(parse_context, markup)
|
|
||||||
@@method_literals[markup] || parse_context.parse_expression(markup)
|
|
||||||
end
|
|
||||||
|
|
||||||
attr_reader :attachment, :child_condition
|
attr_reader :attachment, :child_condition
|
||||||
attr_accessor :left, :operator, :right
|
attr_accessor :left, :operator, :right
|
||||||
|
|
||||||
@@ -64,7 +43,7 @@ module Liquid
|
|||||||
@child_condition = nil
|
@child_condition = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def evaluate(context = deprecated_default_context)
|
def evaluate(context = Context.new)
|
||||||
condition = self
|
condition = self
|
||||||
result = nil
|
result = nil
|
||||||
loop do
|
loop do
|
||||||
@@ -72,9 +51,9 @@ module Liquid
|
|||||||
|
|
||||||
case condition.child_relation
|
case condition.child_relation
|
||||||
when :or
|
when :or
|
||||||
break if Liquid::Utils.to_liquid_value(result)
|
break if result
|
||||||
when :and
|
when :and
|
||||||
break unless Liquid::Utils.to_liquid_value(result)
|
break unless result
|
||||||
else
|
else
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
@@ -112,7 +91,7 @@ module Liquid
|
|||||||
private
|
private
|
||||||
|
|
||||||
def equal_variables(left, right)
|
def equal_variables(left, right)
|
||||||
if left.is_a?(MethodLiteral)
|
if left.is_a?(Liquid::Expression::MethodLiteral)
|
||||||
if right.respond_to?(left.method_name)
|
if right.respond_to?(left.method_name)
|
||||||
return right.send(left.method_name)
|
return right.send(left.method_name)
|
||||||
else
|
else
|
||||||
@@ -120,7 +99,7 @@ module Liquid
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if right.is_a?(MethodLiteral)
|
if right.is_a?(Liquid::Expression::MethodLiteral)
|
||||||
if left.respond_to?(right.method_name)
|
if left.respond_to?(right.method_name)
|
||||||
return left.send(right.method_name)
|
return left.send(right.method_name)
|
||||||
else
|
else
|
||||||
@@ -137,8 +116,8 @@ module Liquid
|
|||||||
# return this as the result.
|
# return this as the result.
|
||||||
return context.evaluate(left) if op.nil?
|
return context.evaluate(left) if op.nil?
|
||||||
|
|
||||||
left = Liquid::Utils.to_liquid_value(context.evaluate(left))
|
left = context.evaluate(left)
|
||||||
right = Liquid::Utils.to_liquid_value(context.evaluate(right))
|
right = context.evaluate(right)
|
||||||
|
|
||||||
operation = self.class.operators[op] || raise(Liquid::ArgumentError, "Unknown operator #{op}")
|
operation = self.class.operators[op] || raise(Liquid::ArgumentError, "Unknown operator #{op}")
|
||||||
|
|
||||||
@@ -153,19 +132,11 @@ module Liquid
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def deprecated_default_context
|
|
||||||
warn("DEPRECATION WARNING: Condition#evaluate without a context argument is deprecated" \
|
|
||||||
" and will be removed from Liquid 6.0.0.")
|
|
||||||
Context.new
|
|
||||||
end
|
|
||||||
|
|
||||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||||
def children
|
def children
|
||||||
[
|
[
|
||||||
@node.left,
|
@node.left, @node.right,
|
||||||
@node.right,
|
@node.child_condition, @node.attachment
|
||||||
@node.child_condition,
|
|
||||||
@node.attachment
|
|
||||||
].compact
|
].compact
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Liquid
|
|
||||||
module Const
|
|
||||||
EMPTY_HASH = {}.freeze
|
|
||||||
EMPTY_ARRAY = [].freeze
|
|
||||||
end
|
|
||||||
end
|
|
||||||
+22
-39
@@ -15,48 +15,36 @@ module Liquid
|
|||||||
# context['bob'] #=> nil class Context
|
# context['bob'] #=> nil class Context
|
||||||
class Context
|
class Context
|
||||||
attr_reader :scopes, :errors, :registers, :environments, :resource_limits, :static_registers, :static_environments
|
attr_reader :scopes, :errors, :registers, :environments, :resource_limits, :static_registers, :static_environments
|
||||||
attr_accessor :exception_renderer, :template_name, :partial, :global_filter, :strict_variables, :strict_filters, :environment
|
attr_accessor :exception_renderer, :template_name, :partial, :global_filter, :strict_variables, :strict_filters
|
||||||
|
|
||||||
# rubocop:disable Metrics/ParameterLists
|
# rubocop:disable Metrics/ParameterLists
|
||||||
def self.build(environment: Environment.default, environments: {}, outer_scope: {}, registers: {}, rethrow_errors: false, resource_limits: nil, 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, environment, &block)
|
new(environments, outer_scope, registers, rethrow_errors, resource_limits, static_environments)
|
||||||
end
|
end
|
||||||
|
|
||||||
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil, static_environments = {}, environment = Environment.default)
|
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil, static_environments = {})
|
||||||
@environment = environment
|
|
||||||
@environments = [environments]
|
@environments = [environments]
|
||||||
@environments.flatten!
|
@environments.flatten!
|
||||||
|
|
||||||
@static_environments = [static_environments].flatten(1).freeze
|
@static_environments = [static_environments].flat_map(&:freeze).freeze
|
||||||
@scopes = [outer_scope || {}]
|
@scopes = [(outer_scope || {})]
|
||||||
@registers = registers.is_a?(Registers) ? registers : Registers.new(registers)
|
@registers = registers
|
||||||
@errors = []
|
@errors = []
|
||||||
@partial = false
|
@partial = false
|
||||||
@strict_variables = false
|
@strict_variables = false
|
||||||
@resource_limits = resource_limits || ResourceLimits.new(environment.default_resource_limits)
|
@resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
|
||||||
@base_scope_depth = 0
|
@base_scope_depth = 0
|
||||||
@interrupts = []
|
squash_instance_assigns_with_environments
|
||||||
@filters = []
|
|
||||||
@global_filter = nil
|
|
||||||
@disabled_tags = {}
|
|
||||||
|
|
||||||
# Instead of constructing new StringScanner objects for each Expression parse,
|
self.exception_renderer = Template.default_exception_renderer
|
||||||
# we recycle the same one.
|
|
||||||
@string_scanner = StringScanner.new("")
|
|
||||||
|
|
||||||
@registers.static[:cached_partials] ||= {}
|
|
||||||
@registers.static[:file_system] ||= environment.file_system
|
|
||||||
@registers.static[:template_factory] ||= Liquid::TemplateFactory.new
|
|
||||||
|
|
||||||
self.exception_renderer = environment.exception_renderer
|
|
||||||
if rethrow_errors
|
if rethrow_errors
|
||||||
self.exception_renderer = Liquid::RAISE_EXCEPTION_LAMBDA
|
self.exception_renderer = ->(_e) { raise }
|
||||||
end
|
end
|
||||||
|
|
||||||
yield self if block_given?
|
@interrupts = []
|
||||||
|
@filters = []
|
||||||
# Do this last, since it could result in this object being passed to a Proc in the environment
|
@global_filter = nil
|
||||||
squash_instance_assigns_with_environments
|
@disabled_tags = {}
|
||||||
end
|
end
|
||||||
# rubocop:enable Metrics/ParameterLists
|
# rubocop:enable Metrics/ParameterLists
|
||||||
|
|
||||||
@@ -65,7 +53,7 @@ module Liquid
|
|||||||
end
|
end
|
||||||
|
|
||||||
def strainer
|
def strainer
|
||||||
@strainer ||= @environment.create_strainer(self, @filters)
|
@strainer ||= StrainerFactory.create(self, @filters)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Adds filters to this context.
|
# Adds filters to this context.
|
||||||
@@ -133,7 +121,7 @@ module Liquid
|
|||||||
# context['var'] = 'hi'
|
# context['var'] = 'hi'
|
||||||
# end
|
# end
|
||||||
#
|
#
|
||||||
# context['var'] #=> nil
|
# context['var] #=> nil
|
||||||
def stack(new_scope = {})
|
def stack(new_scope = {})
|
||||||
push(new_scope)
|
push(new_scope)
|
||||||
yield
|
yield
|
||||||
@@ -146,11 +134,10 @@ module Liquid
|
|||||||
def new_isolated_subcontext
|
def new_isolated_subcontext
|
||||||
check_overflow
|
check_overflow
|
||||||
|
|
||||||
self.class.build(
|
Context.build(
|
||||||
environment: @environment,
|
|
||||||
resource_limits: resource_limits,
|
resource_limits: resource_limits,
|
||||||
static_environments: static_environments,
|
static_environments: static_environments,
|
||||||
registers: Registers.new(registers),
|
registers: StaticRegisters.new(registers)
|
||||||
).tap do |subcontext|
|
).tap do |subcontext|
|
||||||
subcontext.base_scope_depth = base_scope_depth + 1
|
subcontext.base_scope_depth = base_scope_depth + 1
|
||||||
subcontext.exception_renderer = exception_renderer
|
subcontext.exception_renderer = exception_renderer
|
||||||
@@ -180,7 +167,7 @@ module Liquid
|
|||||||
# Example:
|
# Example:
|
||||||
# products == empty #=> products.empty?
|
# products == empty #=> products.empty?
|
||||||
def [](expression)
|
def [](expression)
|
||||||
evaluate(Expression.parse(expression, @string_scanner))
|
evaluate(Expression.parse(expression))
|
||||||
end
|
end
|
||||||
|
|
||||||
def key?(key)
|
def key?(key)
|
||||||
@@ -203,14 +190,10 @@ module Liquid
|
|||||||
try_variable_find_in_environments(key, raise_on_not_found: raise_on_not_found)
|
try_variable_find_in_environments(key, raise_on_not_found: raise_on_not_found)
|
||||||
end
|
end
|
||||||
|
|
||||||
# update variable's context before invoking #to_liquid
|
variable = variable.to_liquid
|
||||||
variable.context = self if variable.respond_to?(:context=)
|
variable.context = self if variable.respond_to?(:context=)
|
||||||
|
|
||||||
liquid_variable = variable.to_liquid
|
variable
|
||||||
|
|
||||||
liquid_variable.context = self if variable != liquid_variable && liquid_variable.respond_to?(:context=)
|
|
||||||
|
|
||||||
liquid_variable
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def lookup_and_evaluate(obj, key, raise_on_not_found: true)
|
def lookup_and_evaluate(obj, key, raise_on_not_found: true)
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require "set"
|
|
||||||
|
|
||||||
module Liquid
|
|
||||||
class Deprecations
|
|
||||||
class << self
|
|
||||||
attr_accessor :warned
|
|
||||||
|
|
||||||
Deprecations.warned = Set.new
|
|
||||||
|
|
||||||
def warn(name, alternative)
|
|
||||||
return if warned.include?(name)
|
|
||||||
|
|
||||||
warned << name
|
|
||||||
|
|
||||||
caller_location = caller_locations(2, 1).first
|
|
||||||
Warning.warn("[DEPRECATION] #{name} is deprecated. Use #{alternative} instead. Called from #{caller_location}\n")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -22,7 +22,6 @@ module Liquid
|
|||||||
def parse(tokenizer, parse_context)
|
def parse(tokenizer, parse_context)
|
||||||
while parse_body(tokenizer)
|
while parse_body(tokenizer)
|
||||||
end
|
end
|
||||||
@body.freeze
|
|
||||||
rescue SyntaxError => e
|
rescue SyntaxError => e
|
||||||
e.line_number ||= parse_context.line_number
|
e.line_number ||= parse_context.line_number
|
||||||
raise
|
raise
|
||||||
@@ -42,13 +41,13 @@ module Liquid
|
|||||||
end
|
end
|
||||||
|
|
||||||
def render(context)
|
def render(context)
|
||||||
render_to_output_buffer(context, +'')
|
@body.render(context)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def new_body
|
def new_body
|
||||||
parse_context.new_block_body
|
Liquid::BlockBody.new
|
||||||
end
|
end
|
||||||
|
|
||||||
def parse_body(tokenizer)
|
def parse_body(tokenizer)
|
||||||
|
|||||||
@@ -25,10 +25,6 @@ module Liquid
|
|||||||
class Drop
|
class Drop
|
||||||
attr_writer :context
|
attr_writer :context
|
||||||
|
|
||||||
def initialize
|
|
||||||
@context = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
# Catch all for the method
|
# Catch all for the method
|
||||||
def liquid_method_missing(method)
|
def liquid_method_missing(method)
|
||||||
return nil unless @context&.strict_variables
|
return nil unless @context&.strict_variables
|
||||||
|
|||||||
@@ -1,159 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Liquid
|
|
||||||
# The Environment is the container for all configuration options of Liquid, such as
|
|
||||||
# the registered tags, filters, and the default error mode.
|
|
||||||
class Environment
|
|
||||||
# The default error mode for all templates. This can be overridden on a
|
|
||||||
# per-template basis.
|
|
||||||
attr_accessor :error_mode
|
|
||||||
|
|
||||||
# The tags that are available to use in the template.
|
|
||||||
attr_accessor :tags
|
|
||||||
|
|
||||||
# The strainer template which is used to store filters that are available to
|
|
||||||
# use in templates.
|
|
||||||
attr_accessor :strainer_template
|
|
||||||
|
|
||||||
# The exception renderer that is used to render exceptions that are raised
|
|
||||||
# when rendering a template
|
|
||||||
attr_accessor :exception_renderer
|
|
||||||
|
|
||||||
# The default file system that is used to load templates from.
|
|
||||||
attr_accessor :file_system
|
|
||||||
|
|
||||||
# The default resource limits that are used to limit the resources that a
|
|
||||||
# template can consume.
|
|
||||||
attr_accessor :default_resource_limits
|
|
||||||
|
|
||||||
class << self
|
|
||||||
# Creates a new environment instance.
|
|
||||||
#
|
|
||||||
# @param tags [Hash] The tags that are available to use in
|
|
||||||
# the template.
|
|
||||||
# @param file_system The default file system that is used
|
|
||||||
# to load templates from.
|
|
||||||
# @param error_mode [Symbol] The default error mode for all templates
|
|
||||||
# (either :strict, :warn, or :lax).
|
|
||||||
# @param exception_renderer [Proc] The exception renderer that is used to
|
|
||||||
# render exceptions.
|
|
||||||
# @yieldparam environment [Environment] The environment instance that is being built.
|
|
||||||
# @return [Environment] The new environment instance.
|
|
||||||
def build(tags: nil, file_system: nil, error_mode: nil, exception_renderer: nil)
|
|
||||||
ret = new
|
|
||||||
ret.tags = tags if tags
|
|
||||||
ret.file_system = file_system if file_system
|
|
||||||
ret.error_mode = error_mode if error_mode
|
|
||||||
ret.exception_renderer = exception_renderer if exception_renderer
|
|
||||||
yield ret if block_given?
|
|
||||||
ret.freeze
|
|
||||||
end
|
|
||||||
|
|
||||||
# Returns the default environment instance.
|
|
||||||
#
|
|
||||||
# @return [Environment] The default environment instance.
|
|
||||||
def default
|
|
||||||
@default ||= new
|
|
||||||
end
|
|
||||||
|
|
||||||
# Sets the default environment instance for the duration of the block
|
|
||||||
#
|
|
||||||
# @param environment [Environment] The environment instance to use as the default for the
|
|
||||||
# duration of the block.
|
|
||||||
# @yield
|
|
||||||
# @return [Object] The return value of the block.
|
|
||||||
def dangerously_override(environment)
|
|
||||||
original_default = @default
|
|
||||||
@default = environment
|
|
||||||
yield
|
|
||||||
ensure
|
|
||||||
@default = original_default
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Initializes a new environment instance.
|
|
||||||
# @api private
|
|
||||||
def initialize
|
|
||||||
@tags = Tags::STANDARD_TAGS.dup
|
|
||||||
@error_mode = :lax
|
|
||||||
@strainer_template = Class.new(StrainerTemplate).tap do |klass|
|
|
||||||
klass.add_filter(StandardFilters)
|
|
||||||
end
|
|
||||||
@exception_renderer = ->(exception) { exception }
|
|
||||||
@file_system = BlankFileSystem.new
|
|
||||||
@default_resource_limits = Const::EMPTY_HASH
|
|
||||||
@strainer_template_class_cache = {}
|
|
||||||
end
|
|
||||||
|
|
||||||
# Registers a new tag with the environment.
|
|
||||||
#
|
|
||||||
# @param name [String] The name of the tag.
|
|
||||||
# @param klass [Liquid::Tag] The class that implements the tag.
|
|
||||||
# @return [void]
|
|
||||||
def register_tag(name, klass)
|
|
||||||
@tags[name] = klass
|
|
||||||
end
|
|
||||||
|
|
||||||
# Registers a new filter with the environment.
|
|
||||||
#
|
|
||||||
# @param filter [Module] The module that contains the filter methods.
|
|
||||||
# @return [void]
|
|
||||||
def register_filter(filter)
|
|
||||||
@strainer_template_class_cache.clear
|
|
||||||
@strainer_template.add_filter(filter)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Registers multiple filters with this environment.
|
|
||||||
#
|
|
||||||
# @param filters [Array<Module>] The modules that contain the filter methods.
|
|
||||||
# @return [self]
|
|
||||||
def register_filters(filters)
|
|
||||||
@strainer_template_class_cache.clear
|
|
||||||
filters.each { |f| @strainer_template.add_filter(f) }
|
|
||||||
self
|
|
||||||
end
|
|
||||||
|
|
||||||
# Creates a new strainer instance with the given filters, caching the result
|
|
||||||
# for faster lookup.
|
|
||||||
#
|
|
||||||
# @param context [Liquid::Context] The context that the strainer will be
|
|
||||||
# used in.
|
|
||||||
# @param filters [Array<Module>] The filters that the strainer will have
|
|
||||||
# access to.
|
|
||||||
# @return [Liquid::Strainer] The new strainer instance.
|
|
||||||
def create_strainer(context, filters = Const::EMPTY_ARRAY)
|
|
||||||
return @strainer_template.new(context) if filters.empty?
|
|
||||||
|
|
||||||
strainer_template = @strainer_template_class_cache[filters] ||= begin
|
|
||||||
klass = Class.new(@strainer_template)
|
|
||||||
filters.each { |f| klass.add_filter(f) }
|
|
||||||
klass
|
|
||||||
end
|
|
||||||
|
|
||||||
strainer_template.new(context)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Returns the names of all the filter methods that are available to use in
|
|
||||||
# the strainer template.
|
|
||||||
#
|
|
||||||
# @return [Array<String>] The names of all the filter methods.
|
|
||||||
def filter_method_names
|
|
||||||
@strainer_template.filter_method_names
|
|
||||||
end
|
|
||||||
|
|
||||||
# Returns the tag class for the given tag name.
|
|
||||||
#
|
|
||||||
# @param name [String] The name of the tag.
|
|
||||||
# @return [Liquid::Tag] The tag class.
|
|
||||||
def tag_for_name(name)
|
|
||||||
@tags[name]
|
|
||||||
end
|
|
||||||
|
|
||||||
def freeze
|
|
||||||
@tags.freeze
|
|
||||||
# TODO: freeze the tags, currently this is not possible because of liquid-c
|
|
||||||
# @strainer_template.freeze
|
|
||||||
super
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
+15
-16
@@ -40,20 +40,19 @@ module Liquid
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
ArgumentError = Class.new(Error)
|
ArgumentError = Class.new(Error)
|
||||||
ContextError = Class.new(Error)
|
ContextError = Class.new(Error)
|
||||||
FileSystemError = Class.new(Error)
|
FileSystemError = Class.new(Error)
|
||||||
StandardError = Class.new(Error)
|
StandardError = Class.new(Error)
|
||||||
SyntaxError = Class.new(Error)
|
SyntaxError = Class.new(Error)
|
||||||
StackLevelError = Class.new(Error)
|
StackLevelError = Class.new(Error)
|
||||||
MemoryError = Class.new(Error)
|
MemoryError = Class.new(Error)
|
||||||
ZeroDivisionError = Class.new(Error)
|
ZeroDivisionError = Class.new(Error)
|
||||||
FloatDomainError = Class.new(Error)
|
FloatDomainError = Class.new(Error)
|
||||||
UndefinedVariable = Class.new(Error)
|
UndefinedVariable = Class.new(Error)
|
||||||
UndefinedDropMethod = Class.new(Error)
|
UndefinedDropMethod = Class.new(Error)
|
||||||
UndefinedFilter = Class.new(Error)
|
UndefinedFilter = Class.new(Error)
|
||||||
MethodOverrideError = Class.new(Error)
|
MethodOverrideError = Class.new(Error)
|
||||||
DisabledError = Class.new(Error)
|
DisabledError = Class.new(Error)
|
||||||
InternalError = Class.new(Error)
|
InternalError = Class.new(Error)
|
||||||
TemplateEncodingError = Class.new(Error)
|
|
||||||
end
|
end
|
||||||
|
|||||||
+34
-107
@@ -2,121 +2,48 @@
|
|||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
class Expression
|
class Expression
|
||||||
|
class MethodLiteral
|
||||||
|
attr_reader :method_name, :to_s
|
||||||
|
|
||||||
|
def initialize(method_name, to_s)
|
||||||
|
@method_name = method_name
|
||||||
|
@to_s = to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_liquid
|
||||||
|
to_s
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
LITERALS = {
|
LITERALS = {
|
||||||
nil => nil,
|
nil => nil, 'nil' => nil, 'null' => nil, '' => nil,
|
||||||
'nil' => nil,
|
|
||||||
'null' => nil,
|
|
||||||
'' => nil,
|
|
||||||
'true' => true,
|
'true' => true,
|
||||||
'false' => false,
|
'false' => false,
|
||||||
'blank' => '',
|
'blank' => MethodLiteral.new(:blank?, '').freeze,
|
||||||
'empty' => '',
|
'empty' => MethodLiteral.new(:empty?, '').freeze
|
||||||
# in lax mode, minus sign can be a VariableLookup
|
|
||||||
# For simplicity and performace, we treat it like a literal
|
|
||||||
'-' => VariableLookup.parse("-", nil).freeze,
|
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
DOT = ".".ord
|
SINGLE_QUOTED_STRING = /\A'(.*)'\z/m
|
||||||
ZERO = "0".ord
|
DOUBLE_QUOTED_STRING = /\A"(.*)"\z/m
|
||||||
NINE = "9".ord
|
INTEGERS_REGEX = /\A(-?\d+)\z/
|
||||||
DASH = "-".ord
|
FLOATS_REGEX = /\A(-?\d[\d\.]+)\z/
|
||||||
|
RANGES_REGEX = /\A\((\S+)\.\.(\S+)\)\z/
|
||||||
|
|
||||||
# Use an atomic group (?>...) to avoid pathological backtracing from
|
def self.parse(markup)
|
||||||
# malicious input as described in https://github.com/Shopify/liquid/issues/1357
|
if LITERALS.key?(markup)
|
||||||
RANGES_REGEX = /\A\(\s*(?>(\S+)\s*\.\.)\s*(\S+)\s*\)\z/
|
LITERALS[markup]
|
||||||
INTEGER_REGEX = /\A(-?\d+)\z/
|
else
|
||||||
FLOAT_REGEX = /\A(-?\d+)\.\d+\z/
|
|
||||||
|
|
||||||
class << self
|
|
||||||
def parse(markup, ss = StringScanner.new(""), cache = nil)
|
|
||||||
return unless markup
|
|
||||||
|
|
||||||
markup = markup.strip # markup can be a frozen string
|
|
||||||
|
|
||||||
if (markup.start_with?('"') && markup.end_with?('"')) ||
|
|
||||||
(markup.start_with?("'") && markup.end_with?("'"))
|
|
||||||
return markup[1..-2]
|
|
||||||
elsif LITERALS.key?(markup)
|
|
||||||
return LITERALS[markup]
|
|
||||||
end
|
|
||||||
|
|
||||||
# Cache only exists during parsing
|
|
||||||
if cache
|
|
||||||
return cache[markup] if cache.key?(markup)
|
|
||||||
|
|
||||||
cache[markup] = inner_parse(markup, ss, cache).freeze
|
|
||||||
else
|
|
||||||
inner_parse(markup, ss, nil).freeze
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def inner_parse(markup, ss, cache)
|
|
||||||
if (markup.start_with?("(") && markup.end_with?(")")) && markup =~ RANGES_REGEX
|
|
||||||
return RangeLookup.parse(
|
|
||||||
Regexp.last_match(1),
|
|
||||||
Regexp.last_match(2),
|
|
||||||
ss,
|
|
||||||
cache,
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
if (num = parse_number(markup, ss))
|
|
||||||
num
|
|
||||||
else
|
|
||||||
VariableLookup.parse(markup, ss, cache)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def parse_number(markup, ss)
|
|
||||||
# check if the markup is simple integer or float
|
|
||||||
case markup
|
case markup
|
||||||
when INTEGER_REGEX
|
when SINGLE_QUOTED_STRING, DOUBLE_QUOTED_STRING
|
||||||
return Integer(markup, 10)
|
Regexp.last_match(1)
|
||||||
when FLOAT_REGEX
|
when INTEGERS_REGEX
|
||||||
return markup.to_f
|
Regexp.last_match(1).to_i
|
||||||
end
|
when RANGES_REGEX
|
||||||
|
RangeLookup.parse(Regexp.last_match(1), Regexp.last_match(2))
|
||||||
ss.string = markup
|
when FLOATS_REGEX
|
||||||
# the first byte must be a digit or a dash
|
Regexp.last_match(1).to_f
|
||||||
byte = ss.scan_byte
|
|
||||||
|
|
||||||
return false if byte != DASH && (byte < ZERO || byte > NINE)
|
|
||||||
|
|
||||||
if byte == DASH
|
|
||||||
peek_byte = ss.peek_byte
|
|
||||||
|
|
||||||
# if it starts with a dash, the next byte must be a digit
|
|
||||||
return false if peek_byte.nil? || !(peek_byte >= ZERO && peek_byte <= NINE)
|
|
||||||
end
|
|
||||||
|
|
||||||
# The markup could be a float with multiple dots
|
|
||||||
first_dot_pos = nil
|
|
||||||
num_end_pos = nil
|
|
||||||
|
|
||||||
while (byte = ss.scan_byte)
|
|
||||||
return false if byte != DOT && (byte < ZERO || byte > NINE)
|
|
||||||
|
|
||||||
# we found our number and now we are just scanning the rest of the string
|
|
||||||
next if num_end_pos
|
|
||||||
|
|
||||||
if byte == DOT
|
|
||||||
if first_dot_pos.nil?
|
|
||||||
first_dot_pos = ss.pos
|
|
||||||
else
|
|
||||||
# we found another dot, so we know that the number ends here
|
|
||||||
num_end_pos = ss.pos - 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
num_end_pos = markup.length if ss.eos?
|
|
||||||
|
|
||||||
if num_end_pos
|
|
||||||
# number ends with a number "123.123"
|
|
||||||
markup.byteslice(0, num_end_pos).to_f
|
|
||||||
else
|
else
|
||||||
# number ends with a dot "123."
|
VariableLookup.parse(markup)
|
||||||
markup.byteslice(0, first_dot_pos).to_f
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
# @liquid_public_docs
|
|
||||||
# @liquid_type object
|
|
||||||
# @liquid_name forloop
|
|
||||||
# @liquid_summary
|
|
||||||
# Information about a parent [`for` loop](/docs/api/liquid/tags/for).
|
|
||||||
class ForloopDrop < Drop
|
class ForloopDrop < Drop
|
||||||
def initialize(name, length, parentloop)
|
def initialize(name, length, parentloop)
|
||||||
@name = name
|
@name = name
|
||||||
@@ -14,68 +9,28 @@ module Liquid
|
|||||||
@index = 0
|
@index = 0
|
||||||
end
|
end
|
||||||
|
|
||||||
# @liquid_public_docs
|
attr_reader :name, :length, :parentloop
|
||||||
# @liquid_name length
|
|
||||||
# @liquid_summary
|
|
||||||
# The total number of iterations in the loop.
|
|
||||||
# @liquid_return [number]
|
|
||||||
attr_reader :length
|
|
||||||
|
|
||||||
# @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
|
def index
|
||||||
@index + 1
|
@index + 1
|
||||||
end
|
end
|
||||||
|
|
||||||
# @liquid_public_docs
|
|
||||||
# @liquid_summary
|
|
||||||
# The 0-based index of the current iteration.
|
|
||||||
# @liquid_return [number]
|
|
||||||
def index0
|
def index0
|
||||||
@index
|
@index
|
||||||
end
|
end
|
||||||
|
|
||||||
# @liquid_public_docs
|
|
||||||
# @liquid_summary
|
|
||||||
# The 1-based index of the current iteration, in reverse order.
|
|
||||||
# @liquid_return [number]
|
|
||||||
def rindex
|
def rindex
|
||||||
@length - @index
|
@length - @index
|
||||||
end
|
end
|
||||||
|
|
||||||
# @liquid_public_docs
|
|
||||||
# @liquid_summary
|
|
||||||
# The 0-based index of the current iteration, in reverse order.
|
|
||||||
# @liquid_return [number]
|
|
||||||
def rindex0
|
def rindex0
|
||||||
@length - @index - 1
|
@length - @index - 1
|
||||||
end
|
end
|
||||||
|
|
||||||
# @liquid_public_docs
|
|
||||||
# @liquid_summary
|
|
||||||
# Returns `true` if the current iteration is the first. Returns `false` if not.
|
|
||||||
# @liquid_return [boolean]
|
|
||||||
def first
|
def first
|
||||||
@index == 0
|
@index == 0
|
||||||
end
|
end
|
||||||
|
|
||||||
# @liquid_public_docs
|
|
||||||
# @liquid_summary
|
|
||||||
# Returns `true` if the current iteration is the last. Returns `false` if not.
|
|
||||||
# @liquid_return [boolean]
|
|
||||||
def last
|
def last
|
||||||
@index == @length - 1
|
@index == @length - 1
|
||||||
end
|
end
|
||||||
|
|||||||
+43
-154
@@ -1,173 +1,62 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "strscan"
|
||||||
module Liquid
|
module Liquid
|
||||||
class Lexer
|
class Lexer
|
||||||
CLOSE_ROUND = [:close_round, ")"].freeze
|
SPECIALS = {
|
||||||
CLOSE_SQUARE = [:close_square, "]"].freeze
|
'|' => :pipe,
|
||||||
COLON = [:colon, ":"].freeze
|
'.' => :dot,
|
||||||
COMMA = [:comma, ","].freeze
|
':' => :colon,
|
||||||
COMPARISION_NOT_EQUAL = [:comparison, "!="].freeze
|
',' => :comma,
|
||||||
COMPARISON_CONTAINS = [:comparison, "contains"].freeze
|
'[' => :open_square,
|
||||||
COMPARISON_EQUAL = [:comparison, "=="].freeze
|
']' => :close_square,
|
||||||
COMPARISON_GREATER_THAN = [:comparison, ">"].freeze
|
'(' => :open_round,
|
||||||
COMPARISON_GREATER_THAN_OR_EQUAL = [:comparison, ">="].freeze
|
')' => :close_round,
|
||||||
COMPARISON_LESS_THAN = [:comparison, "<"].freeze
|
'?' => :question,
|
||||||
COMPARISON_LESS_THAN_OR_EQUAL = [:comparison, "<="].freeze
|
'-' => :dash,
|
||||||
COMPARISON_NOT_EQUAL_ALT = [:comparison, "<>"].freeze
|
}.freeze
|
||||||
DASH = [:dash, "-"].freeze
|
|
||||||
DOT = [:dot, "."].freeze
|
|
||||||
DOTDOT = [:dotdot, ".."].freeze
|
|
||||||
DOT_ORD = ".".ord
|
|
||||||
DOUBLE_STRING_LITERAL = /"[^\"]*"/
|
|
||||||
EOS = [:end_of_string].freeze
|
|
||||||
IDENTIFIER = /[a-zA-Z_][\w-]*\??/
|
IDENTIFIER = /[a-zA-Z_][\w-]*\??/
|
||||||
NUMBER_LITERAL = /-?\d+(\.\d+)?/
|
|
||||||
OPEN_ROUND = [:open_round, "("].freeze
|
|
||||||
OPEN_SQUARE = [:open_square, "["].freeze
|
|
||||||
PIPE = [:pipe, "|"].freeze
|
|
||||||
QUESTION = [:question, "?"].freeze
|
|
||||||
RUBY_WHITESPACE = [" ", "\t", "\r", "\n", "\f"].freeze
|
|
||||||
SINGLE_STRING_LITERAL = /'[^\']*'/
|
SINGLE_STRING_LITERAL = /'[^\']*'/
|
||||||
|
DOUBLE_STRING_LITERAL = /"[^\"]*"/
|
||||||
|
NUMBER_LITERAL = /-?\d+(\.\d+)?/
|
||||||
|
DOTDOT = /\.\./
|
||||||
|
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/
|
||||||
WHITESPACE_OR_NOTHING = /\s*/
|
WHITESPACE_OR_NOTHING = /\s*/
|
||||||
|
|
||||||
SINGLE_COMPARISON_TOKENS = [].tap do |table|
|
def initialize(input)
|
||||||
table["<".ord] = COMPARISON_LESS_THAN
|
@ss = StringScanner.new(input)
|
||||||
table[">".ord] = COMPARISON_GREATER_THAN
|
|
||||||
table.freeze
|
|
||||||
end
|
end
|
||||||
|
|
||||||
TWO_CHARS_COMPARISON_JUMP_TABLE = [].tap do |table|
|
def tokenize
|
||||||
table["=".ord] = [].tap do |sub_table|
|
@output = []
|
||||||
sub_table["=".ord] = COMPARISON_EQUAL
|
|
||||||
sub_table.freeze
|
|
||||||
end
|
|
||||||
table["!".ord] = [].tap do |sub_table|
|
|
||||||
sub_table["=".ord] = COMPARISION_NOT_EQUAL
|
|
||||||
sub_table.freeze
|
|
||||||
end
|
|
||||||
table.freeze
|
|
||||||
end
|
|
||||||
|
|
||||||
COMPARISON_JUMP_TABLE = [].tap do |table|
|
until @ss.eos?
|
||||||
table["<".ord] = [].tap do |sub_table|
|
@ss.skip(WHITESPACE_OR_NOTHING)
|
||||||
sub_table["=".ord] = COMPARISON_LESS_THAN_OR_EQUAL
|
break if @ss.eos?
|
||||||
sub_table[">".ord] = COMPARISON_NOT_EQUAL_ALT
|
tok = if (t = @ss.scan(COMPARISON_OPERATOR))
|
||||||
sub_table.freeze
|
[:comparison, t]
|
||||||
end
|
elsif (t = @ss.scan(SINGLE_STRING_LITERAL))
|
||||||
table[">".ord] = [].tap do |sub_table|
|
[:string, t]
|
||||||
sub_table["=".ord] = COMPARISON_GREATER_THAN_OR_EQUAL
|
elsif (t = @ss.scan(DOUBLE_STRING_LITERAL))
|
||||||
sub_table.freeze
|
[:string, t]
|
||||||
end
|
elsif (t = @ss.scan(NUMBER_LITERAL))
|
||||||
table.freeze
|
[:number, t]
|
||||||
end
|
elsif (t = @ss.scan(IDENTIFIER))
|
||||||
|
[:id, t]
|
||||||
NEXT_MATCHER_JUMP_TABLE = [].tap do |table|
|
elsif (t = @ss.scan(DOTDOT))
|
||||||
"a".upto("z") do |c|
|
[:dotdot, t]
|
||||||
table[c.ord] = [:id, IDENTIFIER].freeze
|
else
|
||||||
table[c.upcase.ord] = [:id, IDENTIFIER].freeze
|
c = @ss.getch
|
||||||
end
|
if (s = SPECIALS[c])
|
||||||
table["_".ord] = [:id, IDENTIFIER].freeze
|
[s, c]
|
||||||
|
|
||||||
"0".upto("9") do |c|
|
|
||||||
table[c.ord] = [:number, NUMBER_LITERAL].freeze
|
|
||||||
end
|
|
||||||
table["-".ord] = [:number, NUMBER_LITERAL].freeze
|
|
||||||
|
|
||||||
table["'".ord] = [:string, SINGLE_STRING_LITERAL].freeze
|
|
||||||
table["\"".ord] = [:string, DOUBLE_STRING_LITERAL].freeze
|
|
||||||
table.freeze
|
|
||||||
end
|
|
||||||
|
|
||||||
SPECIAL_TABLE = [].tap do |table|
|
|
||||||
table["|".ord] = PIPE
|
|
||||||
table[".".ord] = DOT
|
|
||||||
table[":".ord] = COLON
|
|
||||||
table[",".ord] = COMMA
|
|
||||||
table["[".ord] = OPEN_SQUARE
|
|
||||||
table["]".ord] = CLOSE_SQUARE
|
|
||||||
table["(".ord] = OPEN_ROUND
|
|
||||||
table[")".ord] = CLOSE_ROUND
|
|
||||||
table["?".ord] = QUESTION
|
|
||||||
table["-".ord] = DASH
|
|
||||||
end
|
|
||||||
|
|
||||||
NUMBER_TABLE = [].tap do |table|
|
|
||||||
"0".upto("9") do |c|
|
|
||||||
table[c.ord] = true
|
|
||||||
end
|
|
||||||
table.freeze
|
|
||||||
end
|
|
||||||
|
|
||||||
# rubocop:disable Metrics/BlockNesting
|
|
||||||
class << self
|
|
||||||
def tokenize(ss)
|
|
||||||
output = []
|
|
||||||
|
|
||||||
until ss.eos?
|
|
||||||
ss.skip(WHITESPACE_OR_NOTHING)
|
|
||||||
|
|
||||||
break if ss.eos?
|
|
||||||
|
|
||||||
start_pos = ss.pos
|
|
||||||
peeked = ss.peek_byte
|
|
||||||
|
|
||||||
if (special = SPECIAL_TABLE[peeked])
|
|
||||||
ss.scan_byte
|
|
||||||
# Special case for ".."
|
|
||||||
if special == DOT && ss.peek_byte == DOT_ORD
|
|
||||||
ss.scan_byte
|
|
||||||
output << DOTDOT
|
|
||||||
elsif special == DASH
|
|
||||||
# Special case for negative numbers
|
|
||||||
if (peeked_byte = ss.peek_byte) && NUMBER_TABLE[peeked_byte]
|
|
||||||
ss.pos -= 1
|
|
||||||
output << [:number, ss.scan(NUMBER_LITERAL)]
|
|
||||||
else
|
|
||||||
output << special
|
|
||||||
end
|
|
||||||
else
|
|
||||||
output << special
|
|
||||||
end
|
|
||||||
elsif (sub_table = TWO_CHARS_COMPARISON_JUMP_TABLE[peeked])
|
|
||||||
ss.scan_byte
|
|
||||||
if (peeked_byte = ss.peek_byte) && (found = sub_table[peeked_byte])
|
|
||||||
output << found
|
|
||||||
ss.scan_byte
|
|
||||||
else
|
|
||||||
raise_syntax_error(start_pos, ss)
|
|
||||||
end
|
|
||||||
elsif (sub_table = COMPARISON_JUMP_TABLE[peeked])
|
|
||||||
ss.scan_byte
|
|
||||||
if (peeked_byte = ss.peek_byte) && (found = sub_table[peeked_byte])
|
|
||||||
output << found
|
|
||||||
ss.scan_byte
|
|
||||||
else
|
|
||||||
output << SINGLE_COMPARISON_TOKENS[peeked]
|
|
||||||
end
|
|
||||||
else
|
else
|
||||||
type, pattern = NEXT_MATCHER_JUMP_TABLE[peeked]
|
raise SyntaxError, "Unexpected character #{c}"
|
||||||
|
|
||||||
if type && (t = ss.scan(pattern))
|
|
||||||
# Special case for "contains"
|
|
||||||
output << if type == :id && t == "contains" && output.last&.first != :dot
|
|
||||||
COMPARISON_CONTAINS
|
|
||||||
else
|
|
||||||
[type, t]
|
|
||||||
end
|
|
||||||
else
|
|
||||||
raise_syntax_error(start_pos, ss)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
# rubocop:enable Metrics/BlockNesting
|
@output << tok
|
||||||
output << EOS
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def raise_syntax_error(start_pos, ss)
|
@output << [:end_of_string]
|
||||||
ss.pos = start_pos
|
|
||||||
# the character could be a UTF-8 character, use getch to get all the bytes
|
|
||||||
raise SyntaxError, "Unexpected character #{ss.getch}"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -13,17 +13,16 @@
|
|||||||
for_invalid_attribute: "Invalid attribute in for loop. Valid attributes are limit and offset"
|
for_invalid_attribute: "Invalid attribute in for loop. Valid attributes are limit and offset"
|
||||||
if: "Syntax Error in tag 'if' - Valid syntax: if [expression]"
|
if: "Syntax Error in tag 'if' - Valid syntax: if [expression]"
|
||||||
include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]"
|
include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]"
|
||||||
inline_comment_invalid: "Syntax error in tag '#' - Each line of comments must be prefixed by the '#' character"
|
unknown_tag: "Unknown tag '%{tag}'"
|
||||||
invalid_delimiter: "'%{tag}' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}"
|
invalid_delimiter: "'%{tag}' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}"
|
||||||
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_else: "%{block_name} tag does not expect 'else' tag"
|
||||||
unexpected_outer_tag: "Unexpected outer '%{tag}' tag"
|
unexpected_outer_tag: "Unexpected outer '%{tag}' tag"
|
||||||
unknown_tag: "Unknown tag '%{tag}'"
|
tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"
|
||||||
variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}"
|
variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}"
|
||||||
|
tag_never_closed: "'%{block_name}' tag was never closed"
|
||||||
|
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:
|
argument:
|
||||||
include: "Argument error in tag 'include' - Illegal template name"
|
include: "Argument error in tag 'include' - Illegal template name"
|
||||||
disabled:
|
disabled:
|
||||||
|
|||||||
@@ -3,27 +3,14 @@
|
|||||||
module Liquid
|
module Liquid
|
||||||
class ParseContext
|
class ParseContext
|
||||||
attr_accessor :locale, :line_number, :trim_whitespace, :depth
|
attr_accessor :locale, :line_number, :trim_whitespace, :depth
|
||||||
attr_reader :partial, :warnings, :error_mode, :environment
|
attr_reader :partial, :warnings, :error_mode
|
||||||
|
|
||||||
def initialize(options = Const::EMPTY_HASH)
|
def initialize(options = {})
|
||||||
@environment = options.fetch(:environment, Environment.default)
|
|
||||||
@template_options = options ? options.dup : {}
|
@template_options = options ? options.dup : {}
|
||||||
|
|
||||||
@locale = @template_options[:locale] ||= I18n.new
|
@locale = @template_options[:locale] ||= I18n.new
|
||||||
@warnings = []
|
@warnings = []
|
||||||
|
|
||||||
# constructing new StringScanner in Lexer, Tokenizer, etc is expensive
|
|
||||||
# This StringScanner will be shared by all of them
|
|
||||||
@string_scanner = StringScanner.new("")
|
|
||||||
|
|
||||||
@expression_cache = if options[:expression_cache].nil?
|
|
||||||
{}
|
|
||||||
elsif options[:expression_cache].respond_to?(:[]) && options[:expression_cache].respond_to?(:[]=)
|
|
||||||
options[:expression_cache]
|
|
||||||
elsif options[:expression_cache]
|
|
||||||
{}
|
|
||||||
end
|
|
||||||
|
|
||||||
self.depth = 0
|
self.depth = 0
|
||||||
self.partial = false
|
self.partial = false
|
||||||
end
|
end
|
||||||
@@ -32,33 +19,11 @@ module Liquid
|
|||||||
@options[option_key]
|
@options[option_key]
|
||||||
end
|
end
|
||||||
|
|
||||||
def new_block_body
|
|
||||||
Liquid::BlockBody.new
|
|
||||||
end
|
|
||||||
|
|
||||||
def new_parser(input)
|
|
||||||
@string_scanner.string = input
|
|
||||||
Parser.new(@string_scanner)
|
|
||||||
end
|
|
||||||
|
|
||||||
def new_tokenizer(source, start_line_number: nil, for_liquid_tag: false)
|
|
||||||
Tokenizer.new(
|
|
||||||
source: source,
|
|
||||||
string_scanner: @string_scanner,
|
|
||||||
line_number: start_line_number,
|
|
||||||
for_liquid_tag: for_liquid_tag,
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def parse_expression(markup)
|
|
||||||
Expression.parse(markup, @string_scanner, @expression_cache)
|
|
||||||
end
|
|
||||||
|
|
||||||
def partial=(value)
|
def partial=(value)
|
||||||
@partial = value
|
@partial = value
|
||||||
@options = value ? partial_options : @template_options
|
@options = value ? partial_options : @template_options
|
||||||
|
|
||||||
@error_mode = @options[:error_mode] || @environment.error_mode
|
@error_mode = @options[:error_mode] || Template.error_mode
|
||||||
end
|
end
|
||||||
|
|
||||||
def partial_options
|
def partial_options
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ module Liquid
|
|||||||
protected
|
protected
|
||||||
|
|
||||||
def children
|
def children
|
||||||
@node.respond_to?(:nodelist) ? Array(@node.nodelist) : Const::EMPTY_ARRAY
|
@node.respond_to?(:nodelist) ? Array(@node.nodelist) : []
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+19
-26
@@ -3,8 +3,8 @@
|
|||||||
module Liquid
|
module Liquid
|
||||||
class Parser
|
class Parser
|
||||||
def initialize(input)
|
def initialize(input)
|
||||||
ss = input.is_a?(StringScanner) ? input : StringScanner.new(input)
|
l = Lexer.new(input)
|
||||||
@tokens = Lexer.tokenize(ss)
|
@tokens = l.tokenize
|
||||||
@p = 0 # pointer to current location
|
@p = 0 # pointer to current location
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -46,20 +46,16 @@ module Liquid
|
|||||||
tok[0] == type
|
tok[0] == type
|
||||||
end
|
end
|
||||||
|
|
||||||
|
SINGLE_TOKEN_EXPRESSION_TYPES = [:string, :number].freeze
|
||||||
|
private_constant :SINGLE_TOKEN_EXPRESSION_TYPES
|
||||||
|
|
||||||
def expression
|
def expression
|
||||||
token = @tokens[@p]
|
token = @tokens[@p]
|
||||||
case token[0]
|
if token[0] == :id
|
||||||
when :id
|
variable_signature
|
||||||
str = consume
|
elsif SINGLE_TOKEN_EXPRESSION_TYPES.include?(token[0])
|
||||||
str << variable_lookups
|
|
||||||
when :open_square
|
|
||||||
str = consume.dup
|
|
||||||
str << expression
|
|
||||||
str << consume(:close_square)
|
|
||||||
str << variable_lookups
|
|
||||||
when :string, :number
|
|
||||||
consume
|
consume
|
||||||
when :open_round
|
elsif token.first == :open_round
|
||||||
consume
|
consume
|
||||||
first = expression
|
first = expression
|
||||||
consume(:dotdot)
|
consume(:dotdot)
|
||||||
@@ -82,19 +78,16 @@ module Liquid
|
|||||||
str
|
str
|
||||||
end
|
end
|
||||||
|
|
||||||
def variable_lookups
|
def variable_signature
|
||||||
str = +""
|
str = consume(:id)
|
||||||
loop do
|
while look(:open_square)
|
||||||
if look(:open_square)
|
str << consume
|
||||||
str << consume
|
str << expression
|
||||||
str << expression
|
str << consume(:close_square)
|
||||||
str << consume(:close_square)
|
end
|
||||||
elsif look(:dot)
|
if look(:dot)
|
||||||
str << consume
|
str << consume
|
||||||
str << consume(:id)
|
str << variable_signature
|
||||||
else
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
str
|
str
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,18 +2,6 @@
|
|||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
module ParserSwitching
|
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)
|
def parse_with_selected_parser(markup)
|
||||||
case parse_context.error_mode
|
case parse_context.error_mode
|
||||||
when :strict then strict_parse_with_error_context(markup)
|
when :strict then strict_parse_with_error_context(markup)
|
||||||
|
|||||||
@@ -3,29 +3,20 @@
|
|||||||
module Liquid
|
module Liquid
|
||||||
class PartialCache
|
class PartialCache
|
||||||
def self.load(template_name, context:, parse_context:)
|
def self.load(template_name, context:, parse_context:)
|
||||||
cached_partials = context.registers[:cached_partials]
|
cached_partials = (context.registers[:cached_partials] ||= {})
|
||||||
cache_key = "#{template_name}:#{parse_context.error_mode}"
|
cached = cached_partials[template_name]
|
||||||
cached = cached_partials[cache_key]
|
|
||||||
return cached if cached
|
return cached if cached
|
||||||
|
|
||||||
file_system = context.registers[:file_system]
|
file_system = (context.registers[:file_system] ||= Liquid::Template.file_system)
|
||||||
source = file_system.read_template_file(template_name)
|
source = file_system.read_template_file(template_name)
|
||||||
|
|
||||||
parse_context.partial = true
|
parse_context.partial = true
|
||||||
|
|
||||||
template_factory = context.registers[:template_factory]
|
template_factory = (context.registers[:template_factory] ||= Liquid::TemplateFactory.new)
|
||||||
template = template_factory.for(template_name)
|
template = template_factory.for(template_name)
|
||||||
|
|
||||||
begin
|
partial = template.parse(source, parse_context)
|
||||||
partial = template.parse(source, parse_context)
|
cached_partials[template_name] = partial
|
||||||
rescue Liquid::Error => e
|
|
||||||
e.template_name = template&.name || template_name
|
|
||||||
raise e
|
|
||||||
end
|
|
||||||
|
|
||||||
partial.name ||= template_name
|
|
||||||
|
|
||||||
cached_partials[cache_key] = partial
|
|
||||||
ensure
|
ensure
|
||||||
parse_context.partial = false
|
parse_context.partial = false
|
||||||
end
|
end
|
||||||
|
|||||||
+95
-63
@@ -25,7 +25,7 @@ module Liquid
|
|||||||
# node.code
|
# node.code
|
||||||
#
|
#
|
||||||
# # Which template and line number of this node.
|
# # 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.partial
|
||||||
# node.line_number
|
# node.line_number
|
||||||
#
|
#
|
||||||
@@ -46,94 +46,126 @@ module Liquid
|
|||||||
include Enumerable
|
include Enumerable
|
||||||
|
|
||||||
class Timing
|
class Timing
|
||||||
attr_reader :code, :template_name, :line_number, :children
|
attr_reader :code, :partial, :line_number, :children, :total_time, :self_time
|
||||||
attr_accessor :total_time
|
|
||||||
alias_method :render_time, :total_time
|
|
||||||
alias_method :partial, :template_name
|
|
||||||
|
|
||||||
def initialize(code: nil, template_name: nil, line_number: nil)
|
def initialize(node, partial)
|
||||||
@code = code
|
@code = node.respond_to?(:raw) ? node.raw : node
|
||||||
@template_name = template_name
|
@partial = partial
|
||||||
@line_number = line_number
|
@line_number = node.respond_to?(:line_number) ? node.line_number : nil
|
||||||
@children = []
|
@children = []
|
||||||
end
|
end
|
||||||
|
|
||||||
def self_time
|
def self.start(node, partial)
|
||||||
@self_time ||= begin
|
new(node, partial).tap(&:start)
|
||||||
total_children_time = 0.0
|
end
|
||||||
|
|
||||||
|
def start
|
||||||
|
@start_time = Time.now
|
||||||
|
end
|
||||||
|
|
||||||
|
def finish
|
||||||
|
@end_time = Time.now
|
||||||
|
@total_time = @end_time - @start_time
|
||||||
|
|
||||||
|
if @children.empty?
|
||||||
|
@self_time = @total_time
|
||||||
|
else
|
||||||
|
total_children_time = 0
|
||||||
@children.each do |child|
|
@children.each do |child|
|
||||||
total_children_time += child.total_time
|
total_children_time += child.total_time
|
||||||
end
|
end
|
||||||
@total_time - total_children_time
|
@self_time = @total_time - total_children_time
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def render_time
|
||||||
|
@end_time - @start_time
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
attr_reader :total_time
|
def self.profile_node_render(node)
|
||||||
alias_method :total_render_time, :total_time
|
if Profiler.current_profile && node.respond_to?(:render)
|
||||||
|
Profiler.current_profile.start_node(node)
|
||||||
def initialize
|
output = yield
|
||||||
@root_children = []
|
Profiler.current_profile.end_node(node)
|
||||||
@current_children = nil
|
output
|
||||||
@total_time = 0.0
|
|
||||||
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
|
|
||||||
end
|
|
||||||
|
|
||||||
def children
|
|
||||||
children = @root_children
|
|
||||||
if children.length == 1
|
|
||||||
children.first.children
|
|
||||||
else
|
else
|
||||||
children
|
yield
|
||||||
end
|
end
|
||||||
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(partial_name = "<root>")
|
||||||
|
@partial_stack = [partial_name]
|
||||||
|
|
||||||
|
@root_timing = Timing.new("", current_partial)
|
||||||
|
@timing_stack = [@root_timing]
|
||||||
|
|
||||||
|
@render_start_at = Time.now
|
||||||
|
@render_end_at = @render_start_at
|
||||||
|
end
|
||||||
|
|
||||||
|
def start
|
||||||
|
Thread.current[:liquid_profiler] = self
|
||||||
|
@render_start_at = Time.now
|
||||||
|
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)
|
def each(&block)
|
||||||
children.each(&block)
|
@root_timing.children.each(&block)
|
||||||
end
|
end
|
||||||
|
|
||||||
def [](idx)
|
def [](idx)
|
||||||
children[idx]
|
@root_timing.children[idx]
|
||||||
end
|
end
|
||||||
|
|
||||||
def length
|
def length
|
||||||
children.length
|
@root_timing.children.length
|
||||||
end
|
end
|
||||||
|
|
||||||
def profile_node(template_name, code: nil, line_number: nil)
|
def start_node(node)
|
||||||
timing = Timing.new(code: code, template_name: template_name, line_number: line_number)
|
@timing_stack.push(Timing.start(node, current_partial))
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
def end_node(_node)
|
||||||
|
timing = @timing_stack.pop
|
||||||
|
timing.finish
|
||||||
|
|
||||||
def monotonic_time
|
@timing_stack.last.children << timing
|
||||||
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,33 +3,19 @@
|
|||||||
module Liquid
|
module Liquid
|
||||||
module BlockBodyProfilingHook
|
module BlockBodyProfilingHook
|
||||||
def render_node(context, output, node)
|
def render_node(context, output, node)
|
||||||
if (profiler = context.profiler)
|
Profiler.profile_node_render(node) do
|
||||||
profiler.profile_node(context.template_name, code: node.raw, line_number: node.line_number) do
|
|
||||||
super
|
|
||||||
end
|
|
||||||
else
|
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
BlockBody.prepend(BlockBodyProfilingHook)
|
BlockBody.prepend(BlockBodyProfilingHook)
|
||||||
|
|
||||||
module DocumentProfilingHook
|
module IncludeProfilingHook
|
||||||
def render_to_output_buffer(context, output)
|
def render_to_output_buffer(context, output)
|
||||||
return super unless context.profiler
|
Profiler.profile_children(context.evaluate(@template_name_expr).to_s) do
|
||||||
context.profiler.profile(context.template_name) { super }
|
super
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
Document.prepend(DocumentProfilingHook)
|
Include.prepend(IncludeProfilingHook)
|
||||||
|
|
||||||
module ContextProfilingHook
|
|
||||||
attr_accessor :profiler
|
|
||||||
|
|
||||||
def new_isolated_subcontext
|
|
||||||
new_context = super
|
|
||||||
new_context.profiler = profiler
|
|
||||||
new_context
|
|
||||||
end
|
|
||||||
end
|
|
||||||
Context.prepend(ContextProfilingHook)
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,28 +2,16 @@
|
|||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
class RangeLookup
|
class RangeLookup
|
||||||
def self.parse(start_markup, end_markup, string_scanner, cache = nil)
|
def self.parse(start_markup, end_markup)
|
||||||
start_obj = Expression.parse(start_markup, string_scanner, cache)
|
start_obj = Expression.parse(start_markup)
|
||||||
end_obj = Expression.parse(end_markup, string_scanner, cache)
|
end_obj = Expression.parse(end_markup)
|
||||||
if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate)
|
if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate)
|
||||||
new(start_obj, end_obj)
|
new(start_obj, end_obj)
|
||||||
else
|
else
|
||||||
begin
|
start_obj.to_i..end_obj.to_i
|
||||||
start_obj.to_i..end_obj.to_i
|
|
||||||
rescue NoMethodError
|
|
||||||
invalid_expr = start_markup unless start_obj.respond_to?(:to_i)
|
|
||||||
invalid_expr ||= end_markup unless end_obj.respond_to?(:to_i)
|
|
||||||
if invalid_expr
|
|
||||||
raise Liquid::SyntaxError, "Invalid expression type '#{invalid_expr}' in range expression"
|
|
||||||
end
|
|
||||||
|
|
||||||
raise
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
attr_reader :start_obj, :end_obj
|
|
||||||
|
|
||||||
def initialize(start_obj, end_obj)
|
def initialize(start_obj, end_obj)
|
||||||
@start_obj = start_obj
|
@start_obj = start_obj
|
||||||
@end_obj = end_obj
|
@end_obj = end_obj
|
||||||
@@ -47,11 +35,5 @@ module Liquid
|
|||||||
Utils.to_integer(input)
|
Utils.to_integer(input)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
|
||||||
def children
|
|
||||||
[@node.start_obj, @node.end_obj]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Liquid
|
||||||
|
class Register
|
||||||
|
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
|
|
||||||
+122
-696
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,44 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Liquid
|
||||||
|
class StaticRegisters
|
||||||
|
attr_reader :static
|
||||||
|
|
||||||
|
def initialize(registers = {})
|
||||||
|
@static = registers.is_a?(StaticRegisters) ? registers.static : registers
|
||||||
|
@registers = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
def []=(key, value)
|
||||||
|
@registers[key] = value
|
||||||
|
end
|
||||||
|
|
||||||
|
def [](key)
|
||||||
|
if @registers.key?(key)
|
||||||
|
@registers[key]
|
||||||
|
else
|
||||||
|
@static[key]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete(key)
|
||||||
|
@registers.delete(key)
|
||||||
|
end
|
||||||
|
|
||||||
|
UNDEFINED = Object.new
|
||||||
|
|
||||||
|
def fetch(key, default = UNDEFINED, &block)
|
||||||
|
if @registers.key?(key)
|
||||||
|
@registers.fetch(key)
|
||||||
|
elsif default != UNDEFINED
|
||||||
|
@static.fetch(key, default, &block)
|
||||||
|
else
|
||||||
|
@static.fetch(key, &block)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def key?(key)
|
||||||
|
@registers.key?(key) || @static.key?(key)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# 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
|
||||||
|
global_filters << filter
|
||||||
|
end
|
||||||
|
|
||||||
|
def create(context, filters = [])
|
||||||
|
strainer_from_cache(filters).new(context)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def global_filters
|
||||||
|
@global_filters ||= []
|
||||||
|
end
|
||||||
|
|
||||||
|
def strainer_from_cache(filters)
|
||||||
|
strainer_class_cache[filters] ||= begin
|
||||||
|
klass = Class.new(StrainerTemplate)
|
||||||
|
global_filters.each { |f| klass.add_filter(f) }
|
||||||
|
filters.each { |f| klass.add_filter(f) }
|
||||||
|
klass
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def strainer_class_cache
|
||||||
|
@strainer_class_cache ||= {}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -31,15 +31,6 @@ module Liquid
|
|||||||
filter_methods.include?(method.to_s)
|
filter_methods.include?(method.to_s)
|
||||||
end
|
end
|
||||||
|
|
||||||
def inherited(subclass)
|
|
||||||
super
|
|
||||||
subclass.instance_variable_set(:@filter_methods, @filter_methods.dup)
|
|
||||||
end
|
|
||||||
|
|
||||||
def filter_method_names
|
|
||||||
filter_methods.map(&:to_s).to_a
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def filter_methods
|
def filter_methods
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
# @liquid_public_docs
|
|
||||||
# @liquid_type object
|
|
||||||
# @liquid_name tablerowloop
|
|
||||||
# @liquid_summary
|
|
||||||
# Information about a parent [`tablerow` loop](/docs/api/liquid/tags/tablerow).
|
|
||||||
class TablerowloopDrop < Drop
|
class TablerowloopDrop < Drop
|
||||||
def initialize(length, cols)
|
def initialize(length, cols)
|
||||||
@length = length
|
@length = length
|
||||||
@@ -15,92 +10,40 @@ module Liquid
|
|||||||
@index = 0
|
@index = 0
|
||||||
end
|
end
|
||||||
|
|
||||||
# @liquid_public_docs
|
attr_reader :length, :col, :row
|
||||||
# @liquid_summary
|
|
||||||
# The total number of iterations in the loop.
|
|
||||||
# @liquid_return [number]
|
|
||||||
attr_reader :length
|
|
||||||
|
|
||||||
# @liquid_public_docs
|
|
||||||
# @liquid_summary
|
|
||||||
# The 1-based index of the current column.
|
|
||||||
# @liquid_return [number]
|
|
||||||
attr_reader :col
|
|
||||||
|
|
||||||
# @liquid_public_docs
|
|
||||||
# @liquid_summary
|
|
||||||
# The 1-based index of current row.
|
|
||||||
# @liquid_return [number]
|
|
||||||
attr_reader :row
|
|
||||||
|
|
||||||
# @liquid_public_docs
|
|
||||||
# @liquid_summary
|
|
||||||
# The 1-based index of the current iteration.
|
|
||||||
# @liquid_return [number]
|
|
||||||
def index
|
def index
|
||||||
@index + 1
|
@index + 1
|
||||||
end
|
end
|
||||||
|
|
||||||
# @liquid_public_docs
|
|
||||||
# @liquid_summary
|
|
||||||
# The 0-based index of the current iteration.
|
|
||||||
# @liquid_return [number]
|
|
||||||
def index0
|
def index0
|
||||||
@index
|
@index
|
||||||
end
|
end
|
||||||
|
|
||||||
# @liquid_public_docs
|
|
||||||
# @liquid_summary
|
|
||||||
# The 0-based index of the current column.
|
|
||||||
# @liquid_return [number]
|
|
||||||
def col0
|
def col0
|
||||||
@col - 1
|
@col - 1
|
||||||
end
|
end
|
||||||
|
|
||||||
# @liquid_public_docs
|
|
||||||
# @liquid_summary
|
|
||||||
# The 1-based index of the current iteration, in reverse order.
|
|
||||||
# @liquid_return [number]
|
|
||||||
def rindex
|
def rindex
|
||||||
@length - @index
|
@length - @index
|
||||||
end
|
end
|
||||||
|
|
||||||
# @liquid_public_docs
|
|
||||||
# @liquid_summary
|
|
||||||
# The 0-based index of the current iteration, in reverse order.
|
|
||||||
# @liquid_return [number]
|
|
||||||
def rindex0
|
def rindex0
|
||||||
@length - @index - 1
|
@length - @index - 1
|
||||||
end
|
end
|
||||||
|
|
||||||
# @liquid_public_docs
|
|
||||||
# @liquid_summary
|
|
||||||
# Returns `true` if the current iteration is the first. Returns `false` if not.
|
|
||||||
# @liquid_return [boolean]
|
|
||||||
def first
|
def first
|
||||||
@index == 0
|
@index == 0
|
||||||
end
|
end
|
||||||
|
|
||||||
# @liquid_public_docs
|
|
||||||
# @liquid_summary
|
|
||||||
# Returns `true` if the current iteration is the last. Returns `false` if not.
|
|
||||||
# @liquid_return [boolean]
|
|
||||||
def last
|
def last
|
||||||
@index == @length - 1
|
@index == @length - 1
|
||||||
end
|
end
|
||||||
|
|
||||||
# @liquid_public_docs
|
|
||||||
# @liquid_summary
|
|
||||||
# Returns `true` if the current column is the first in the row. Returns `false` if not.
|
|
||||||
# @liquid_return [boolean]
|
|
||||||
def col_first
|
def col_first
|
||||||
@col == 1
|
@col == 1
|
||||||
end
|
end
|
||||||
|
|
||||||
# @liquid_public_docs
|
|
||||||
# @liquid_summary
|
|
||||||
# Returns `true` if the current column is the last in the row. Returns `false` if not.
|
|
||||||
# @liquid_return [boolean]
|
|
||||||
def col_last
|
def col_last
|
||||||
@col == @cols
|
@col == @cols
|
||||||
end
|
end
|
||||||
|
|||||||
+3
-19
@@ -1,8 +1,5 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'liquid/tag/disabler'
|
|
||||||
require 'liquid/tag/disableable'
|
|
||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
class Tag
|
class Tag
|
||||||
attr_reader :nodelist, :tag_name, :line_number, :parse_context
|
attr_reader :nodelist, :tag_name, :line_number, :parse_context
|
||||||
@@ -17,18 +14,12 @@ module Liquid
|
|||||||
end
|
end
|
||||||
|
|
||||||
def disable_tags(*tag_names)
|
def disable_tags(*tag_names)
|
||||||
tag_names += disabled_tags
|
@disabled_tags ||= []
|
||||||
define_singleton_method(:disabled_tags) { tag_names }
|
@disabled_tags.concat(tag_names)
|
||||||
prepend(Disabler)
|
prepend(Disabler)
|
||||||
end
|
end
|
||||||
|
|
||||||
private :new
|
private :new
|
||||||
|
|
||||||
protected
|
|
||||||
|
|
||||||
def disabled_tags
|
|
||||||
[]
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def initialize(tag_name, markup, parse_context)
|
def initialize(tag_name, markup, parse_context)
|
||||||
@@ -57,19 +48,12 @@ module Liquid
|
|||||||
# of the `render_to_output_buffer` method will become the default and the `render`
|
# of the `render_to_output_buffer` method will become the default and the `render`
|
||||||
# method will be removed.
|
# method will be removed.
|
||||||
def render_to_output_buffer(context, output)
|
def render_to_output_buffer(context, output)
|
||||||
render_result = render(context)
|
output << render(context)
|
||||||
output << render_result if render_result
|
|
||||||
output
|
output
|
||||||
end
|
end
|
||||||
|
|
||||||
def blank?
|
def blank?
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def parse_expression(markup)
|
|
||||||
parse_context.parse_expression(markup)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,6 +3,14 @@
|
|||||||
module Liquid
|
module Liquid
|
||||||
class Tag
|
class Tag
|
||||||
module Disabler
|
module Disabler
|
||||||
|
module ClassMethods
|
||||||
|
attr_reader :disabled_tags
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.prepended(base)
|
||||||
|
base.extend(ClassMethods)
|
||||||
|
end
|
||||||
|
|
||||||
def render_to_output_buffer(context, output)
|
def render_to_output_buffer(context, output)
|
||||||
context.with_disabled_tags(self.class.disabled_tags) do
|
context.with_disabled_tags(self.class.disabled_tags) do
|
||||||
super
|
super
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require_relative "tags/table_row"
|
|
||||||
require_relative "tags/echo"
|
|
||||||
require_relative "tags/if"
|
|
||||||
require_relative "tags/break"
|
|
||||||
require_relative "tags/inline_comment"
|
|
||||||
require_relative "tags/for"
|
|
||||||
require_relative "tags/assign"
|
|
||||||
require_relative "tags/ifchanged"
|
|
||||||
require_relative "tags/case"
|
|
||||||
require_relative "tags/include"
|
|
||||||
require_relative "tags/continue"
|
|
||||||
require_relative "tags/capture"
|
|
||||||
require_relative "tags/decrement"
|
|
||||||
require_relative "tags/unless"
|
|
||||||
require_relative "tags/increment"
|
|
||||||
require_relative "tags/comment"
|
|
||||||
require_relative "tags/raw"
|
|
||||||
require_relative "tags/render"
|
|
||||||
require_relative "tags/cycle"
|
|
||||||
|
|
||||||
module Liquid
|
|
||||||
module Tags
|
|
||||||
STANDARD_TAGS = {
|
|
||||||
'cycle' => Cycle,
|
|
||||||
'render' => Render,
|
|
||||||
'raw' => Raw,
|
|
||||||
'comment' => Comment,
|
|
||||||
'increment' => Increment,
|
|
||||||
'unless' => Unless,
|
|
||||||
'decrement' => Decrement,
|
|
||||||
'capture' => Capture,
|
|
||||||
'continue' => Continue,
|
|
||||||
'include' => Include,
|
|
||||||
'case' => Case,
|
|
||||||
'ifchanged' => Ifchanged,
|
|
||||||
'assign' => Assign,
|
|
||||||
'for' => For,
|
|
||||||
'#' => InlineComment,
|
|
||||||
'break' => Break,
|
|
||||||
'if' => If,
|
|
||||||
'echo' => Echo,
|
|
||||||
'tablerow' => TableRow,
|
|
||||||
}.freeze
|
|
||||||
end
|
|
||||||
end
|
|
||||||
+14
-28
@@ -1,35 +1,26 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
# @liquid_public_docs
|
# Assign sets a variable in your template.
|
||||||
# @liquid_type tag
|
#
|
||||||
# @liquid_category variable
|
# {% assign foo = 'monkey' %}
|
||||||
# @liquid_name assign
|
#
|
||||||
# @liquid_summary
|
# You can then use the variable later in the page.
|
||||||
# Creates a new variable.
|
#
|
||||||
# @liquid_description
|
# {{ foo }}
|
||||||
# You can create variables of any [basic type](/docs/api/liquid/basics#types), [object](/docs/api/liquid/objects), or object property.
|
#
|
||||||
# @liquid_syntax
|
|
||||||
# {% assign variable_name = value %}
|
|
||||||
# @liquid_syntax_keyword variable_name The name of the variable being created.
|
|
||||||
# @liquid_syntax_keyword value The value you want to assign to the variable.
|
|
||||||
class Assign < Tag
|
class Assign < Tag
|
||||||
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
|
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
|
||||||
|
|
||||||
# @api private
|
|
||||||
def self.raise_syntax_error(parse_context)
|
|
||||||
raise Liquid::SyntaxError, parse_context.locale.t('errors.syntax.assign')
|
|
||||||
end
|
|
||||||
|
|
||||||
attr_reader :to, :from
|
attr_reader :to, :from
|
||||||
|
|
||||||
def initialize(tag_name, markup, parse_context)
|
def initialize(tag_name, markup, options)
|
||||||
super
|
super
|
||||||
if markup =~ Syntax
|
if markup =~ Syntax
|
||||||
@to = Regexp.last_match(1)
|
@to = Regexp.last_match(1)
|
||||||
@from = Variable.new(Regexp.last_match(2), parse_context)
|
@from = Variable.new(Regexp.last_match(2), options)
|
||||||
else
|
else
|
||||||
self.class.raise_syntax_error(parse_context)
|
raise SyntaxError, options[:locale].t('errors.syntax.assign')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -49,18 +40,11 @@ module Liquid
|
|||||||
def assign_score_of(val)
|
def assign_score_of(val)
|
||||||
if val.instance_of?(String)
|
if val.instance_of?(String)
|
||||||
val.bytesize
|
val.bytesize
|
||||||
elsif val.instance_of?(Array)
|
elsif val.instance_of?(Array) || val.instance_of?(Hash)
|
||||||
sum = 1
|
sum = 1
|
||||||
# Uses #each to avoid extra allocations.
|
# Uses #each to avoid extra allocations.
|
||||||
val.each { |child| sum += assign_score_of(child) }
|
val.each { |child| sum += assign_score_of(child) }
|
||||||
sum
|
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
|
else
|
||||||
1
|
1
|
||||||
end
|
end
|
||||||
@@ -72,4 +56,6 @@ module Liquid
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Template.register_tag('assign', Assign)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -10,14 +10,6 @@ module Liquid
|
|||||||
# {% endif %}
|
# {% endif %}
|
||||||
# {% endfor %}
|
# {% endfor %}
|
||||||
#
|
#
|
||||||
# @liquid_public_docs
|
|
||||||
# @liquid_type tag
|
|
||||||
# @liquid_category iteration
|
|
||||||
# @liquid_name break
|
|
||||||
# @liquid_summary
|
|
||||||
# Stops a [`for` loop](/docs/api/liquid/tags/for) from iterating.
|
|
||||||
# @liquid_syntax
|
|
||||||
# {% break %}
|
|
||||||
class Break < Tag
|
class Break < Tag
|
||||||
INTERRUPT = BreakInterrupt.new.freeze
|
INTERRUPT = BreakInterrupt.new.freeze
|
||||||
|
|
||||||
@@ -26,4 +18,6 @@ module Liquid
|
|||||||
output
|
output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Template.register_tag('break', Break)
|
||||||
end
|
end
|
||||||
|
|||||||
+12
-13
@@ -1,20 +1,17 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
# @liquid_public_docs
|
# Capture stores the result of a block into a variable without rendering it inplace.
|
||||||
# @liquid_type tag
|
#
|
||||||
# @liquid_category variable
|
# {% capture heading %}
|
||||||
# @liquid_name capture
|
# Monkeys!
|
||||||
# @liquid_summary
|
|
||||||
# Creates a new variable with a string value.
|
|
||||||
# @liquid_description
|
|
||||||
# You can create complex strings with Liquid logic and variables.
|
|
||||||
# @liquid_syntax
|
|
||||||
# {% capture variable %}
|
|
||||||
# value
|
|
||||||
# {% endcapture %}
|
# {% endcapture %}
|
||||||
# @liquid_syntax_keyword variable The name of the variable being created.
|
# ...
|
||||||
# @liquid_syntax_keyword value The value you want to assign to the variable.
|
# <h1>{{ heading }}</h1>
|
||||||
|
#
|
||||||
|
# Capture is useful for saving content for use later in your template, such as
|
||||||
|
# in a sidebar or footer.
|
||||||
|
#
|
||||||
class Capture < Block
|
class Capture < Block
|
||||||
Syntax = /(#{VariableSignature}+)/o
|
Syntax = /(#{VariableSignature}+)/o
|
||||||
|
|
||||||
@@ -39,4 +36,6 @@ module Liquid
|
|||||||
true
|
true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Template.register_tag('capture', Capture)
|
||||||
end
|
end
|
||||||
|
|||||||
+8
-39
@@ -1,27 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
# @liquid_public_docs
|
|
||||||
# @liquid_type tag
|
|
||||||
# @liquid_category conditional
|
|
||||||
# @liquid_name case
|
|
||||||
# @liquid_summary
|
|
||||||
# Renders a specific expression depending on the value of a specific variable.
|
|
||||||
# @liquid_syntax
|
|
||||||
# {% case variable %}
|
|
||||||
# {% when first_value %}
|
|
||||||
# first_expression
|
|
||||||
# {% when second_value %}
|
|
||||||
# second_expression
|
|
||||||
# {% else %}
|
|
||||||
# third_expression
|
|
||||||
# {% endcase %}
|
|
||||||
# @liquid_syntax_keyword variable The name of the variable you want to base your case statement on.
|
|
||||||
# @liquid_syntax_keyword first_value A specific value to check for.
|
|
||||||
# @liquid_syntax_keyword second_value A specific value to check for.
|
|
||||||
# @liquid_syntax_keyword first_expression An expression to be rendered when the variable's value matches `first_value`.
|
|
||||||
# @liquid_syntax_keyword second_expression An expression to be rendered when the variable's value matches `second_value`.
|
|
||||||
# @liquid_syntax_keyword third_expression An expression to be rendered when the variable's value has no match.
|
|
||||||
class Case < Block
|
class Case < Block
|
||||||
Syntax = /(#{QuotedFragment})/o
|
Syntax = /(#{QuotedFragment})/o
|
||||||
WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om
|
WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om
|
||||||
@@ -33,23 +12,18 @@ module Liquid
|
|||||||
@blocks = []
|
@blocks = []
|
||||||
|
|
||||||
if markup =~ Syntax
|
if markup =~ Syntax
|
||||||
@left = parse_expression(Regexp.last_match(1))
|
@left = Expression.parse(Regexp.last_match(1))
|
||||||
else
|
else
|
||||||
raise SyntaxError, options[:locale].t("errors.syntax.case")
|
raise SyntaxError, options[:locale].t("errors.syntax.case")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def parse(tokens)
|
def parse(tokens)
|
||||||
body = case_body = new_body
|
body = new_body
|
||||||
body = @blocks.last.attachment while parse_body(body, tokens)
|
body = @blocks.last.attachment while parse_body(body, tokens)
|
||||||
@blocks.reverse_each do |condition|
|
if blank?
|
||||||
body = condition.attachment
|
@blocks.each { |condition| condition.attachment.remove_blank_strings }
|
||||||
unless body.frozen?
|
|
||||||
body.remove_blank_strings if blank?
|
|
||||||
body.freeze
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
case_body.freeze
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def nodelist
|
def nodelist
|
||||||
@@ -73,14 +47,7 @@ module Liquid
|
|||||||
@blocks.each do |block|
|
@blocks.each do |block|
|
||||||
if block.else?
|
if block.else?
|
||||||
block.attachment.render_to_output_buffer(context, output) if execute_else_block
|
block.attachment.render_to_output_buffer(context, output) if execute_else_block
|
||||||
next
|
elsif block.evaluate(context)
|
||||||
end
|
|
||||||
|
|
||||||
result = Liquid::Utils.to_liquid_value(
|
|
||||||
block.evaluate(context),
|
|
||||||
)
|
|
||||||
|
|
||||||
if result
|
|
||||||
execute_else_block = false
|
execute_else_block = false
|
||||||
block.attachment.render_to_output_buffer(context, output)
|
block.attachment.render_to_output_buffer(context, output)
|
||||||
end
|
end
|
||||||
@@ -101,7 +68,7 @@ module Liquid
|
|||||||
|
|
||||||
markup = Regexp.last_match(2)
|
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)
|
block.attach(body)
|
||||||
@blocks << block
|
@blocks << block
|
||||||
end
|
end
|
||||||
@@ -123,4 +90,6 @@ module Liquid
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Template.register_tag('case', Case)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,19 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
# @liquid_public_docs
|
|
||||||
# @liquid_type tag
|
|
||||||
# @liquid_category syntax
|
|
||||||
# @liquid_name comment
|
|
||||||
# @liquid_summary
|
|
||||||
# Prevents an expression from being rendered or output.
|
|
||||||
# @liquid_description
|
|
||||||
# Any text inside `comment` tags won't be output, and any Liquid code will be parsed, but not executed.
|
|
||||||
# @liquid_syntax
|
|
||||||
# {% comment %}
|
|
||||||
# content
|
|
||||||
# {% endcomment %}
|
|
||||||
# @liquid_syntax_keyword content The content of the comment.
|
|
||||||
class Comment < Block
|
class Comment < Block
|
||||||
def render_to_output_buffer(_context, output)
|
def render_to_output_buffer(_context, output)
|
||||||
output
|
output
|
||||||
@@ -25,64 +12,7 @@ module Liquid
|
|||||||
def blank?
|
def blank?
|
||||||
true
|
true
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
|
Template.register_tag('comment', Comment)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
# @liquid_public_docs
|
# Continue tag to be used to break out of a for loop.
|
||||||
# @liquid_type tag
|
#
|
||||||
# @liquid_category iteration
|
# == Basic Usage:
|
||||||
# @liquid_name continue
|
# {% for item in collection %}
|
||||||
# @liquid_summary
|
# {% if item.condition %}
|
||||||
# Causes a [`for` loop](/docs/api/liquid/tags/for) to skip to the next iteration.
|
# {% continue %}
|
||||||
# @liquid_syntax
|
# {% endif %}
|
||||||
# {% continue %}
|
# {% endfor %}
|
||||||
|
#
|
||||||
class Continue < Tag
|
class Continue < Tag
|
||||||
INTERRUPT = ContinueInterrupt.new.freeze
|
INTERRUPT = ContinueInterrupt.new.freeze
|
||||||
|
|
||||||
@@ -17,4 +18,6 @@ module Liquid
|
|||||||
output
|
output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Template.register_tag('continue', Continue)
|
||||||
end
|
end
|
||||||
|
|||||||
+15
-26
@@ -1,19 +1,18 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
# @liquid_public_docs
|
# Cycle is usually used within a loop to alternate between values, like colors or DOM classes.
|
||||||
# @liquid_type tag
|
#
|
||||||
# @liquid_category iteration
|
# {% for item in items %}
|
||||||
# @liquid_name cycle
|
# <div class="{% cycle 'red', 'green', 'blue' %}"> {{ item }} </div>
|
||||||
# @liquid_summary
|
# {% end %}
|
||||||
# Loops through a group of strings and outputs them one at a time for each iteration of a [`for` loop](/docs/api/liquid/tags/for).
|
#
|
||||||
# @liquid_description
|
# <div class="red"> Item one </div>
|
||||||
# The `cycle` tag must be used inside a `for` loop.
|
# <div class="green"> Item two </div>
|
||||||
|
# <div class="blue"> Item three </div>
|
||||||
|
# <div class="red"> Item four </div>
|
||||||
|
# <div class="green"> Item five</div>
|
||||||
#
|
#
|
||||||
# > Tip:
|
|
||||||
# > Use the `cycle` tag to output text in a predictable pattern. For example, to apply odd/even classes to rows in a table.
|
|
||||||
# @liquid_syntax
|
|
||||||
# {% cycle string, string, ... %}
|
|
||||||
class Cycle < Tag
|
class Cycle < Tag
|
||||||
SimpleSyntax = /\A#{QuotedFragment}+/o
|
SimpleSyntax = /\A#{QuotedFragment}+/o
|
||||||
NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om
|
NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om
|
||||||
@@ -25,21 +24,15 @@ module Liquid
|
|||||||
case markup
|
case markup
|
||||||
when NamedSyntax
|
when NamedSyntax
|
||||||
@variables = variables_from_string(Regexp.last_match(2))
|
@variables = variables_from_string(Regexp.last_match(2))
|
||||||
@name = parse_expression(Regexp.last_match(1))
|
@name = Expression.parse(Regexp.last_match(1))
|
||||||
@is_named = true
|
|
||||||
when SimpleSyntax
|
when SimpleSyntax
|
||||||
@variables = variables_from_string(markup)
|
@variables = variables_from_string(markup)
|
||||||
@name = @variables.to_s
|
@name = @variables.to_s
|
||||||
@is_named = !@name.match?(/\w+:0x\h{8}/)
|
|
||||||
else
|
else
|
||||||
raise SyntaxError, options[:locale].t("errors.syntax.cycle")
|
raise SyntaxError, options[:locale].t("errors.syntax.cycle")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def named?
|
|
||||||
@is_named
|
|
||||||
end
|
|
||||||
|
|
||||||
def render_to_output_buffer(context, output)
|
def render_to_output_buffer(context, output)
|
||||||
context.registers[:cycle] ||= {}
|
context.registers[:cycle] ||= {}
|
||||||
|
|
||||||
@@ -68,13 +61,7 @@ module Liquid
|
|||||||
def variables_from_string(markup)
|
def variables_from_string(markup)
|
||||||
markup.split(',').collect do |var|
|
markup.split(',').collect do |var|
|
||||||
var =~ /\s*(#{QuotedFragment})\s*/o
|
var =~ /\s*(#{QuotedFragment})\s*/o
|
||||||
next unless Regexp.last_match(1)
|
Regexp.last_match(1) ? Expression.parse(Regexp.last_match(1)) : nil
|
||||||
|
|
||||||
# Expression Parser returns cached objects, and we need to dup them to
|
|
||||||
# start the cycle over for each new cycle call.
|
|
||||||
# Liquid-C does not have a cache, so we don't need to dup the object.
|
|
||||||
var = parse_expression(Regexp.last_match(1))
|
|
||||||
var.is_a?(VariableLookup) ? var.dup : var
|
|
||||||
end.compact
|
end.compact
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -84,4 +71,6 @@ module Liquid
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Template.register_tag('cycle', Cycle)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,38 +1,38 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
# @liquid_public_docs
|
# decrement is used in a place where one needs to insert a counter
|
||||||
# @liquid_type tag
|
# into a template, and needs the counter to survive across
|
||||||
# @liquid_category variable
|
# multiple instantiations of the template.
|
||||||
# @liquid_name decrement
|
# NOTE: decrement is a pre-decrement, --i,
|
||||||
# @liquid_summary
|
# while increment is post: i++.
|
||||||
# Creates a new variable, with a default value of -1, that's decreased by 1 with each subsequent call.
|
|
||||||
# @liquid_description
|
|
||||||
# Variables that are declared with `decrement` are unique to the [layout](/themes/architecture/layouts), [template](/themes/architecture/templates),
|
|
||||||
# or [section](/themes/architecture/sections) file that they're created in. However, the variable is shared across
|
|
||||||
# [snippets](/themes/architecture#snippets) included in the file.
|
|
||||||
#
|
#
|
||||||
# Similarly, variables that are created with `decrement` are independent from those created with [`assign`](/docs/api/liquid/tags/assign)
|
# (To achieve the survival, the application must keep the context)
|
||||||
# and [`capture`](/docs/api/liquid/tags/capture). However, `decrement` and [`increment`](/docs/api/liquid/tags/increment) share
|
#
|
||||||
# variables.
|
# if the variable does not exist, it is created with value 0.
|
||||||
# @liquid_syntax
|
|
||||||
# {% decrement variable_name %}
|
|
||||||
# @liquid_syntax_keyword variable_name The name of the variable being decremented.
|
|
||||||
class Decrement < Tag
|
|
||||||
attr_reader :variable_name
|
|
||||||
|
|
||||||
|
# Hello: {% decrement variable %}
|
||||||
|
#
|
||||||
|
# gives you:
|
||||||
|
#
|
||||||
|
# Hello: -1
|
||||||
|
# Hello: -2
|
||||||
|
# Hello: -3
|
||||||
|
#
|
||||||
|
class Decrement < Tag
|
||||||
def initialize(tag_name, markup, options)
|
def initialize(tag_name, markup, options)
|
||||||
super
|
super
|
||||||
@variable_name = markup.strip
|
@variable = markup.strip
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_to_output_buffer(context, output)
|
def render_to_output_buffer(context, output)
|
||||||
counter_environment = context.environments.first
|
value = context.environments.first[@variable] ||= 0
|
||||||
value = counter_environment[@variable_name] || 0
|
|
||||||
value -= 1
|
value -= 1
|
||||||
counter_environment[@variable_name] = value
|
context.environments.first[@variable] = value
|
||||||
output << value.to_s
|
output << value.to_s
|
||||||
output
|
output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Template.register_tag('decrement', Decrement)
|
||||||
end
|
end
|
||||||
|
|||||||
+11
-24
@@ -1,26 +1,17 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
# @liquid_public_docs
|
# Echo outputs an expression
|
||||||
# @liquid_type tag
|
#
|
||||||
# @liquid_category syntax
|
# {% echo monkey %}
|
||||||
# @liquid_name echo
|
# {% echo user.name %}
|
||||||
# @liquid_summary
|
#
|
||||||
# Outputs an expression.
|
# This is identical to variable output syntax, like {{ foo }}, but works
|
||||||
# @liquid_description
|
# inside {% liquid %} tags. The full syntax is supported, including filters:
|
||||||
# Using the `echo` tag is the same as wrapping an expression in curly brackets (`{{` and `}}`). However, unlike the curly
|
#
|
||||||
# bracket method, you can use the `echo` tag inside [`liquid` tags](/docs/api/liquid/tags/liquid).
|
# {% echo user | link %}
|
||||||
#
|
#
|
||||||
# > Tip:
|
|
||||||
# > You can use [filters](/docs/api/liquid/filters) on expressions inside `echo` tags.
|
|
||||||
# @liquid_syntax
|
|
||||||
# {% liquid
|
|
||||||
# echo expression
|
|
||||||
# %}
|
|
||||||
# @liquid_syntax_keyword expression The expression to be output.
|
|
||||||
class Echo < Tag
|
class Echo < Tag
|
||||||
attr_reader :variable
|
|
||||||
|
|
||||||
def initialize(tag_name, markup, parse_context)
|
def initialize(tag_name, markup, parse_context)
|
||||||
super
|
super
|
||||||
@variable = Variable.new(markup, parse_context)
|
@variable = Variable.new(markup, parse_context)
|
||||||
@@ -29,11 +20,7 @@ module Liquid
|
|||||||
def render(context)
|
def render(context)
|
||||||
@variable.render_to_output_buffer(context, +'')
|
@variable.render_to_output_buffer(context, +'')
|
||||||
end
|
end
|
||||||
|
|
||||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
|
||||||
def children
|
|
||||||
[@node.variable]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Template.register_tag('echo', Echo)
|
||||||
end
|
end
|
||||||
|
|||||||
+53
-33
@@ -1,29 +1,50 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
# @liquid_public_docs
|
# "For" iterates over an array or collection.
|
||||||
# @liquid_type tag
|
# Several useful variables are available to you within the loop.
|
||||||
# @liquid_category iteration
|
#
|
||||||
# @liquid_name for
|
# == Basic usage:
|
||||||
# @liquid_summary
|
# {% for item in collection %}
|
||||||
# Renders an expression for every item in an array.
|
# {{ forloop.index }}: {{ item.name }}
|
||||||
# @liquid_description
|
# {% endfor %}
|
||||||
# You can do a maximum of 50 iterations with a `for` loop. If you need to iterate over more than 50 items, then use the
|
#
|
||||||
# [`paginate` tag](/docs/api/liquid/tags/paginate) to split the items over multiple pages.
|
# == Advanced usage:
|
||||||
|
# {% for item in collection %}
|
||||||
|
# <div {% if forloop.first %}class="first"{% endif %}>
|
||||||
|
# Item {{ forloop.index }}: {{ item.name }}
|
||||||
|
# </div>
|
||||||
|
# {% else %}
|
||||||
|
# There is nothing in the collection.
|
||||||
|
# {% endfor %}
|
||||||
|
#
|
||||||
|
# You can also define a limit and offset much like SQL. Remember
|
||||||
|
# that offset starts at 0 for the first item.
|
||||||
|
#
|
||||||
|
# {% for item in collection limit:5 offset:10 %}
|
||||||
|
# {{ item.name }}
|
||||||
|
# {% end %}
|
||||||
|
#
|
||||||
|
# To reverse the for loop simply use {% for item in collection reversed %} (note that the flag's spelling is different to the filter `reverse`)
|
||||||
|
#
|
||||||
|
# == Available variables:
|
||||||
|
#
|
||||||
|
# forloop.name:: 'item-collection'
|
||||||
|
# forloop.length:: Length of the loop
|
||||||
|
# forloop.index:: The current item's position in the collection;
|
||||||
|
# forloop.index starts at 1.
|
||||||
|
# This is helpful for non-programmers who start believe
|
||||||
|
# the first item in an array is 1, not 0.
|
||||||
|
# forloop.index0:: The current item's position in the collection
|
||||||
|
# where the first item is 0
|
||||||
|
# forloop.rindex:: Number of items remaining in the loop
|
||||||
|
# (length - index) where 1 is the last item.
|
||||||
|
# forloop.rindex0:: Number of items remaining in the loop
|
||||||
|
# where 0 is the last item.
|
||||||
|
# forloop.first:: Returns true if the item is the first item.
|
||||||
|
# forloop.last:: Returns true if the item is the last item.
|
||||||
|
# forloop.parentloop:: Provides access to the parent loop, if present.
|
||||||
#
|
#
|
||||||
# > Tip:
|
|
||||||
# > Every `for` loop has an associated [`forloop` object](/docs/api/liquid/objects/forloop) with information about the loop.
|
|
||||||
# @liquid_syntax
|
|
||||||
# {% for variable in array %}
|
|
||||||
# expression
|
|
||||||
# {% endfor %}
|
|
||||||
# @liquid_syntax_keyword variable The current item in the array.
|
|
||||||
# @liquid_syntax_keyword array The array to iterate over.
|
|
||||||
# @liquid_syntax_keyword expression The expression to render for each iteration.
|
|
||||||
# @liquid_optional_param limit [number] The number of iterations to perform.
|
|
||||||
# @liquid_optional_param offset [number] The 1-based index to start iterating at.
|
|
||||||
# @liquid_optional_param range [untyped] A custom numeric range to iterate over.
|
|
||||||
# @liquid_optional_param reversed [untyped] Iterate in reverse order.
|
|
||||||
class For < Block
|
class For < Block
|
||||||
Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
|
Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
|
||||||
|
|
||||||
@@ -42,11 +63,9 @@ module Liquid
|
|||||||
parse_body(@else_block, tokens)
|
parse_body(@else_block, tokens)
|
||||||
end
|
end
|
||||||
if blank?
|
if blank?
|
||||||
@else_block&.remove_blank_strings
|
|
||||||
@for_block.remove_blank_strings
|
@for_block.remove_blank_strings
|
||||||
|
@else_block&.remove_blank_strings
|
||||||
end
|
end
|
||||||
@else_block&.freeze
|
|
||||||
@for_block.freeze
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def nodelist
|
def nodelist
|
||||||
@@ -78,7 +97,7 @@ module Liquid
|
|||||||
collection_name = Regexp.last_match(2)
|
collection_name = Regexp.last_match(2)
|
||||||
@reversed = !!Regexp.last_match(3)
|
@reversed = !!Regexp.last_match(3)
|
||||||
@name = "#{@variable_name}-#{collection_name}"
|
@name = "#{@variable_name}-#{collection_name}"
|
||||||
@collection_name = parse_expression(collection_name)
|
@collection_name = Expression.parse(collection_name)
|
||||||
markup.scan(TagAttributes) do |key, value|
|
markup.scan(TagAttributes) do |key, value|
|
||||||
set_attribute(key, value)
|
set_attribute(key, value)
|
||||||
end
|
end
|
||||||
@@ -88,22 +107,21 @@ module Liquid
|
|||||||
end
|
end
|
||||||
|
|
||||||
def strict_parse(markup)
|
def strict_parse(markup)
|
||||||
p = @parse_context.new_parser(markup)
|
p = Parser.new(markup)
|
||||||
@variable_name = p.consume(:id)
|
@variable_name = p.consume(:id)
|
||||||
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in") unless p.id?('in')
|
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in") unless p.id?('in')
|
||||||
|
|
||||||
collection_name = p.expression
|
collection_name = p.expression
|
||||||
@collection_name = parse_expression(collection_name)
|
@collection_name = Expression.parse(collection_name)
|
||||||
|
|
||||||
@name = "#{@variable_name}-#{collection_name}"
|
@name = "#{@variable_name}-#{collection_name}"
|
||||||
@reversed = p.id?('reversed')
|
@reversed = p.id?('reversed')
|
||||||
|
|
||||||
while p.look(:comma) || p.look(:id)
|
while p.look(:id) && p.look(:colon, 1)
|
||||||
p.consume?(:comma)
|
|
||||||
unless (attribute = p.id?('limit') || p.id?('offset'))
|
unless (attribute = p.id?('limit') || p.id?('offset'))
|
||||||
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_attribute")
|
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_attribute")
|
||||||
end
|
end
|
||||||
p.consume(:colon)
|
p.consume
|
||||||
set_attribute(attribute, p.expression)
|
set_attribute(attribute, p.expression)
|
||||||
end
|
end
|
||||||
p.consume(:end_of_string)
|
p.consume(:end_of_string)
|
||||||
@@ -180,10 +198,10 @@ module Liquid
|
|||||||
@from = if expr == 'continue'
|
@from = if expr == 'continue'
|
||||||
:continue
|
:continue
|
||||||
else
|
else
|
||||||
parse_expression(expr)
|
Expression.parse(expr)
|
||||||
end
|
end
|
||||||
when 'limit'
|
when 'limit'
|
||||||
@limit = parse_expression(expr)
|
@limit = Expression.parse(expr)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -201,4 +219,6 @@ module Liquid
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Template.register_tag('for', For)
|
||||||
end
|
end
|
||||||
|
|||||||
+21
-33
@@ -1,18 +1,16 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
# @liquid_public_docs
|
# If is the conditional block
|
||||||
# @liquid_type tag
|
#
|
||||||
# @liquid_category conditional
|
# {% if user.admin %}
|
||||||
# @liquid_name if
|
# Admin user!
|
||||||
# @liquid_summary
|
# {% else %}
|
||||||
# Renders an expression if a specific condition is `true`.
|
# Not admin user
|
||||||
# @liquid_syntax
|
|
||||||
# {% if condition %}
|
|
||||||
# expression
|
|
||||||
# {% endif %}
|
# {% endif %}
|
||||||
# @liquid_syntax_keyword condition The condition to evaluate.
|
#
|
||||||
# @liquid_syntax_keyword expression The expression to render if the condition is met.
|
# There are {% if count < 5 %} less {% else %} more {% endif %} items than you need.
|
||||||
|
#
|
||||||
class If < Block
|
class If < Block
|
||||||
Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o
|
Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o
|
||||||
ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o
|
ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o
|
||||||
@@ -33,17 +31,13 @@ module Liquid
|
|||||||
def parse(tokens)
|
def parse(tokens)
|
||||||
while parse_body(@blocks.last.attachment, tokens)
|
while parse_body(@blocks.last.attachment, tokens)
|
||||||
end
|
end
|
||||||
@blocks.reverse_each do |block|
|
if blank?
|
||||||
block.attachment.remove_blank_strings if blank?
|
@blocks.each { |condition| condition.attachment.remove_blank_strings }
|
||||||
block.attachment.freeze
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
ELSE_TAG_NAMES = ['elsif', 'else'].freeze
|
|
||||||
private_constant :ELSE_TAG_NAMES
|
|
||||||
|
|
||||||
def unknown_tag(tag, markup, tokens)
|
def unknown_tag(tag, markup, tokens)
|
||||||
if ELSE_TAG_NAMES.include?(tag)
|
if ['elsif', 'else'].include?(tag)
|
||||||
push_block(tag, markup)
|
push_block(tag, markup)
|
||||||
else
|
else
|
||||||
super
|
super
|
||||||
@@ -52,11 +46,7 @@ module Liquid
|
|||||||
|
|
||||||
def render_to_output_buffer(context, output)
|
def render_to_output_buffer(context, output)
|
||||||
@blocks.each do |block|
|
@blocks.each do |block|
|
||||||
result = Liquid::Utils.to_liquid_value(
|
if block.evaluate(context)
|
||||||
block.evaluate(context),
|
|
||||||
)
|
|
||||||
|
|
||||||
if result
|
|
||||||
return block.attachment.render_to_output_buffer(context, output)
|
return block.attachment.render_to_output_buffer(context, output)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -77,22 +67,18 @@ module Liquid
|
|||||||
block.attach(new_body)
|
block.attach(new_body)
|
||||||
end
|
end
|
||||||
|
|
||||||
def parse_expression(markup)
|
|
||||||
Condition.parse_expression(parse_context, markup)
|
|
||||||
end
|
|
||||||
|
|
||||||
def lax_parse(markup)
|
def lax_parse(markup)
|
||||||
expressions = markup.scan(ExpressionsAndOperators)
|
expressions = markup.scan(ExpressionsAndOperators)
|
||||||
raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop =~ Syntax
|
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?
|
until expressions.empty?
|
||||||
operator = expressions.pop.to_s.strip
|
operator = expressions.pop.to_s.strip
|
||||||
|
|
||||||
raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop.to_s =~ Syntax
|
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)
|
raise SyntaxError, options[:locale].t("errors.syntax.if") unless BOOLEAN_OPERATORS.include?(operator)
|
||||||
new_condition.send(operator, condition)
|
new_condition.send(operator, condition)
|
||||||
condition = new_condition
|
condition = new_condition
|
||||||
@@ -102,7 +88,7 @@ module Liquid
|
|||||||
end
|
end
|
||||||
|
|
||||||
def strict_parse(markup)
|
def strict_parse(markup)
|
||||||
p = @parse_context.new_parser(markup)
|
p = Parser.new(markup)
|
||||||
condition = parse_binary_comparisons(p)
|
condition = parse_binary_comparisons(p)
|
||||||
p.consume(:end_of_string)
|
p.consume(:end_of_string)
|
||||||
condition
|
condition
|
||||||
@@ -111,7 +97,7 @@ module Liquid
|
|||||||
def parse_binary_comparisons(p)
|
def parse_binary_comparisons(p)
|
||||||
condition = parse_comparison(p)
|
condition = parse_comparison(p)
|
||||||
first_condition = condition
|
first_condition = condition
|
||||||
while (op = p.id?('and') || p.id?('or'))
|
while (op = (p.id?('and') || p.id?('or')))
|
||||||
child_condition = parse_comparison(p)
|
child_condition = parse_comparison(p)
|
||||||
condition.send(op, child_condition)
|
condition.send(op, child_condition)
|
||||||
condition = child_condition
|
condition = child_condition
|
||||||
@@ -120,9 +106,9 @@ module Liquid
|
|||||||
end
|
end
|
||||||
|
|
||||||
def parse_comparison(p)
|
def parse_comparison(p)
|
||||||
a = parse_expression(p.expression)
|
a = Expression.parse(p.expression)
|
||||||
if (op = p.consume?(:comparison))
|
if (op = p.consume?(:comparison))
|
||||||
b = parse_expression(p.expression)
|
b = Expression.parse(p.expression)
|
||||||
Condition.new(a, op, b)
|
Condition.new(a, op, b)
|
||||||
else
|
else
|
||||||
Condition.new(a)
|
Condition.new(a)
|
||||||
@@ -135,4 +121,6 @@ module Liquid
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Template.register_tag('if', If)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -14,4 +14,6 @@ module Liquid
|
|||||||
output
|
output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Template.register_tag('ifchanged', Ifchanged)
|
||||||
end
|
end
|
||||||
|
|||||||
+22
-24
@@ -1,22 +1,20 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
# @liquid_public_docs
|
# Include allows templates to relate with other templates
|
||||||
# @liquid_type tag
|
#
|
||||||
# @liquid_category theme
|
# Simply include another template:
|
||||||
# @liquid_name include
|
#
|
||||||
# @liquid_summary
|
# {% include 'product' %}
|
||||||
# Renders a [snippet](/themes/architecture#snippets).
|
#
|
||||||
# @liquid_description
|
# Include a template with a local variable:
|
||||||
# Inside the snippet, you can access and alter variables that are [created](/docs/api/liquid/tags/variable-tags) outside of the
|
#
|
||||||
# snippet.
|
# {% include 'product' with products[0] %}
|
||||||
# @liquid_syntax
|
#
|
||||||
# {% include 'filename' %}
|
# Include a template for a collection:
|
||||||
# @liquid_syntax_keyword filename The name of the snippet to render, without the `.liquid` extension.
|
#
|
||||||
# @liquid_deprecated
|
# {% include 'product' for products %}
|
||||||
# Deprecated because the way that variables are handled reduces performance and makes code harder to both read and maintain.
|
|
||||||
#
|
#
|
||||||
# The `include` tag has been replaced by [`render`](/docs/api/liquid/tags/render).
|
|
||||||
class Include < Tag
|
class Include < Tag
|
||||||
prepend Tag::Disableable
|
prepend Tag::Disableable
|
||||||
|
|
||||||
@@ -34,12 +32,12 @@ module Liquid
|
|||||||
variable_name = Regexp.last_match(3)
|
variable_name = Regexp.last_match(3)
|
||||||
|
|
||||||
@alias_name = Regexp.last_match(5)
|
@alias_name = Regexp.last_match(5)
|
||||||
@variable_name_expr = variable_name ? parse_expression(variable_name) : nil
|
@variable_name_expr = variable_name ? Expression.parse(variable_name) : nil
|
||||||
@template_name_expr = parse_expression(template_name)
|
@template_name_expr = Expression.parse(template_name)
|
||||||
@attributes = {}
|
@attributes = {}
|
||||||
|
|
||||||
markup.scan(TagAttributes) do |key, value|
|
markup.scan(TagAttributes) do |key, value|
|
||||||
@attributes[key] = parse_expression(value)
|
@attributes[key] = Expression.parse(value)
|
||||||
end
|
end
|
||||||
|
|
||||||
else
|
else
|
||||||
@@ -52,12 +50,12 @@ module Liquid
|
|||||||
|
|
||||||
def render_to_output_buffer(context, output)
|
def render_to_output_buffer(context, output)
|
||||||
template_name = context.evaluate(@template_name_expr)
|
template_name = context.evaluate(@template_name_expr)
|
||||||
raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name.is_a?(String)
|
raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name
|
||||||
|
|
||||||
partial = PartialCache.load(
|
partial = PartialCache.load(
|
||||||
template_name,
|
template_name,
|
||||||
context: context,
|
context: context,
|
||||||
parse_context: parse_context,
|
parse_context: parse_context
|
||||||
)
|
)
|
||||||
|
|
||||||
context_variable_name = @alias_name || template_name.split('/').last
|
context_variable_name = @alias_name || template_name.split('/').last
|
||||||
@@ -70,11 +68,9 @@ module Liquid
|
|||||||
|
|
||||||
old_template_name = context.template_name
|
old_template_name = context.template_name
|
||||||
old_partial = context.partial
|
old_partial = context.partial
|
||||||
|
|
||||||
begin
|
begin
|
||||||
context.template_name = partial.name
|
context.template_name = template_name
|
||||||
context.partial = true
|
context.partial = true
|
||||||
|
|
||||||
context.stack do
|
context.stack do
|
||||||
@attributes.each do |key, value|
|
@attributes.each do |key, value|
|
||||||
context[key] = context.evaluate(value)
|
context[key] = context.evaluate(value)
|
||||||
@@ -110,4 +106,6 @@ module Liquid
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Template.register_tag('include', Include)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,38 +1,35 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
# @liquid_public_docs
|
# increment is used in a place where one needs to insert a counter
|
||||||
# @liquid_type tag
|
# into a template, and needs the counter to survive across
|
||||||
# @liquid_category variable
|
# multiple instantiations of the template.
|
||||||
# @liquid_name increment
|
# (To achieve the survival, the application must keep the context)
|
||||||
# @liquid_summary
|
#
|
||||||
# Creates a new variable, with a default value of 0, that's increased by 1 with each subsequent call.
|
# if the variable does not exist, it is created with value 0.
|
||||||
# @liquid_description
|
#
|
||||||
# Variables that are declared with `increment` are unique to the [layout](/themes/architecture/layouts), [template](/themes/architecture/templates),
|
# Hello: {% increment variable %}
|
||||||
# or [section](/themes/architecture/sections) file that they're created in. However, the variable is shared across
|
#
|
||||||
# [snippets](/themes/architecture#snippets) included in the file.
|
# gives you:
|
||||||
|
#
|
||||||
|
# Hello: 0
|
||||||
|
# Hello: 1
|
||||||
|
# Hello: 2
|
||||||
#
|
#
|
||||||
# Similarly, variables that are created with `increment` are independent from those created with [`assign`](/docs/api/liquid/tags/assign)
|
|
||||||
# and [`capture`](/docs/api/liquid/tags/capture). However, `increment` and [`decrement`](/docs/api/liquid/tags/decrement) share
|
|
||||||
# variables.
|
|
||||||
# @liquid_syntax
|
|
||||||
# {% increment variable_name %}
|
|
||||||
# @liquid_syntax_keyword variable_name The name of the variable being incremented.
|
|
||||||
class Increment < Tag
|
class Increment < Tag
|
||||||
attr_reader :variable_name
|
|
||||||
|
|
||||||
def initialize(tag_name, markup, options)
|
def initialize(tag_name, markup, options)
|
||||||
super
|
super
|
||||||
@variable_name = markup.strip
|
@variable = markup.strip
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_to_output_buffer(context, output)
|
def render_to_output_buffer(context, output)
|
||||||
counter_environment = context.environments.first
|
value = context.environments.first[@variable] ||= 0
|
||||||
value = counter_environment[@variable_name] || 0
|
context.environments.first[@variable] = value + 1
|
||||||
counter_environment[@variable_name] = value + 1
|
|
||||||
|
|
||||||
output << value.to_s
|
output << value.to_s
|
||||||
output
|
output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Template.register_tag('increment', Increment)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,28 +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
|
|
||||||
end
|
|
||||||
+6
-15
@@ -1,19 +1,9 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
# @liquid_public_docs
|
|
||||||
# @liquid_type tag
|
|
||||||
# @liquid_category syntax
|
|
||||||
# @liquid_name raw
|
|
||||||
# @liquid_summary
|
|
||||||
# Outputs any Liquid code as text instead of rendering it.
|
|
||||||
# @liquid_syntax
|
|
||||||
# {% raw %}
|
|
||||||
# expression
|
|
||||||
# {% endraw %}
|
|
||||||
# @liquid_syntax_keyword expression The expression to be output without being rendered.
|
|
||||||
class Raw < Block
|
class Raw < Block
|
||||||
Syntax = /\A\s*\z/
|
Syntax = /\A\s*\z/
|
||||||
|
FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
|
||||||
|
|
||||||
def initialize(tag_name, markup, parse_context)
|
def initialize(tag_name, markup, parse_context)
|
||||||
super
|
super
|
||||||
@@ -24,15 +14,14 @@ module Liquid
|
|||||||
def parse(tokens)
|
def parse(tokens)
|
||||||
@body = +''
|
@body = +''
|
||||||
while (token = tokens.shift)
|
while (token = tokens.shift)
|
||||||
if token =~ BlockBody::FullTokenPossiblyInvalid && block_delimiter == Regexp.last_match(2)
|
if token =~ FullTokenPossiblyInvalid
|
||||||
parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
|
|
||||||
@body << Regexp.last_match(1) if Regexp.last_match(1) != ""
|
@body << Regexp.last_match(1) if Regexp.last_match(1) != ""
|
||||||
return
|
return if block_delimiter == Regexp.last_match(2)
|
||||||
end
|
end
|
||||||
@body << token unless token.empty?
|
@body << token unless token.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
raise_tag_never_closed(block_name)
|
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_never_closed", block_name: block_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_to_output_buffer(_context, output)
|
def render_to_output_buffer(_context, output)
|
||||||
@@ -56,4 +45,6 @@ module Liquid
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Template.register_tag('raw', Raw)
|
||||||
end
|
end
|
||||||
|
|||||||
+13
-40
@@ -1,37 +1,13 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
# @liquid_public_docs
|
|
||||||
# @liquid_type tag
|
|
||||||
# @liquid_category theme
|
|
||||||
# @liquid_name render
|
|
||||||
# @liquid_summary
|
|
||||||
# Renders a [snippet](/themes/architecture#snippets) or [app block](/themes/architecture/sections/section-schema#render-app-blocks).
|
|
||||||
# @liquid_description
|
|
||||||
# Inside snippets and app blocks, you can't directly access variables that are [created](/docs/api/liquid/tags/variable-tags) outside
|
|
||||||
# of the snippet or app block. However, you can [specify variables as parameters](/docs/api/liquid/tags/render#render-passing-variables-to-a-snippet)
|
|
||||||
# to pass outside variables to snippets.
|
|
||||||
#
|
|
||||||
# While you can't directly access created variables, you can access global objects, as well as any objects that are
|
|
||||||
# directly accessible outside the snippet or app block. For example, a snippet or app block inside the [product template](/themes/architecture/templates/product)
|
|
||||||
# can access the [`product` object](/docs/api/liquid/objects/product), and a snippet or app block inside a [section](/themes/architecture/sections)
|
|
||||||
# can access the [`section` object](/docs/api/liquid/objects/section).
|
|
||||||
#
|
|
||||||
# Outside a snippet or app block, you can't access variables created inside the snippet or app block.
|
|
||||||
#
|
|
||||||
# > Note:
|
|
||||||
# > When you render a snippet using the `render` tag, you can't use the [`include` tag](/docs/api/liquid/tags/include)
|
|
||||||
# > inside the snippet.
|
|
||||||
# @liquid_syntax
|
|
||||||
# {% render 'filename' %}
|
|
||||||
# @liquid_syntax_keyword filename The name of the snippet to render, without the `.liquid` extension.
|
|
||||||
class Render < Tag
|
class Render < Tag
|
||||||
FOR = 'for'
|
FOR = 'for'
|
||||||
SYNTAX = /(#{QuotedString}+)(\s+(with|#{FOR})\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
|
SYNTAX = /(#{QuotedString}+)(\s+(with|#{FOR})\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
|
||||||
|
|
||||||
disable_tags "include"
|
disable_tags "include"
|
||||||
|
|
||||||
attr_reader :template_name_expr, :variable_name_expr, :attributes, :alias_name
|
attr_reader :template_name_expr, :attributes
|
||||||
|
|
||||||
def initialize(tag_name, markup, options)
|
def initialize(tag_name, markup, options)
|
||||||
super
|
super
|
||||||
@@ -43,40 +19,36 @@ module Liquid
|
|||||||
variable_name = Regexp.last_match(4)
|
variable_name = Regexp.last_match(4)
|
||||||
|
|
||||||
@alias_name = Regexp.last_match(6)
|
@alias_name = Regexp.last_match(6)
|
||||||
@variable_name_expr = variable_name ? parse_expression(variable_name) : nil
|
@variable_name_expr = variable_name ? Expression.parse(variable_name) : nil
|
||||||
@template_name_expr = parse_expression(template_name)
|
@template_name_expr = Expression.parse(template_name)
|
||||||
@is_for_loop = (with_or_for == FOR)
|
@for = (with_or_for == FOR)
|
||||||
|
|
||||||
@attributes = {}
|
@attributes = {}
|
||||||
markup.scan(TagAttributes) do |key, value|
|
markup.scan(TagAttributes) do |key, value|
|
||||||
@attributes[key] = parse_expression(value)
|
@attributes[key] = Expression.parse(value)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def for_loop?
|
|
||||||
@is_for_loop
|
|
||||||
end
|
|
||||||
|
|
||||||
def render_to_output_buffer(context, output)
|
def render_to_output_buffer(context, output)
|
||||||
render_tag(context, output)
|
render_tag(context, output)
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_tag(context, output)
|
def render_tag(context, output)
|
||||||
# The expression should be a String literal, which parses to a String object
|
# Though we evaluate this here we will only ever parse it as a string literal.
|
||||||
template_name = @template_name_expr
|
template_name = context.evaluate(@template_name_expr)
|
||||||
raise ::ArgumentError unless template_name.is_a?(String)
|
raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name
|
||||||
|
|
||||||
partial = PartialCache.load(
|
partial = PartialCache.load(
|
||||||
template_name,
|
template_name,
|
||||||
context: context,
|
context: context,
|
||||||
parse_context: parse_context,
|
parse_context: parse_context
|
||||||
)
|
)
|
||||||
|
|
||||||
context_variable_name = @alias_name || template_name.split('/').last
|
context_variable_name = @alias_name || template_name.split('/').last
|
||||||
|
|
||||||
render_partial_func = ->(var, forloop) {
|
render_partial_func = ->(var, forloop) {
|
||||||
inner_context = context.new_isolated_subcontext
|
inner_context = context.new_isolated_subcontext
|
||||||
inner_context.template_name = partial.name
|
inner_context.template_name = template_name
|
||||||
inner_context.partial = true
|
inner_context.partial = true
|
||||||
inner_context['forloop'] = forloop if forloop
|
inner_context['forloop'] = forloop if forloop
|
||||||
|
|
||||||
@@ -89,7 +61,7 @@ module Liquid
|
|||||||
}
|
}
|
||||||
|
|
||||||
variable = @variable_name_expr ? context.evaluate(@variable_name_expr) : nil
|
variable = @variable_name_expr ? context.evaluate(@variable_name_expr) : nil
|
||||||
if @is_for_loop && variable.respond_to?(:each) && variable.respond_to?(:count)
|
if @for && variable.respond_to?(:each) && variable.respond_to?(:count)
|
||||||
forloop = Liquid::ForloopDrop.new(template_name, variable.count, nil)
|
forloop = Liquid::ForloopDrop.new(template_name, variable.count, nil)
|
||||||
variable.each { |var| render_partial_func.call(var, forloop) }
|
variable.each { |var| render_partial_func.call(var, forloop) }
|
||||||
else
|
else
|
||||||
@@ -103,9 +75,10 @@ module Liquid
|
|||||||
def children
|
def children
|
||||||
[
|
[
|
||||||
@node.template_name_expr,
|
@node.template_name_expr,
|
||||||
@node.variable_name_expr,
|
|
||||||
] + @node.attributes.values
|
] + @node.attributes.values
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Template.register_tag('render', Render)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,28 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
# @liquid_public_docs
|
|
||||||
# @liquid_type tag
|
|
||||||
# @liquid_category iteration
|
|
||||||
# @liquid_name tablerow
|
|
||||||
# @liquid_summary
|
|
||||||
# Generates HTML table rows for every item in an array.
|
|
||||||
# @liquid_description
|
|
||||||
# The `tablerow` tag must be wrapped in HTML `<table>` and `</table>` tags.
|
|
||||||
#
|
|
||||||
# > Tip:
|
|
||||||
# > Every `tablerow` loop has an associated [`tablerowloop` object](/docs/api/liquid/objects/tablerowloop) with information about the loop.
|
|
||||||
# @liquid_syntax
|
|
||||||
# {% tablerow variable in array %}
|
|
||||||
# expression
|
|
||||||
# {% endtablerow %}
|
|
||||||
# @liquid_syntax_keyword variable The current item in the array.
|
|
||||||
# @liquid_syntax_keyword array The array to iterate over.
|
|
||||||
# @liquid_syntax_keyword expression The expression to render.
|
|
||||||
# @liquid_optional_param cols [number] The number of columns that the table should have.
|
|
||||||
# @liquid_optional_param limit [number] The number of iterations to perform.
|
|
||||||
# @liquid_optional_param offset [number] The 1-based index to start iterating at.
|
|
||||||
# @liquid_optional_param range [untyped] A custom numeric range to iterate over.
|
|
||||||
class TableRow < Block
|
class TableRow < Block
|
||||||
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
|
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
|
||||||
|
|
||||||
@@ -32,10 +10,10 @@ module Liquid
|
|||||||
super
|
super
|
||||||
if markup =~ Syntax
|
if markup =~ Syntax
|
||||||
@variable_name = Regexp.last_match(1)
|
@variable_name = Regexp.last_match(1)
|
||||||
@collection_name = parse_expression(Regexp.last_match(2))
|
@collection_name = Expression.parse(Regexp.last_match(2))
|
||||||
@attributes = {}
|
@attributes = {}
|
||||||
markup.scan(TagAttributes) do |key, value|
|
markup.scan(TagAttributes) do |key, value|
|
||||||
@attributes[key] = parse_expression(value)
|
@attributes[key] = Expression.parse(value)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
raise SyntaxError, options[:locale].t("errors.syntax.table_row")
|
raise SyntaxError, options[:locale].t("errors.syntax.table_row")
|
||||||
@@ -45,13 +23,13 @@ module Liquid
|
|||||||
def render_to_output_buffer(context, output)
|
def render_to_output_buffer(context, output)
|
||||||
(collection = context.evaluate(@collection_name)) || (return '')
|
(collection = context.evaluate(@collection_name)) || (return '')
|
||||||
|
|
||||||
from = @attributes.key?('offset') ? to_integer(context.evaluate(@attributes['offset'])) : 0
|
from = @attributes.key?('offset') ? context.evaluate(@attributes['offset']).to_i : 0
|
||||||
to = @attributes.key?('limit') ? from + to_integer(context.evaluate(@attributes['limit'])) : nil
|
to = @attributes.key?('limit') ? from + context.evaluate(@attributes['limit']).to_i : nil
|
||||||
|
|
||||||
collection = Utils.slice_collection(collection, from, to)
|
collection = Utils.slice_collection(collection, from, to)
|
||||||
length = collection.length
|
length = collection.length
|
||||||
|
|
||||||
cols = @attributes.key?('cols') ? to_integer(context.evaluate(@attributes['cols'])) : length
|
cols = context.evaluate(@attributes['cols']).to_i
|
||||||
|
|
||||||
output << "<tr class=\"row1\">\n"
|
output << "<tr class=\"row1\">\n"
|
||||||
context.stack do
|
context.stack do
|
||||||
@@ -65,12 +43,6 @@ module Liquid
|
|||||||
super
|
super
|
||||||
output << '</td>'
|
output << '</td>'
|
||||||
|
|
||||||
# Handle any interrupts if they exist.
|
|
||||||
if context.interrupt?
|
|
||||||
interrupt = context.pop_interrupt
|
|
||||||
break if interrupt.is_a?(BreakInterrupt)
|
|
||||||
end
|
|
||||||
|
|
||||||
if tablerowloop.col_last && !tablerowloop.last
|
if tablerowloop.col_last && !tablerowloop.last
|
||||||
output << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">"
|
output << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">"
|
||||||
end
|
end
|
||||||
@@ -88,13 +60,7 @@ module Liquid
|
|||||||
super + @node.attributes.values + [@node.collection_name]
|
super + @node.attributes.values + [@node.collection_name]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def to_integer(value)
|
|
||||||
value.to_i
|
|
||||||
rescue NoMethodError
|
|
||||||
raise Liquid::ArgumentError, "invalid integer"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Template.register_tag('tablerow', TableRow)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,40 +3,21 @@
|
|||||||
require_relative 'if'
|
require_relative 'if'
|
||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
# @liquid_public_docs
|
# Unless is a conditional just like 'if' but works on the inverse logic.
|
||||||
# @liquid_type tag
|
#
|
||||||
# @liquid_category conditional
|
# {% unless x < 0 %} x is greater than zero {% endunless %}
|
||||||
# @liquid_name unless
|
#
|
||||||
# @liquid_summary
|
|
||||||
# Renders an expression unless a specific condition is `true`.
|
|
||||||
# @liquid_description
|
|
||||||
# > Tip:
|
|
||||||
# > Similar to the [`if` tag](/docs/api/liquid/tags/if), you can use `elsif` to add more conditions to an `unless` tag.
|
|
||||||
# @liquid_syntax
|
|
||||||
# {% unless condition %}
|
|
||||||
# expression
|
|
||||||
# {% endunless %}
|
|
||||||
# @liquid_syntax_keyword condition The condition to evaluate.
|
|
||||||
# @liquid_syntax_keyword expression The expression to render unless the condition is met.
|
|
||||||
class Unless < If
|
class Unless < If
|
||||||
def render_to_output_buffer(context, output)
|
def render_to_output_buffer(context, output)
|
||||||
# First condition is interpreted backwards ( if not )
|
# First condition is interpreted backwards ( if not )
|
||||||
first_block = @blocks.first
|
first_block = @blocks.first
|
||||||
result = Liquid::Utils.to_liquid_value(
|
unless first_block.evaluate(context)
|
||||||
first_block.evaluate(context),
|
|
||||||
)
|
|
||||||
|
|
||||||
unless result
|
|
||||||
return first_block.attachment.render_to_output_buffer(context, output)
|
return first_block.attachment.render_to_output_buffer(context, output)
|
||||||
end
|
end
|
||||||
|
|
||||||
# After the first condition unless works just like if
|
# After the first condition unless works just like if
|
||||||
@blocks[1..-1].each do |block|
|
@blocks[1..-1].each do |block|
|
||||||
result = Liquid::Utils.to_liquid_value(
|
if block.evaluate(context)
|
||||||
block.evaluate(context),
|
|
||||||
)
|
|
||||||
|
|
||||||
if result
|
|
||||||
return block.attachment.render_to_output_buffer(context, output)
|
return block.attachment.render_to_output_buffer(context, output)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -44,4 +25,6 @@ module Liquid
|
|||||||
output
|
output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Template.register_tag('unless', Unless)
|
||||||
end
|
end
|
||||||
|
|||||||
+88
-80
@@ -15,9 +15,45 @@ module Liquid
|
|||||||
# template.render('user_name' => 'bob')
|
# template.render('user_name' => 'bob')
|
||||||
#
|
#
|
||||||
class Template
|
class Template
|
||||||
attr_accessor :root, :name
|
attr_accessor :root
|
||||||
attr_reader :resource_limits, :warnings
|
attr_reader :resource_limits, :warnings
|
||||||
|
|
||||||
|
class TagRegistry
|
||||||
|
include Enumerable
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
@tags = {}
|
||||||
|
@cache = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
def [](tag_name)
|
||||||
|
return nil unless @tags.key?(tag_name)
|
||||||
|
return @cache[tag_name] if Liquid.cache_classes
|
||||||
|
|
||||||
|
lookup_class(@tags[tag_name]).tap { |o| @cache[tag_name] = o }
|
||||||
|
end
|
||||||
|
|
||||||
|
def []=(tag_name, klass)
|
||||||
|
@tags[tag_name] = klass.name
|
||||||
|
@cache[tag_name] = klass
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete(tag_name)
|
||||||
|
@tags.delete(tag_name)
|
||||||
|
@cache.delete(tag_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def each(&block)
|
||||||
|
@tags.each(&block)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def lookup_class(name)
|
||||||
|
Object.const_get(name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
attr_reader :profiler
|
attr_reader :profiler
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
@@ -25,85 +61,57 @@ module Liquid
|
|||||||
# :lax acts like liquid 2.5 and silently ignores malformed tags in most cases.
|
# :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.
|
# :warn is the default and will give deprecation warnings when invalid syntax is used.
|
||||||
# :strict will enforce correct syntax.
|
# :strict will enforce correct syntax.
|
||||||
def error_mode=(mode)
|
attr_accessor :error_mode
|
||||||
Deprecations.warn("Template.error_mode=", "Environment#error_mode=")
|
Template.error_mode = :lax
|
||||||
Environment.default.error_mode = mode
|
|
||||||
|
attr_accessor :default_exception_renderer
|
||||||
|
Template.default_exception_renderer = lambda do |exception|
|
||||||
|
exception
|
||||||
end
|
end
|
||||||
|
|
||||||
def error_mode
|
attr_accessor :file_system
|
||||||
Environment.default.error_mode
|
Template.file_system = BlankFileSystem.new
|
||||||
end
|
|
||||||
|
|
||||||
def default_exception_renderer=(renderer)
|
attr_accessor :tags
|
||||||
Deprecations.warn("Template.default_exception_renderer=", "Environment#exception_renderer=")
|
Template.tags = TagRegistry.new
|
||||||
Environment.default.exception_renderer = renderer
|
private :tags=
|
||||||
end
|
|
||||||
|
|
||||||
def default_exception_renderer
|
|
||||||
Environment.default.exception_renderer
|
|
||||||
end
|
|
||||||
|
|
||||||
def file_system=(file_system)
|
|
||||||
Deprecations.warn("Template.file_system=", "Environment#file_system=")
|
|
||||||
Environment.default.file_system = file_system
|
|
||||||
end
|
|
||||||
|
|
||||||
def file_system
|
|
||||||
Environment.default.file_system
|
|
||||||
end
|
|
||||||
|
|
||||||
def tags
|
|
||||||
Environment.default.tags
|
|
||||||
end
|
|
||||||
|
|
||||||
def register_tag(name, klass)
|
def register_tag(name, klass)
|
||||||
Deprecations.warn("Template.register_tag", "Environment#register_tag")
|
tags[name.to_s] = klass
|
||||||
Environment.default.register_tag(name, klass)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Pass a module with filter methods which should be available
|
# Pass a module with filter methods which should be available
|
||||||
# to all liquid views. Good for registering the standard library
|
# to all liquid views. Good for registering the standard library
|
||||||
def register_filter(mod)
|
def register_filter(mod)
|
||||||
Deprecations.warn("Template.register_filter", "Environment#register_filter")
|
StrainerFactory.add_global_filter(mod)
|
||||||
Environment.default.register_filter(mod)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private def default_resource_limits=(limits)
|
attr_accessor :default_resource_limits
|
||||||
Deprecations.warn("Template.default_resource_limits=", "Environment#default_resource_limits=")
|
Template.default_resource_limits = {}
|
||||||
Environment.default.default_resource_limits = limits
|
private :default_resource_limits=
|
||||||
end
|
|
||||||
|
|
||||||
def default_resource_limits
|
|
||||||
Environment.default.default_resource_limits
|
|
||||||
end
|
|
||||||
|
|
||||||
# creates a new <tt>Template</tt> object from liquid source code
|
# creates a new <tt>Template</tt> object from liquid source code
|
||||||
# To enable profiling, pass in <tt>profile: true</tt> as an option.
|
# To enable profiling, pass in <tt>profile: true</tt> as an option.
|
||||||
# See Liquid::Profiler for more information
|
# See Liquid::Profiler for more information
|
||||||
def parse(source, options = {})
|
def parse(source, options = {})
|
||||||
environment = options[:environment] || Environment.default
|
new.parse(source, options)
|
||||||
new(environment: environment).parse(source, options)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def initialize(environment: Environment.default)
|
def initialize
|
||||||
@environment = environment
|
|
||||||
@rethrow_errors = false
|
@rethrow_errors = false
|
||||||
@resource_limits = ResourceLimits.new(environment.default_resource_limits)
|
@resource_limits = ResourceLimits.new(Template.default_resource_limits)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Parse source code.
|
# Parse source code.
|
||||||
# Returns self for easy chaining
|
# Returns self for easy chaining
|
||||||
def parse(source, options = {})
|
def parse(source, options = {})
|
||||||
parse_context = configure_options(options)
|
@options = options
|
||||||
source = source.to_s.to_str
|
@profiling = options[:profile]
|
||||||
|
@line_numbers = options[:line_numbers] || @profiling
|
||||||
unless source.valid_encoding?
|
parse_context = options.is_a?(ParseContext) ? options : ParseContext.new(options)
|
||||||
raise TemplateEncodingError, parse_context.locale.t("errors.syntax.invalid_template_encoding")
|
@root = Document.parse(tokenize(source), parse_context)
|
||||||
end
|
@warnings = parse_context.warnings
|
||||||
|
|
||||||
tokenizer = parse_context.new_tokenizer(source, start_line_number: @line_numbers && 1)
|
|
||||||
@root = Document.parse(tokenizer, parse_context)
|
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -145,31 +153,32 @@ module Liquid
|
|||||||
c = args.shift
|
c = args.shift
|
||||||
|
|
||||||
if @rethrow_errors
|
if @rethrow_errors
|
||||||
c.exception_renderer = Liquid::RAISE_EXCEPTION_LAMBDA
|
c.exception_renderer = ->(_e) { raise }
|
||||||
end
|
end
|
||||||
|
|
||||||
c
|
c
|
||||||
when Liquid::Drop
|
when Liquid::Drop
|
||||||
drop = args.shift
|
drop = args.shift
|
||||||
drop.context = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits, {}, @environment)
|
drop.context = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits)
|
||||||
when Hash
|
when Hash
|
||||||
Context.new([args.shift, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits, {}, @environment)
|
Context.new([args.shift, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits)
|
||||||
when nil
|
when nil
|
||||||
Context.new(assigns, instance_assigns, registers, @rethrow_errors, @resource_limits, {}, @environment)
|
Context.new(assigns, instance_assigns, registers, @rethrow_errors, @resource_limits)
|
||||||
else
|
else
|
||||||
raise ArgumentError, "Expected Hash or Liquid::Context as parameter"
|
raise ArgumentError, "Expected Hash or Liquid::Context as parameter"
|
||||||
end
|
end
|
||||||
|
|
||||||
output = nil
|
output = nil
|
||||||
|
|
||||||
|
context_register = context.registers.is_a?(StaticRegisters) ? context.registers.static : context.registers
|
||||||
|
|
||||||
case args.last
|
case args.last
|
||||||
when Hash
|
when Hash
|
||||||
options = args.pop
|
options = args.pop
|
||||||
output = options[:output] if options[:output]
|
output = options[:output] if options[:output]
|
||||||
static_registers = context.registers.static
|
|
||||||
|
|
||||||
options[:registers]&.each do |key, register|
|
options[:registers]&.each do |key, register|
|
||||||
static_registers[key] = register
|
context_register[key] = register
|
||||||
end
|
end
|
||||||
|
|
||||||
apply_options_to_context(context, options)
|
apply_options_to_context(context, options)
|
||||||
@@ -180,15 +189,12 @@ module Liquid
|
|||||||
# Retrying a render resets resource usage
|
# Retrying a render resets resource usage
|
||||||
context.resource_limits.reset
|
context.resource_limits.reset
|
||||||
|
|
||||||
if @profiling && context.profiler.nil?
|
|
||||||
@profiler = context.profiler = Liquid::Profiler.new
|
|
||||||
end
|
|
||||||
|
|
||||||
context.template_name ||= name
|
|
||||||
|
|
||||||
begin
|
begin
|
||||||
# render the nodelist.
|
# 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
|
rescue Liquid::MemoryError => e
|
||||||
context.handle_error(e)
|
context.handle_error(e)
|
||||||
ensure
|
ensure
|
||||||
@@ -207,23 +213,25 @@ module Liquid
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def configure_options(options)
|
def tokenize(source)
|
||||||
if (profiling = options[:profile])
|
Tokenizer.new(source, @line_numbers)
|
||||||
|
end
|
||||||
|
|
||||||
|
def with_profiling(context)
|
||||||
|
if @profiling && !context.partial
|
||||||
raise "Profiler not loaded, require 'liquid/profiler' first" unless defined?(Liquid::Profiler)
|
raise "Profiler not loaded, require 'liquid/profiler' first" unless defined?(Liquid::Profiler)
|
||||||
end
|
|
||||||
|
|
||||||
@options = options
|
@profiler = Profiler.new(context.template_name)
|
||||||
@profiling = profiling
|
@profiler.start
|
||||||
@line_numbers = options[:line_numbers] || @profiling
|
|
||||||
parse_context = if options.is_a?(ParseContext)
|
begin
|
||||||
options
|
yield
|
||||||
|
ensure
|
||||||
|
@profiler.stop
|
||||||
|
end
|
||||||
else
|
else
|
||||||
opts = options.key?(:environment) ? options : options.merge(environment: @environment)
|
yield
|
||||||
ParseContext.new(opts)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@warnings = parse_context.warnings
|
|
||||||
parse_context
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def apply_options_to_context(context, options)
|
def apply_options_to_context(context, options)
|
||||||
|
|||||||
+11
-127
@@ -1,45 +1,18 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require "strscan"
|
|
||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
class Tokenizer
|
class Tokenizer
|
||||||
attr_reader :line_number, :for_liquid_tag
|
attr_reader :line_number, :for_liquid_tag
|
||||||
|
|
||||||
TAG_END = /%\}/
|
def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false)
|
||||||
TAG_OR_VARIABLE_START = /\{[\{\%]/
|
@source = source
|
||||||
NEWLINE = /\n/
|
@line_number = line_number || (line_numbers ? 1 : nil)
|
||||||
|
|
||||||
OPEN_CURLEY = "{".ord
|
|
||||||
CLOSE_CURLEY = "}".ord
|
|
||||||
PERCENTAGE = "%".ord
|
|
||||||
|
|
||||||
def initialize(
|
|
||||||
source:,
|
|
||||||
string_scanner:,
|
|
||||||
line_numbers: false,
|
|
||||||
line_number: nil,
|
|
||||||
for_liquid_tag: false
|
|
||||||
)
|
|
||||||
@line_number = line_number || (line_numbers ? 1 : nil)
|
|
||||||
@for_liquid_tag = for_liquid_tag
|
@for_liquid_tag = for_liquid_tag
|
||||||
@source = source.to_s.to_str
|
@tokens = tokenize
|
||||||
@offset = 0
|
|
||||||
@tokens = []
|
|
||||||
|
|
||||||
if @source
|
|
||||||
@ss = string_scanner
|
|
||||||
@ss.string = @source
|
|
||||||
tokenize
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def shift
|
def shift
|
||||||
token = @tokens[@offset]
|
(token = @tokens.shift) || return
|
||||||
|
|
||||||
return unless token
|
|
||||||
|
|
||||||
@offset += 1
|
|
||||||
|
|
||||||
if @line_number
|
if @line_number
|
||||||
@line_number += @for_liquid_tag ? 1 : token.count("\n")
|
@line_number += @for_liquid_tag ? 1 : token.count("\n")
|
||||||
@@ -51,105 +24,16 @@ module Liquid
|
|||||||
private
|
private
|
||||||
|
|
||||||
def tokenize
|
def tokenize
|
||||||
if @for_liquid_tag
|
return [] if @source.to_s.empty?
|
||||||
@tokens = @source.split("\n")
|
|
||||||
else
|
|
||||||
@tokens << shift_normal until @ss.eos?
|
|
||||||
end
|
|
||||||
|
|
||||||
@source = nil
|
return @source.split("\n") if @for_liquid_tag
|
||||||
@ss = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
def shift_normal
|
tokens = @source.split(TemplateParser)
|
||||||
token = next_token
|
|
||||||
|
|
||||||
return unless token
|
# removes the rogue empty element at the beginning of the array
|
||||||
|
tokens.shift if tokens[0]&.empty?
|
||||||
|
|
||||||
token
|
tokens
|
||||||
end
|
|
||||||
|
|
||||||
def next_token
|
|
||||||
# possible states: :text, :tag, :variable
|
|
||||||
byte_a = @ss.peek_byte
|
|
||||||
|
|
||||||
if byte_a == OPEN_CURLEY
|
|
||||||
@ss.scan_byte
|
|
||||||
|
|
||||||
byte_b = @ss.peek_byte
|
|
||||||
|
|
||||||
if byte_b == PERCENTAGE
|
|
||||||
@ss.scan_byte
|
|
||||||
return next_tag_token
|
|
||||||
elsif byte_b == OPEN_CURLEY
|
|
||||||
@ss.scan_byte
|
|
||||||
return next_variable_token
|
|
||||||
end
|
|
||||||
|
|
||||||
@ss.pos -= 1
|
|
||||||
end
|
|
||||||
|
|
||||||
next_text_token
|
|
||||||
end
|
|
||||||
|
|
||||||
def next_text_token
|
|
||||||
start = @ss.pos
|
|
||||||
|
|
||||||
unless @ss.skip_until(TAG_OR_VARIABLE_START)
|
|
||||||
token = @ss.rest
|
|
||||||
@ss.terminate
|
|
||||||
return token
|
|
||||||
end
|
|
||||||
|
|
||||||
pos = @ss.pos -= 2
|
|
||||||
@source.byteslice(start, pos - start)
|
|
||||||
end
|
|
||||||
|
|
||||||
def next_variable_token
|
|
||||||
start = @ss.pos - 2
|
|
||||||
|
|
||||||
byte_a = byte_b = @ss.scan_byte
|
|
||||||
|
|
||||||
while byte_b
|
|
||||||
byte_a = @ss.scan_byte while byte_a && (byte_a != CLOSE_CURLEY && byte_a != OPEN_CURLEY)
|
|
||||||
|
|
||||||
break unless byte_a
|
|
||||||
|
|
||||||
if @ss.eos?
|
|
||||||
return byte_a == CLOSE_CURLEY ? @source.byteslice(start, @ss.pos - start) : "{{"
|
|
||||||
end
|
|
||||||
|
|
||||||
byte_b = @ss.scan_byte
|
|
||||||
|
|
||||||
if byte_a == CLOSE_CURLEY
|
|
||||||
if byte_b == CLOSE_CURLEY
|
|
||||||
return @source.byteslice(start, @ss.pos - start)
|
|
||||||
elsif byte_b != CLOSE_CURLEY
|
|
||||||
@ss.pos -= 1
|
|
||||||
return @source.byteslice(start, @ss.pos - start)
|
|
||||||
end
|
|
||||||
elsif byte_a == OPEN_CURLEY && byte_b == PERCENTAGE
|
|
||||||
return next_tag_token_with_start(start)
|
|
||||||
end
|
|
||||||
|
|
||||||
byte_a = byte_b
|
|
||||||
end
|
|
||||||
|
|
||||||
"{{"
|
|
||||||
end
|
|
||||||
|
|
||||||
def next_tag_token
|
|
||||||
start = @ss.pos - 2
|
|
||||||
if (len = @ss.skip_until(TAG_END))
|
|
||||||
@source.byteslice(start, len + 2)
|
|
||||||
else
|
|
||||||
"{%"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def next_tag_token_with_start(start)
|
|
||||||
@ss.skip_until(TAG_END)
|
|
||||||
@source.byteslice(start, @ss.pos - start)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -81,99 +81,5 @@ module Liquid
|
|||||||
rescue ::ArgumentError
|
rescue ::ArgumentError
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.to_liquid_value(obj)
|
|
||||||
# Enable "obj" to represent itself as a primitive value like integer, string, or boolean
|
|
||||||
return obj.to_liquid_value if obj.respond_to?(:to_liquid_value)
|
|
||||||
|
|
||||||
# Otherwise return the object itself
|
|
||||||
obj
|
|
||||||
end
|
|
||||||
|
|
||||||
if RUBY_VERSION >= '3.4'
|
|
||||||
def self.to_s(obj, seen = {})
|
|
||||||
case obj
|
|
||||||
when Hash
|
|
||||||
hash_inspect(obj, seen)
|
|
||||||
when Array
|
|
||||||
array_inspect(obj, seen)
|
|
||||||
else
|
|
||||||
obj.to_s
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.inspect(obj, seen = {})
|
|
||||||
case obj
|
|
||||||
when Hash
|
|
||||||
hash_inspect(obj, seen)
|
|
||||||
when Array
|
|
||||||
array_inspect(obj, seen)
|
|
||||||
else
|
|
||||||
obj.inspect
|
|
||||||
end
|
|
||||||
end
|
|
||||||
else
|
|
||||||
def self.to_s(obj, seen = nil)
|
|
||||||
obj.to_s
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.inspect(obj, seen = nil)
|
|
||||||
obj.inspect
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.array_inspect(arr, seen = {})
|
|
||||||
if seen[arr.object_id]
|
|
||||||
return "[...]"
|
|
||||||
end
|
|
||||||
|
|
||||||
seen[arr.object_id] = true
|
|
||||||
str = +"["
|
|
||||||
cursor = 0
|
|
||||||
len = arr.length
|
|
||||||
|
|
||||||
while cursor < len
|
|
||||||
if cursor > 0
|
|
||||||
str << ", "
|
|
||||||
end
|
|
||||||
|
|
||||||
item_str = inspect(arr[cursor], seen)
|
|
||||||
str << item_str
|
|
||||||
cursor += 1
|
|
||||||
end
|
|
||||||
|
|
||||||
str << "]"
|
|
||||||
str
|
|
||||||
ensure
|
|
||||||
seen.delete(arr.object_id)
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.hash_inspect(hash, seen = {})
|
|
||||||
if seen[hash.object_id]
|
|
||||||
return "{...}"
|
|
||||||
end
|
|
||||||
seen[hash.object_id] = true
|
|
||||||
|
|
||||||
str = +"{"
|
|
||||||
first = true
|
|
||||||
hash.each do |key, value|
|
|
||||||
if first
|
|
||||||
first = false
|
|
||||||
else
|
|
||||||
str << ", "
|
|
||||||
end
|
|
||||||
|
|
||||||
key_str = inspect(key, seen)
|
|
||||||
str << key_str
|
|
||||||
str << "=>"
|
|
||||||
|
|
||||||
value_str = inspect(value, seen)
|
|
||||||
str << value_str
|
|
||||||
end
|
|
||||||
str << "}"
|
|
||||||
str
|
|
||||||
ensure
|
|
||||||
seen.delete(hash.object_id)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+15
-25
@@ -30,7 +30,7 @@ module Liquid
|
|||||||
@parse_context = parse_context
|
@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
|
end
|
||||||
|
|
||||||
def raw
|
def raw
|
||||||
@@ -47,7 +47,7 @@ module Liquid
|
|||||||
|
|
||||||
name_markup = Regexp.last_match(1)
|
name_markup = Regexp.last_match(1)
|
||||||
filter_markup = Regexp.last_match(2)
|
filter_markup = Regexp.last_match(2)
|
||||||
@name = parse_context.parse_expression(name_markup)
|
@name = Expression.parse(name_markup)
|
||||||
if filter_markup =~ FilterMarkupRegex
|
if filter_markup =~ FilterMarkupRegex
|
||||||
filters = Regexp.last_match(1).scan(FilterParser)
|
filters = Regexp.last_match(1).scan(FilterParser)
|
||||||
filters.each do |f|
|
filters.each do |f|
|
||||||
@@ -61,14 +61,12 @@ module Liquid
|
|||||||
|
|
||||||
def strict_parse(markup)
|
def strict_parse(markup)
|
||||||
@filters = []
|
@filters = []
|
||||||
p = @parse_context.new_parser(markup)
|
p = Parser.new(markup)
|
||||||
|
|
||||||
return if p.look(:end_of_string)
|
@name = Expression.parse(p.expression)
|
||||||
|
|
||||||
@name = parse_context.parse_expression(p.expression)
|
|
||||||
while p.consume?(:pipe)
|
while p.consume?(:pipe)
|
||||||
filtername = p.consume(:id)
|
filtername = p.consume(:id)
|
||||||
filterargs = p.consume?(:colon) ? parse_filterargs(p) : Const::EMPTY_ARRAY
|
filterargs = p.consume?(:colon) ? parse_filterargs(p) : []
|
||||||
@filters << parse_filter_expressions(filtername, filterargs)
|
@filters << parse_filter_expressions(filtername, filterargs)
|
||||||
end
|
end
|
||||||
p.consume(:end_of_string)
|
p.consume(:end_of_string)
|
||||||
@@ -83,11 +81,9 @@ module Liquid
|
|||||||
end
|
end
|
||||||
|
|
||||||
def render(context)
|
def render(context)
|
||||||
obj = context.evaluate(@name)
|
obj = @filters.inject(context.evaluate(@name)) do |output, (filter_name, filter_args, filter_kwargs)|
|
||||||
|
|
||||||
@filters.each do |filter_name, filter_args, filter_kwargs|
|
|
||||||
filter_args = evaluate_filter_expressions(context, 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
|
end
|
||||||
|
|
||||||
context.apply_global_filter(obj)
|
context.apply_global_filter(obj)
|
||||||
@@ -95,21 +91,15 @@ module Liquid
|
|||||||
|
|
||||||
def render_to_output_buffer(context, output)
|
def render_to_output_buffer(context, output)
|
||||||
obj = render(context)
|
obj = render(context)
|
||||||
render_obj_to_output(obj, output)
|
|
||||||
output
|
|
||||||
end
|
|
||||||
|
|
||||||
def render_obj_to_output(obj, output)
|
if obj.is_a?(Array)
|
||||||
case obj
|
output << obj.join
|
||||||
when NilClass
|
elsif obj.nil?
|
||||||
# Do nothing
|
|
||||||
when Array
|
|
||||||
obj.each do |o|
|
|
||||||
render_obj_to_output(o, output)
|
|
||||||
end
|
|
||||||
else
|
else
|
||||||
output << Liquid::Utils.to_s(obj)
|
output << obj.to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
|
output
|
||||||
end
|
end
|
||||||
|
|
||||||
def disabled?(_context)
|
def disabled?(_context)
|
||||||
@@ -128,9 +118,9 @@ module Liquid
|
|||||||
unparsed_args.each do |a|
|
unparsed_args.each do |a|
|
||||||
if (matches = a.match(JustTagAttributes))
|
if (matches = a.match(JustTagAttributes))
|
||||||
keyword_args ||= {}
|
keyword_args ||= {}
|
||||||
keyword_args[matches[1]] = parse_context.parse_expression(matches[2])
|
keyword_args[matches[1]] = Expression.parse(matches[2])
|
||||||
else
|
else
|
||||||
filter_args << parse_context.parse_expression(a)
|
filter_args << Expression.parse(a)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
result = [filter_name, filter_args]
|
result = [filter_name, filter_args]
|
||||||
|
|||||||
@@ -2,24 +2,21 @@
|
|||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
class VariableLookup
|
class VariableLookup
|
||||||
COMMAND_METHODS = ['size', 'first', 'last'].freeze
|
SQUARE_BRACKETED = /\A\[(.*)\]\z/m
|
||||||
|
COMMAND_METHODS = ['size', 'first', 'last'].freeze
|
||||||
|
|
||||||
attr_reader :name, :lookups
|
attr_reader :name, :lookups
|
||||||
|
|
||||||
def self.parse(markup, string_scanner = StringScanner.new(""), cache = nil)
|
def self.parse(markup)
|
||||||
new(markup, string_scanner, cache)
|
new(markup)
|
||||||
end
|
end
|
||||||
|
|
||||||
def initialize(markup, string_scanner = StringScanner.new(""), cache = nil)
|
def initialize(markup)
|
||||||
lookups = markup.scan(VariableParser)
|
lookups = markup.scan(VariableParser)
|
||||||
|
|
||||||
name = lookups.shift
|
name = lookups.shift
|
||||||
if name&.start_with?('[') && name&.end_with?(']')
|
if name =~ SQUARE_BRACKETED
|
||||||
name = Expression.parse(
|
name = Expression.parse(Regexp.last_match(1))
|
||||||
name[1..-2],
|
|
||||||
string_scanner,
|
|
||||||
cache,
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
@name = name
|
@name = name
|
||||||
|
|
||||||
@@ -28,22 +25,14 @@ module Liquid
|
|||||||
|
|
||||||
@lookups.each_index do |i|
|
@lookups.each_index do |i|
|
||||||
lookup = lookups[i]
|
lookup = lookups[i]
|
||||||
if lookup&.start_with?('[') && lookup&.end_with?(']')
|
if lookup =~ SQUARE_BRACKETED
|
||||||
lookups[i] = Expression.parse(
|
lookups[i] = Expression.parse(Regexp.last_match(1))
|
||||||
lookup[1..-2],
|
|
||||||
string_scanner,
|
|
||||||
cache,
|
|
||||||
)
|
|
||||||
elsif COMMAND_METHODS.include?(lookup)
|
elsif COMMAND_METHODS.include?(lookup)
|
||||||
@command_flags |= 1 << i
|
@command_flags |= 1 << i
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def lookup_command?(lookup_index)
|
|
||||||
@command_flags & (1 << lookup_index) != 0
|
|
||||||
end
|
|
||||||
|
|
||||||
def evaluate(context)
|
def evaluate(context)
|
||||||
name = context.evaluate(@name)
|
name = context.evaluate(@name)
|
||||||
object = context.find_variable(name)
|
object = context.find_variable(name)
|
||||||
@@ -51,9 +40,6 @@ module Liquid
|
|||||||
@lookups.each_index do |i|
|
@lookups.each_index do |i|
|
||||||
key = context.evaluate(@lookups[i])
|
key = context.evaluate(@lookups[i])
|
||||||
|
|
||||||
# Cast "key" to its liquid value to enable it to act as a primitive value
|
|
||||||
key = Liquid::Utils.to_liquid_value(key)
|
|
||||||
|
|
||||||
# If object is a hash- or array-like object we look for the
|
# If object is a hash- or array-like object we look for the
|
||||||
# presence of the key and if its available we return it
|
# presence of the key and if its available we return it
|
||||||
if object.respond_to?(:[]) &&
|
if object.respond_to?(:[]) &&
|
||||||
@@ -67,7 +53,7 @@ module Liquid
|
|||||||
# Some special cases. If the part wasn't in square brackets and
|
# Some special cases. If the part wasn't in square brackets and
|
||||||
# no key with the same name was found we interpret following calls
|
# no key with the same name was found we interpret following calls
|
||||||
# as commands and call them on the current object
|
# as commands and call them on the current object
|
||||||
elsif lookup_command?(i) && object.respond_to?(key)
|
elsif @command_flags & (1 << i) != 0 && object.respond_to?(key)
|
||||||
object = object.send(key).to_liquid
|
object = object.send(key).to_liquid
|
||||||
|
|
||||||
# No key was present with the desired value and it wasn't one of the directly supported
|
# No key was present with the desired value and it wasn't one of the directly supported
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
VERSION = "5.7.0"
|
VERSION = "4.0.3"
|
||||||
end
|
end
|
||||||
|
|||||||
+4
-8
@@ -13,24 +13,20 @@ Gem::Specification.new do |s|
|
|||||||
s.summary = "A secure, non-evaling end user template engine with aesthetic markup."
|
s.summary = "A secure, non-evaling end user template engine with aesthetic markup."
|
||||||
s.authors = ["Tobias Lütke"]
|
s.authors = ["Tobias Lütke"]
|
||||||
s.email = ["[email protected]"]
|
s.email = ["[email protected]"]
|
||||||
s.homepage = "https://shopify.github.io/liquid/"
|
s.homepage = "http://www.liquidmarkup.org"
|
||||||
s.license = "MIT"
|
s.license = "MIT"
|
||||||
# s.description = "A secure, non-evaling end user template engine with aesthetic markup."
|
# s.description = "A secure, non-evaling end user template engine with aesthetic markup."
|
||||||
|
|
||||||
s.required_ruby_version = ">= 3.0.0"
|
s.required_ruby_version = ">= 2.4.0"
|
||||||
s.required_rubygems_version = ">= 1.3.7"
|
s.required_rubygems_version = ">= 1.3.7"
|
||||||
|
|
||||||
s.metadata['allowed_push_host'] = 'https://rubygems.org'
|
s.test_files = Dir.glob("{test}/**/*")
|
||||||
|
s.files = Dir.glob("{lib}/**/*") + %w(LICENSE README.md)
|
||||||
s.files = Dir.glob("{lib}/**/*") + %w(LICENSE README.md)
|
|
||||||
|
|
||||||
s.extra_rdoc_files = ["History.md", "README.md"]
|
s.extra_rdoc_files = ["History.md", "README.md"]
|
||||||
|
|
||||||
s.require_path = "lib"
|
s.require_path = "lib"
|
||||||
|
|
||||||
s.add_dependency("strscan", ">= 3.1.1")
|
|
||||||
s.add_dependency("bigdecimal")
|
|
||||||
|
|
||||||
s.add_development_dependency('rake', '~> 13.0')
|
s.add_development_dependency('rake', '~> 13.0')
|
||||||
s.add_development_dependency('minitest')
|
s.add_development_dependency('minitest')
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,23 +3,18 @@
|
|||||||
require 'benchmark/ips'
|
require 'benchmark/ips'
|
||||||
require_relative 'theme_runner'
|
require_relative 'theme_runner'
|
||||||
|
|
||||||
RubyVM::YJIT.enable if defined?(RubyVM::YJIT)
|
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
|
||||||
Liquid::Environment.default.error_mode = ARGV.first.to_sym if ARGV.first
|
|
||||||
|
|
||||||
profiler = ThemeRunner.new
|
profiler = ThemeRunner.new
|
||||||
|
|
||||||
Benchmark.ips do |x|
|
Benchmark.ips do |x|
|
||||||
x.time = 20
|
x.time = 10
|
||||||
x.warmup = 10
|
x.warmup = 5
|
||||||
|
|
||||||
puts
|
puts
|
||||||
puts "Running benchmark for #{x.time} seconds (with #{x.warmup} seconds warmup)."
|
puts "Running benchmark for #{x.time} seconds (with #{x.warmup} seconds warmup)."
|
||||||
puts
|
puts
|
||||||
|
|
||||||
phase = ENV["PHASE"] || "all"
|
x.report("parse:") { profiler.compile }
|
||||||
|
x.report("render:") { profiler.render }
|
||||||
x.report("tokenize:") { profiler.tokenize } if phase == "all" || phase == "tokenize"
|
x.report("parse & render:") { profiler.run }
|
||||||
x.report("parse:") { profiler.compile } if phase == "all" || phase == "parse"
|
|
||||||
x.report("render:") { profiler.render } if phase == "all" || phase == "render"
|
|
||||||
x.report("parse & render:") { profiler.run } if phase == "all" || phase == "run"
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,19 +3,11 @@
|
|||||||
require 'yaml'
|
require 'yaml'
|
||||||
|
|
||||||
module Database
|
module Database
|
||||||
DATABASE_FILE_PATH = "#{__dir__}/vision.database.yml"
|
|
||||||
|
|
||||||
# Load the standard vision toolkit database and re-arrage it to be simply exportable
|
# Load the standard vision toolkit database and re-arrage it to be simply exportable
|
||||||
# to liquid as assigns. All this is based on Shopify
|
# to liquid as assigns. All this is based on Shopify
|
||||||
def self.tables
|
def self.tables
|
||||||
@tables ||= begin
|
@tables ||= begin
|
||||||
db =
|
db = YAML.load_file("#{__dir__}/vision.database.yml")
|
||||||
if YAML.respond_to?(:unsafe_load_file) # Only Psych 4+ can use unsafe_load_file
|
|
||||||
# unsafe_load_file is needed for YAML references
|
|
||||||
YAML.unsafe_load_file(DATABASE_FILE_PATH)
|
|
||||||
else
|
|
||||||
YAML.load_file(DATABASE_FILE_PATH)
|
|
||||||
end
|
|
||||||
|
|
||||||
# From vision source
|
# From vision source
|
||||||
db['products'].each do |product|
|
db['products'].each do |product|
|
||||||
@@ -49,3 +41,8 @@ module Database
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if __FILE__ == $PROGRAM_NAME
|
||||||
|
p(Database.tables['collections']['frontpage'].keys)
|
||||||
|
# p Database.tables['blog']['articles']
|
||||||
|
end
|
||||||
|
|||||||
@@ -11,12 +11,11 @@ require_relative 'shop_filter'
|
|||||||
require_relative 'tag_filter'
|
require_relative 'tag_filter'
|
||||||
require_relative 'weight_filter'
|
require_relative 'weight_filter'
|
||||||
|
|
||||||
default_environment = Liquid::Environment.default
|
Liquid::Template.register_tag('paginate', Paginate)
|
||||||
default_environment.register_tag('paginate', Paginate)
|
Liquid::Template.register_tag('form', CommentForm)
|
||||||
default_environment.register_tag('form', CommentForm)
|
|
||||||
|
|
||||||
default_environment.register_filter(JsonFilter)
|
Liquid::Template.register_filter(JsonFilter)
|
||||||
default_environment.register_filter(MoneyFilter)
|
Liquid::Template.register_filter(MoneyFilter)
|
||||||
default_environment.register_filter(WeightFilter)
|
Liquid::Template.register_filter(WeightFilter)
|
||||||
default_environment.register_filter(ShopFilter)
|
Liquid::Template.register_filter(ShopFilter)
|
||||||
default_environment.register_filter(TagFilter)
|
Liquid::Template.register_filter(TagFilter)
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ class Paginate < Liquid::Block
|
|||||||
|
|
||||||
pagination['items'] = collection_size
|
pagination['items'] = collection_size
|
||||||
pagination['pages'] = page_count - 1
|
pagination['pages'] = page_count - 1
|
||||||
pagination['previous'] = link('« Previous', current_page - 1) if 1 < current_page
|
pagination['previous'] = link('« Previous', current_page - 1) unless 1 >= current_page
|
||||||
pagination['next'] = link('Next »', current_page + 1) if page_count > current_page + 1
|
pagination['next'] = link('Next »', current_page + 1) unless page_count <= current_page + 1
|
||||||
pagination['parts'] = []
|
pagination['parts'] = []
|
||||||
|
|
||||||
hellip_break = false
|
hellip_break = false
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# This profiler run simulates Shopify.
|
# This profiler run simulates Shopify.
|
||||||
# We are looking in the tests directory for liquid files and render them within the designated layout file.
|
# We are looking in the tests directory for liquid files and render them within the designated layout file.
|
||||||
# We will also export a substantial database to liquid which the templates can render values of.
|
# We will also export a substantial database to liquid which the templates can render values of.
|
||||||
# All this is to make the benchmark as non synthetic as possible. All templates and tests are lifted from
|
# All this is to make the benchmark as non syntetic as possible. All templates and tests are lifted from
|
||||||
# direct real-world usage and the profiler measures code that looks very similar to the way it looks in
|
# direct real-world usage and the profiler measures code that looks very similar to the way it looks in
|
||||||
# Shopify which is likely the biggest user of liquid in the world which something to the tune of several
|
# Shopify which is likely the biggest user of liquid in the world which something to the tune of several
|
||||||
# million Template#render calls a day.
|
# million Template#render calls a day.
|
||||||
@@ -48,19 +48,6 @@ class ThemeRunner
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# `tokenize` will just test the tokenizen portion of liquid without any templates
|
|
||||||
def tokenize
|
|
||||||
ss = StringScanner.new("")
|
|
||||||
@tests.each do |test_hash|
|
|
||||||
tokenizer = Liquid::Tokenizer.new(
|
|
||||||
source: test_hash[:liquid],
|
|
||||||
string_scanner: ss,
|
|
||||||
line_numbers: true,
|
|
||||||
)
|
|
||||||
while tokenizer.shift; end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# `run` is called to benchmark rendering and compiling at the same time
|
# `run` is called to benchmark rendering and compiling at the same time
|
||||||
def run
|
def run
|
||||||
each_test do |liquid, layout, assigns, page_template, template_name|
|
each_test do |liquid, layout, assigns, page_template, template_name|
|
||||||
@@ -86,14 +73,10 @@ class ThemeRunner
|
|||||||
|
|
||||||
private
|
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)
|
def compile_and_render(template, layout, assigns, page_template, template_file)
|
||||||
compiled_test = compile_test(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
|
end
|
||||||
|
|
||||||
def compile_all_tests
|
def compile_all_tests
|
||||||
|
|||||||
@@ -1,94 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require "benchmark/ips"
|
|
||||||
|
|
||||||
# benchmark liquid lexing
|
|
||||||
|
|
||||||
require 'liquid'
|
|
||||||
|
|
||||||
RubyVM::YJIT.enable
|
|
||||||
|
|
||||||
STRING_MARKUPS = [
|
|
||||||
"\"foo\"",
|
|
||||||
"\"fooooooooooo\"",
|
|
||||||
"\"foooooooooooooooooooooooooooooo\"",
|
|
||||||
"'foo'",
|
|
||||||
"'fooooooooooo'",
|
|
||||||
"'foooooooooooooooooooooooooooooo'",
|
|
||||||
]
|
|
||||||
|
|
||||||
VARIABLE_MARKUPS = [
|
|
||||||
"article",
|
|
||||||
"article.title",
|
|
||||||
"article.title.size",
|
|
||||||
"very_long_variable_name_2024_11_05",
|
|
||||||
"very_long_variable_name_2024_11_05.size",
|
|
||||||
]
|
|
||||||
|
|
||||||
NUMBER_MARKUPS = [
|
|
||||||
"0",
|
|
||||||
"35",
|
|
||||||
"1241891024912849",
|
|
||||||
"3.5",
|
|
||||||
"3.51214128409128",
|
|
||||||
"12381902839.123819283910283",
|
|
||||||
"123.456.789",
|
|
||||||
"-123",
|
|
||||||
"-12.33",
|
|
||||||
"-405.231",
|
|
||||||
"-0",
|
|
||||||
"0",
|
|
||||||
"0.0",
|
|
||||||
"0.0000000000000000000000",
|
|
||||||
"0.00000000001",
|
|
||||||
]
|
|
||||||
|
|
||||||
RANGE_MARKUPS = [
|
|
||||||
"(1..30)",
|
|
||||||
"(1...30)",
|
|
||||||
"(1..30..5)",
|
|
||||||
"(1.0...30.0)",
|
|
||||||
"(1.........30)",
|
|
||||||
"(1..foo)",
|
|
||||||
"(foo..30)",
|
|
||||||
"(foo..bar)",
|
|
||||||
"(foo...bar...100)",
|
|
||||||
"(foo...bar...100.0)",
|
|
||||||
]
|
|
||||||
|
|
||||||
LITERAL_MARKUPS = [
|
|
||||||
nil,
|
|
||||||
'nil',
|
|
||||||
'null',
|
|
||||||
'',
|
|
||||||
'true',
|
|
||||||
'false',
|
|
||||||
'blank',
|
|
||||||
'empty',
|
|
||||||
]
|
|
||||||
|
|
||||||
MARKUPS = {
|
|
||||||
"string" => STRING_MARKUPS,
|
|
||||||
"literal" => LITERAL_MARKUPS,
|
|
||||||
"variable" => VARIABLE_MARKUPS,
|
|
||||||
"number" => NUMBER_MARKUPS,
|
|
||||||
"range" => RANGE_MARKUPS,
|
|
||||||
}
|
|
||||||
|
|
||||||
Benchmark.ips do |x|
|
|
||||||
x.config(time: 5, warmup: 5)
|
|
||||||
|
|
||||||
MARKUPS.each do |type, markups|
|
|
||||||
x.report("Liquid::Expression#parse: #{type}") do
|
|
||||||
markups.each do |markup|
|
|
||||||
Liquid::Expression.parse(markup)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
x.report("Liquid::Expression#parse: all") do
|
|
||||||
MARKUPS.values.flatten.each do |markup|
|
|
||||||
Liquid::Expression.parse(markup)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require "benchmark/ips"
|
|
||||||
|
|
||||||
# benchmark liquid lexing
|
|
||||||
|
|
||||||
require 'liquid'
|
|
||||||
|
|
||||||
RubyVM::YJIT.enable
|
|
||||||
|
|
||||||
EXPRESSIONS = [
|
|
||||||
"foo[1..2].baz",
|
|
||||||
"12.0",
|
|
||||||
"foo.bar.based",
|
|
||||||
"21 - 62",
|
|
||||||
"foo.bar.baz",
|
|
||||||
"foo > 12",
|
|
||||||
"foo < 12",
|
|
||||||
"foo <= 12",
|
|
||||||
"foo >= 12",
|
|
||||||
"foo <> 12",
|
|
||||||
"foo == 12",
|
|
||||||
"foo != 12",
|
|
||||||
"foo contains 12",
|
|
||||||
"foo contains 'bar'",
|
|
||||||
"foo != 'bar'",
|
|
||||||
"'foo' contains 'bar'",
|
|
||||||
'234089',
|
|
||||||
"foo | default: -1",
|
|
||||||
]
|
|
||||||
|
|
||||||
Benchmark.ips do |x|
|
|
||||||
x.config(time: 10, warmup: 5)
|
|
||||||
|
|
||||||
x.report("Liquid::Lexer#tokenize") do
|
|
||||||
EXPRESSIONS.each do |expr|
|
|
||||||
l = Liquid::Lexer.new(expr)
|
|
||||||
l.tokenize
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
x.compare!
|
|
||||||
end
|
|
||||||
@@ -6,112 +6,45 @@ class AssignTest < Minitest::Test
|
|||||||
include Liquid
|
include Liquid
|
||||||
|
|
||||||
def test_assign_with_hyphen_in_variable_name
|
def test_assign_with_hyphen_in_variable_name
|
||||||
template_source = <<~END_TEMPLATE
|
template_source = <<-END_TEMPLATE
|
||||||
{% assign this-thing = 'Print this-thing' -%}
|
{% assign this-thing = 'Print this-thing' %}
|
||||||
{{ this-thing -}}
|
{{ this-thing }}
|
||||||
END_TEMPLATE
|
END_TEMPLATE
|
||||||
assert_template_result("Print this-thing", template_source)
|
template = Template.parse(template_source)
|
||||||
|
rendered = template.render!
|
||||||
|
assert_equal("Print this-thing", rendered.strip)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_assigned_variable
|
def test_assigned_variable
|
||||||
assert_template_result(
|
assert_template_result('.foo.',
|
||||||
'.foo.',
|
|
||||||
'{% assign foo = values %}.{{ foo[0] }}.',
|
'{% assign foo = values %}.{{ foo[0] }}.',
|
||||||
{ 'values' => %w(foo bar baz) },
|
'values' => %w(foo bar baz))
|
||||||
)
|
|
||||||
|
|
||||||
assert_template_result(
|
assert_template_result('.bar.',
|
||||||
'.bar.',
|
|
||||||
'{% assign foo = values %}.{{ foo[1] }}.',
|
'{% assign foo = values %}.{{ foo[1] }}.',
|
||||||
{ 'values' => %w(foo bar baz) },
|
'values' => %w(foo bar baz))
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_assign_with_filter
|
def test_assign_with_filter
|
||||||
assert_template_result(
|
assert_template_result('.bar.',
|
||||||
'.bar.',
|
|
||||||
'{% assign foo = values | split: "," %}.{{ foo[1] }}.',
|
'{% assign foo = values | split: "," %}.{{ foo[1] }}.',
|
||||||
{ 'values' => "foo,bar,baz" },
|
'values' => "foo,bar,baz")
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_assign_syntax_error
|
def test_assign_syntax_error
|
||||||
assert_match_syntax_error(/assign/, '{% assign foo not values %}.')
|
assert_match_syntax_error(/assign/,
|
||||||
|
'{% assign foo not values %}.',
|
||||||
|
'values' => "foo,bar,baz")
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_assign_uses_error_mode
|
def test_assign_uses_error_mode
|
||||||
assert_match_syntax_error(
|
with_error_mode(:strict) do
|
||||||
"Expected dotdot but found pipe in ",
|
assert_raises(SyntaxError) do
|
||||||
"{% assign foo = ('X' | downcase) %}",
|
Template.parse("{% assign foo = ('X' | downcase) %}")
|
||||||
error_mode: :strict,
|
end
|
||||||
)
|
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
with_error_mode(:lax) do
|
||||||
def value
|
assert Template.parse("{% assign foo = ('X' | downcase) %}")
|
||||||
@obj
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end # AssignTest
|
||||||
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
|
|
||||||
|
|||||||
@@ -9,6 +9,12 @@ class FoobarTag < Liquid::Tag
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
class BlankTestFileSystem
|
||||||
|
def read_template_file(template_path)
|
||||||
|
template_path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
class BlankTest < Minitest::Test
|
class BlankTest < Minitest::Test
|
||||||
include Liquid
|
include Liquid
|
||||||
N = 10
|
N = 10
|
||||||
@@ -27,7 +33,7 @@ class BlankTest < Minitest::Test
|
|||||||
|
|
||||||
def test_new_tags_are_not_blank_by_default
|
def test_new_tags_are_not_blank_by_default
|
||||||
with_custom_tag('foobar', FoobarTag) do
|
with_custom_tag('foobar', FoobarTag) do
|
||||||
assert_equal(" " * N, Liquid::Template.parse(wrap_in_for("{% foobar %}")).render!)
|
assert_template_result(" " * N, wrap_in_for("{% foobar %}"))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -57,11 +63,9 @@ class BlankTest < Minitest::Test
|
|||||||
|
|
||||||
def test_nested_blocks_are_blank_but_only_if_all_children_are
|
def test_nested_blocks_are_blank_but_only_if_all_children_are
|
||||||
assert_template_result("", wrap(wrap(" ")))
|
assert_template_result("", wrap(wrap(" ")))
|
||||||
assert_template_result(
|
assert_template_result("\n but this is not " * (N + 1),
|
||||||
"\n but this is not " * (N + 1),
|
|
||||||
wrap('{% if true %} {% comment %} this is blank {% endcomment %} {% endif %}
|
wrap('{% if true %} {% comment %} this is blank {% endcomment %} {% endif %}
|
||||||
{% if true %} but this is not {% endif %}'),
|
{% if true %} but this is not {% endif %}'))
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_assigns_are_blank
|
def test_assigns_are_blank
|
||||||
@@ -91,21 +95,10 @@ class BlankTest < Minitest::Test
|
|||||||
end
|
end
|
||||||
|
|
||||||
def test_include_is_blank
|
def test_include_is_blank
|
||||||
assert_template_result(
|
Liquid::Template.file_system = BlankTestFileSystem.new
|
||||||
"foobar" * (N + 1),
|
assert_template_result("foobar" * (N + 1), wrap("{% include 'foobar' %}"))
|
||||||
wrap("{% include 'foobar' %}"),
|
assert_template_result(" foobar " * (N + 1), wrap("{% include ' foobar ' %}"))
|
||||||
partials: { 'foobar' => 'foobar' },
|
assert_template_result(" " * (N + 1), wrap(" {% include ' ' %} "))
|
||||||
)
|
|
||||||
assert_template_result(
|
|
||||||
" foobar " * (N + 1),
|
|
||||||
wrap("{% include ' foobar ' %}"),
|
|
||||||
partials: { ' foobar ' => ' foobar ' },
|
|
||||||
)
|
|
||||||
assert_template_result(
|
|
||||||
" " * (N + 1),
|
|
||||||
wrap(" {% include ' ' %} "),
|
|
||||||
partials: { ' ' => ' ' },
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_case_is_blank
|
def test_case_is_blank
|
||||||
|
|||||||
@@ -6,51 +6,9 @@ class BlockTest < Minitest::Test
|
|||||||
include Liquid
|
include Liquid
|
||||||
|
|
||||||
def test_unexpected_end_tag
|
def test_unexpected_end_tag
|
||||||
source = '{% if true %}{% endunless %}'
|
exc = assert_raises(SyntaxError) do
|
||||||
assert_match_syntax_error("Liquid syntax error (line 1): 'endunless' is not a valid delimiter for if tags. use endif", source)
|
Template.parse("{% if true %}{% endunless %}")
|
||||||
end
|
|
||||||
|
|
||||||
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)
|
|
||||||
end
|
end
|
||||||
|
assert_equal(exc.message, "Liquid syntax error: 'endunless' is not a valid delimiter for if tags. use endif")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -10,43 +10,43 @@ class CaptureTest < Minitest::Test
|
|||||||
end
|
end
|
||||||
|
|
||||||
def test_capture_with_hyphen_in_variable_name
|
def test_capture_with_hyphen_in_variable_name
|
||||||
template_source = <<~END_TEMPLATE
|
template_source = <<-END_TEMPLATE
|
||||||
{% capture this-thing %}Print this-thing{% endcapture -%}
|
{% capture this-thing %}Print this-thing{% endcapture %}
|
||||||
{{ this-thing -}}
|
{{ this-thing }}
|
||||||
END_TEMPLATE
|
END_TEMPLATE
|
||||||
assert_template_result("Print this-thing", template_source)
|
template = Template.parse(template_source)
|
||||||
|
rendered = template.render!
|
||||||
|
assert_equal("Print this-thing", rendered.strip)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_capture_to_variable_from_outer_scope_if_existing
|
def test_capture_to_variable_from_outer_scope_if_existing
|
||||||
template_source = <<~END_TEMPLATE
|
template_source = <<-END_TEMPLATE
|
||||||
{% assign var = '' -%}
|
{% assign var = '' %}
|
||||||
{% if true -%}
|
{% if true %}
|
||||||
{% capture var %}first-block-string{% endcapture -%}
|
{% capture var %}first-block-string{% endcapture %}
|
||||||
{% endif -%}
|
{% endif %}
|
||||||
{% if true -%}
|
{% if true %}
|
||||||
{% capture var %}test-string{% endcapture -%}
|
{% capture var %}test-string{% endcapture %}
|
||||||
{% endif -%}
|
{% endif %}
|
||||||
{{var-}}
|
{{var}}
|
||||||
END_TEMPLATE
|
END_TEMPLATE
|
||||||
assert_template_result("test-string", template_source)
|
template = Template.parse(template_source)
|
||||||
|
rendered = template.render!
|
||||||
|
assert_equal("test-string", rendered.gsub(/\s/, ''))
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_assigning_from_capture
|
def test_assigning_from_capture
|
||||||
template_source = <<~END_TEMPLATE
|
template_source = <<-END_TEMPLATE
|
||||||
{% assign first = '' -%}
|
{% assign first = '' %}
|
||||||
{% assign second = '' -%}
|
{% assign second = '' %}
|
||||||
{% for number in (1..3) -%}
|
{% for number in (1..3) %}
|
||||||
{% capture first %}{{number}}{% endcapture -%}
|
{% capture first %}{{number}}{% endcapture %}
|
||||||
{% assign second = first -%}
|
{% assign second = first %}
|
||||||
{% endfor -%}
|
{% endfor %}
|
||||||
{{ first }}-{{ second -}}
|
{{ first }}-{{ second }}
|
||||||
END_TEMPLATE
|
END_TEMPLATE
|
||||||
assert_template_result("3-3", template_source)
|
template = Template.parse(template_source)
|
||||||
|
rendered = template.render!
|
||||||
|
assert_equal("3-3", rendered.gsub(/\s/, ''))
|
||||||
end
|
end
|
||||||
|
end # CaptureTest
|
||||||
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
|
|
||||||
|
|||||||
@@ -2,616 +2,9 @@
|
|||||||
|
|
||||||
require 'test_helper'
|
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 ProductsDrop < Liquid::Drop
|
|
||||||
def initialize(products)
|
|
||||||
@products = products
|
|
||||||
end
|
|
||||||
|
|
||||||
def size
|
|
||||||
@products.size
|
|
||||||
end
|
|
||||||
|
|
||||||
def to_liquid
|
|
||||||
if @context["forloop"]
|
|
||||||
@products.first(@context["forloop"].length)
|
|
||||||
else
|
|
||||||
@products
|
|
||||||
end
|
|
||||||
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
|
class ContextTest < Minitest::Test
|
||||||
include Liquid
|
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
|
def test_override_global_filter
|
||||||
global = Module.new do
|
global = Module.new do
|
||||||
def notice(output)
|
def notice(output)
|
||||||
@@ -626,78 +19,16 @@ class ContextTest < Minitest::Test
|
|||||||
end
|
end
|
||||||
|
|
||||||
with_global_filter(global) do
|
with_global_filter(global) do
|
||||||
assert_equal('Global test', Template.parse("{{'test' | notice }}").render!)
|
assert_equal 'Global test', Template.parse("{{'test' | notice }}").render!
|
||||||
assert_equal('Local test', Template.parse("{{'test' | notice }}").render!({}, filters: [local]))
|
assert_equal 'Local test', Template.parse("{{'test' | notice }}").render!({}, filters: [local])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_has_key_will_not_add_an_error_for_missing_keys
|
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 = Context.new
|
||||||
context.key?('unknown')
|
context.key?('unknown')
|
||||||
assert_empty(context.errors)
|
assert_empty context.errors
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
def test_context_always_uses_static_registers
|
|
||||||
registers = {
|
|
||||||
my_register: :my_value,
|
|
||||||
}
|
|
||||||
c = Context.new({}, {}, registers)
|
|
||||||
assert_instance_of(Registers, c.registers)
|
|
||||||
assert_equal(:my_value, c.registers[:my_register])
|
|
||||||
|
|
||||||
r = Registers.new(registers)
|
|
||||||
c = Context.new({}, {}, r)
|
|
||||||
assert_instance_of(Registers, c.registers)
|
|
||||||
assert_equal(:my_value, c.registers[:my_register])
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_variable_to_liquid_returns_contextual_drop
|
|
||||||
context = {
|
|
||||||
"products" => ProductsDrop.new(["A", "B", "C", "D", "E"]),
|
|
||||||
}
|
|
||||||
|
|
||||||
template = Liquid::Template.parse(<<~LIQUID)
|
|
||||||
{%- for i in (1..3) -%}
|
|
||||||
for_loop_products_count: {{ products | size }}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
unscoped_products_count: {{ products | size }}
|
|
||||||
LIQUID
|
|
||||||
|
|
||||||
result = template.render(context)
|
|
||||||
|
|
||||||
assert_includes(result, "for_loop_products_count: 3")
|
|
||||||
assert_includes(result, "unscoped_products_count: 5")
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_new_isolated_context_inherits_parent_environment
|
|
||||||
global_environment = Liquid::Environment.build(tags: {})
|
|
||||||
context = Context.build(environment: global_environment)
|
|
||||||
|
|
||||||
subcontext = context.new_isolated_subcontext
|
|
||||||
assert_equal(global_environment, subcontext.environment)
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_newly_built_context_inherits_parent_environment
|
|
||||||
global_environment = Liquid::Environment.build(tags: {})
|
|
||||||
context = Context.build(environment: global_environment)
|
|
||||||
assert_equal(global_environment, context.environment)
|
|
||||||
assert(context.environment.tags.each.to_a.empty?)
|
|
||||||
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
|
|
||||||
|
|||||||
@@ -6,12 +6,16 @@ class DocumentTest < Minitest::Test
|
|||||||
include Liquid
|
include Liquid
|
||||||
|
|
||||||
def test_unexpected_outer_tag
|
def test_unexpected_outer_tag
|
||||||
source = "{% else %}"
|
exc = assert_raises(SyntaxError) do
|
||||||
assert_match_syntax_error("Liquid syntax error (line 1): Unexpected outer 'else' tag", source)
|
Template.parse("{% else %}")
|
||||||
|
end
|
||||||
|
assert_equal(exc.message, "Liquid syntax error: Unexpected outer 'else' tag")
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_unknown_tag
|
def test_unknown_tag
|
||||||
source = "{% foo %}"
|
exc = assert_raises(SyntaxError) do
|
||||||
assert_match_syntax_error("Liquid syntax error (line 1): Unknown tag 'foo'", source)
|
Template.parse("{% foo %}")
|
||||||
|
end
|
||||||
|
assert_equal(exc.message, "Liquid syntax error: Unknown tag 'foo'")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ class DropsTest < Minitest::Test
|
|||||||
def test_object_methods_not_allowed
|
def test_object_methods_not_allowed
|
||||||
[:dup, :clone, :singleton_class, :eval, :class_eval, :inspect].each do |method|
|
[:dup, :clone, :singleton_class, :eval, :class_eval, :inspect].each do |method|
|
||||||
output = Liquid::Template.parse(" {{ product.#{method} }} ").render!('product' => ProductDrop.new)
|
output = Liquid::Template.parse(" {{ product.#{method} }} ").render!('product' => ProductDrop.new)
|
||||||
assert_equal(' ', output)
|
assert_equal ' ', output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -210,28 +210,28 @@ class DropsTest < Minitest::Test
|
|||||||
|
|
||||||
def test_enumerable_drop_will_invoke_liquid_method_missing_for_clashing_method_names
|
def test_enumerable_drop_will_invoke_liquid_method_missing_for_clashing_method_names
|
||||||
["select", "each", "map", "cycle"].each do |method|
|
["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' => 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' => RealEnumerableDrop.new))
|
assert_equal method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_some_enumerable_methods_still_get_invoked
|
def test_some_enumerable_methods_still_get_invoked
|
||||||
[:count, :max].each do |method|
|
[: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' => 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' => EnumerableDrop.new))
|
assert_equal "3", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new)
|
||||||
end
|
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|
|
[: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' => 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' => EnumerableDrop.new))
|
assert_equal "1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,9 @@ class ErrorHandlingTest < Minitest::Test
|
|||||||
end
|
end
|
||||||
|
|
||||||
def test_missing_endtag_parse_time_error
|
def test_missing_endtag_parse_time_error
|
||||||
assert_match_syntax_error(/: 'for' tag was never closed\z/, ' {% for a in b %} ... ')
|
assert_raises(Liquid::SyntaxError) do
|
||||||
|
Liquid::Template.parse(' {% for a in b %} ... ')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_unrecognized_operator
|
def test_unrecognized_operator
|
||||||
@@ -82,31 +84,37 @@ class ErrorHandlingTest < Minitest::Test
|
|||||||
end
|
end
|
||||||
|
|
||||||
def test_with_line_numbers_adds_numbers_to_parser_errors
|
def test_with_line_numbers_adds_numbers_to_parser_errors
|
||||||
source = <<~LIQUID
|
err = assert_raises(SyntaxError) do
|
||||||
foobar
|
Liquid::Template.parse('
|
||||||
|
foobar
|
||||||
|
|
||||||
{% "cat" | foobar %}
|
{% "cat" | foobar %}
|
||||||
|
|
||||||
bla
|
bla
|
||||||
LIQUID
|
',
|
||||||
assert_match_syntax_error(/Liquid syntax error \(line 3\)/, source)
|
line_numbers: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_match(/Liquid syntax error \(line 4\)/, err.message)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_with_line_numbers_adds_numbers_to_parser_errors_with_whitespace_trim
|
def test_with_line_numbers_adds_numbers_to_parser_errors_with_whitespace_trim
|
||||||
source = <<~LIQUID
|
err = assert_raises(SyntaxError) do
|
||||||
foobar
|
Liquid::Template.parse('
|
||||||
|
foobar
|
||||||
|
|
||||||
{%- "cat" | foobar -%}
|
{%- "cat" | foobar -%}
|
||||||
|
|
||||||
bla
|
bla
|
||||||
LIQUID
|
',
|
||||||
|
line_numbers: true)
|
||||||
|
end
|
||||||
|
|
||||||
assert_match_syntax_error(/Liquid syntax error \(line 3\)/, source)
|
assert_match(/Liquid syntax error \(line 4\)/, err.message)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_parsing_warn_with_line_numbers_adds_numbers_to_lexer_errors
|
def test_parsing_warn_with_line_numbers_adds_numbers_to_lexer_errors
|
||||||
template = Liquid::Template.parse(
|
template = Liquid::Template.parse('
|
||||||
'
|
|
||||||
foobar
|
foobar
|
||||||
|
|
||||||
{% if 1 =! 2 %}ok{% endif %}
|
{% if 1 =! 2 %}ok{% endif %}
|
||||||
@@ -114,19 +122,15 @@ class ErrorHandlingTest < Minitest::Test
|
|||||||
bla
|
bla
|
||||||
',
|
',
|
||||||
error_mode: :warn,
|
error_mode: :warn,
|
||||||
line_numbers: true,
|
line_numbers: true)
|
||||||
)
|
|
||||||
|
|
||||||
assert_equal(
|
assert_equal(['Liquid syntax error (line 4): Unexpected character = in "1 =! 2"'],
|
||||||
['Liquid syntax error (line 4): Unexpected character = in "1 =! 2"'],
|
template.warnings.map(&:message))
|
||||||
template.warnings.map(&:message),
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_parsing_strict_with_line_numbers_adds_numbers_to_lexer_errors
|
def test_parsing_strict_with_line_numbers_adds_numbers_to_lexer_errors
|
||||||
err = assert_raises(SyntaxError) do
|
err = assert_raises(SyntaxError) do
|
||||||
Liquid::Template.parse(
|
Liquid::Template.parse('
|
||||||
'
|
|
||||||
foobar
|
foobar
|
||||||
|
|
||||||
{% if 1 =! 2 %}ok{% endif %}
|
{% if 1 =! 2 %}ok{% endif %}
|
||||||
@@ -134,25 +138,27 @@ class ErrorHandlingTest < Minitest::Test
|
|||||||
bla
|
bla
|
||||||
',
|
',
|
||||||
error_mode: :strict,
|
error_mode: :strict,
|
||||||
line_numbers: true,
|
line_numbers: true)
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
assert_equal('Liquid syntax error (line 4): Unexpected character = in "1 =! 2"', err.message)
|
assert_equal('Liquid syntax error (line 4): Unexpected character = in "1 =! 2"', err.message)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_syntax_errors_in_nested_blocks_have_correct_line_number
|
def test_syntax_errors_in_nested_blocks_have_correct_line_number
|
||||||
source = <<~LIQUID
|
err = assert_raises(SyntaxError) do
|
||||||
foobar
|
Liquid::Template.parse('
|
||||||
|
foobar
|
||||||
|
|
||||||
{% if 1 != 2 %}
|
{% if 1 != 2 %}
|
||||||
{% foo %}
|
{% foo %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
bla
|
bla
|
||||||
LIQUID
|
',
|
||||||
|
line_numbers: true)
|
||||||
|
end
|
||||||
|
|
||||||
assert_match_syntax_error("Liquid syntax error (line 4): Unknown tag 'foo'", source)
|
assert_equal("Liquid syntax error (line 5): Unknown tag 'foo'", err.message)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_strict_error_messages
|
def test_strict_error_messages
|
||||||
@@ -187,7 +193,7 @@ class ErrorHandlingTest < Minitest::Test
|
|||||||
|
|
||||||
# Liquid should not catch Exceptions that are not subclasses of StandardError, like Interrupt and NoMemoryError
|
# Liquid should not catch Exceptions that are not subclasses of StandardError, like Interrupt and NoMemoryError
|
||||||
def test_exceptions_propagate
|
def test_exceptions_propagate
|
||||||
assert_raises(Exception) do
|
assert_raises Exception do
|
||||||
template = Liquid::Template.parse('{{ errors.exception }}')
|
template = Liquid::Template.parse('{{ errors.exception }}')
|
||||||
template.render('errors' => ErrorDrop.new)
|
template.render('errors' => ErrorDrop.new)
|
||||||
end
|
end
|
||||||
@@ -203,34 +209,20 @@ class ErrorHandlingTest < Minitest::Test
|
|||||||
end
|
end
|
||||||
|
|
||||||
def test_setting_default_exception_renderer
|
def test_setting_default_exception_renderer
|
||||||
|
old_exception_renderer = Liquid::Template.default_exception_renderer
|
||||||
exceptions = []
|
exceptions = []
|
||||||
default_exception_renderer = ->(e) {
|
Liquid::Template.default_exception_renderer = ->(e) {
|
||||||
exceptions << e
|
exceptions << e
|
||||||
''
|
''
|
||||||
}
|
}
|
||||||
|
template = Liquid::Template.parse('This is a runtime error: {{ errors.argument_error }}')
|
||||||
env = Liquid::Environment.build(exception_renderer: default_exception_renderer)
|
|
||||||
template = Liquid::Template.parse('This is a runtime error: {{ errors.argument_error }}', environment: env)
|
|
||||||
|
|
||||||
output = template.render('errors' => ErrorDrop.new)
|
output = template.render('errors' => ErrorDrop.new)
|
||||||
|
|
||||||
assert_equal('This is a runtime error: ', output)
|
assert_equal('This is a runtime error: ', output)
|
||||||
assert_equal([Liquid::ArgumentError], template.errors.map(&:class))
|
assert_equal([Liquid::ArgumentError], template.errors.map(&:class))
|
||||||
end
|
ensure
|
||||||
|
Liquid::Template.default_exception_renderer = old_exception_renderer if old_exception_renderer
|
||||||
def test_setting_exception_renderer_on_environment
|
|
||||||
exceptions = []
|
|
||||||
exception_renderer = ->(e) do
|
|
||||||
exceptions << e
|
|
||||||
''
|
|
||||||
end
|
|
||||||
|
|
||||||
environment = Liquid::Environment.build(exception_renderer: exception_renderer)
|
|
||||||
template = Liquid::Template.parse('This is a runtime error: {{ errors.argument_error }}', environment: environment)
|
|
||||||
output = template.render('errors' => ErrorDrop.new)
|
|
||||||
|
|
||||||
assert_equal('This is a runtime error: ', output)
|
|
||||||
assert_equal([Liquid::ArgumentError], template.errors.map(&:class))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_exception_renderer_exposing_non_liquid_error
|
def test_exception_renderer_exposing_non_liquid_error
|
||||||
@@ -256,96 +248,17 @@ class ErrorHandlingTest < Minitest::Test
|
|||||||
end
|
end
|
||||||
|
|
||||||
def test_included_template_name_with_line_numbers
|
def test_included_template_name_with_line_numbers
|
||||||
environment = Liquid::Environment.build(file_system: TestFileSystem.new)
|
old_file_system = Liquid::Template.file_system
|
||||||
template = Liquid::Template.parse("Argument error:\n{% include 'product' %}", line_numbers: true, environment: environment)
|
|
||||||
page = template.render('errors' => ErrorDrop.new)
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
ensure
|
||||||
|
Liquid::Template.file_system = old_file_system
|
||||||
|
end
|
||||||
assert_equal("Argument error:\nLiquid error (product line 1): argument error", page)
|
assert_equal("Argument error:\nLiquid error (product line 1): argument error", page)
|
||||||
assert_equal("product", template.errors.first.template_name)
|
assert_equal("product", template.errors.first.template_name)
|
||||||
end
|
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),
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,156 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'test_helper'
|
|
||||||
require 'lru_redux'
|
|
||||||
|
|
||||||
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' }}")
|
|
||||||
assert_template_result("emoji🔥", "{{ 'emoji🔥' }}")
|
|
||||||
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("-17.42", "{{ -17.42 }}")
|
|
||||||
assert_template_result("2.5", "{{ 2.5 }}")
|
|
||||||
assert_expression_result(0.0, "0.....5")
|
|
||||||
assert_expression_result(0.0, "-0..1")
|
|
||||||
assert_expression_result(1.5, "1.5")
|
|
||||||
|
|
||||||
# this is a unfortunate quirky behavior of Liquid
|
|
||||||
result = Expression.parse(".5")
|
|
||||||
assert_kind_of(Liquid::VariableLookup, result)
|
|
||||||
|
|
||||||
result = Expression.parse("-.5")
|
|
||||||
assert_kind_of(Liquid::VariableLookup, result)
|
|
||||||
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
|
|
||||||
|
|
||||||
def test_quirky_negative_sign_expression_markup
|
|
||||||
result = Expression.parse("-", nil)
|
|
||||||
assert(result.is_a?(VariableLookup))
|
|
||||||
assert_equal("-", result.name)
|
|
||||||
|
|
||||||
# for this template, the expression markup is "-"
|
|
||||||
assert_template_result(
|
|
||||||
"",
|
|
||||||
"{{ - 'theme.css' - }}",
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_expression_cache
|
|
||||||
skip("Liquid-C does not support Expression caching") if defined?(Liquid::C) && Liquid::C.enabled
|
|
||||||
|
|
||||||
cache = {}
|
|
||||||
template = <<~LIQUID
|
|
||||||
{% assign x = 1 %}
|
|
||||||
{{ x }}
|
|
||||||
{% assign x = 2 %}
|
|
||||||
{{ x }}
|
|
||||||
{% assign y = 1 %}
|
|
||||||
{{ y }}
|
|
||||||
LIQUID
|
|
||||||
|
|
||||||
Liquid::Template.parse(template, expression_cache: cache).render
|
|
||||||
|
|
||||||
assert_equal(
|
|
||||||
["1", "2", "x", "y"],
|
|
||||||
cache.to_a.map { _1[0] }.sort,
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_expression_cache_with_true_boolean
|
|
||||||
skip("Liquid-C does not support Expression caching") if defined?(Liquid::C) && Liquid::C.enabled
|
|
||||||
|
|
||||||
template = <<~LIQUID
|
|
||||||
{% assign x = 1 %}
|
|
||||||
{{ x }}
|
|
||||||
{% assign x = 2 %}
|
|
||||||
{{ x }}
|
|
||||||
{% assign y = 1 %}
|
|
||||||
{{ y }}
|
|
||||||
LIQUID
|
|
||||||
|
|
||||||
parse_context = ParseContext.new(expression_cache: true)
|
|
||||||
|
|
||||||
Liquid::Template.parse(template, parse_context).render
|
|
||||||
|
|
||||||
cache = parse_context.instance_variable_get(:@expression_cache)
|
|
||||||
|
|
||||||
assert_equal(
|
|
||||||
["1", "2", "x", "y"],
|
|
||||||
cache.to_a.map { _1[0] }.sort,
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_expression_cache_with_lru_redux
|
|
||||||
skip("Liquid-C does not support Expression caching") if defined?(Liquid::C) && Liquid::C.enabled
|
|
||||||
|
|
||||||
cache = LruRedux::Cache.new(10)
|
|
||||||
template = <<~LIQUID
|
|
||||||
{% assign x = 1 %}
|
|
||||||
{{ x }}
|
|
||||||
{% assign x = 2 %}
|
|
||||||
{{ x }}
|
|
||||||
{% assign y = 1 %}
|
|
||||||
{{ y }}
|
|
||||||
LIQUID
|
|
||||||
|
|
||||||
Liquid::Template.parse(template, expression_cache: cache).render
|
|
||||||
|
|
||||||
assert_equal(
|
|
||||||
["1", "2", "x", "y"],
|
|
||||||
cache.to_a.map { _1[0] }.sort,
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_disable_expression_cache
|
|
||||||
skip("Liquid-C does not support Expression caching") if defined?(Liquid::C) && Liquid::C.enabled
|
|
||||||
|
|
||||||
template = <<~LIQUID
|
|
||||||
{% assign x = 1 %}
|
|
||||||
{{ x }}
|
|
||||||
{% assign x = 2 %}
|
|
||||||
{{ x }}
|
|
||||||
{% assign y = 1 %}
|
|
||||||
{{ y }}
|
|
||||||
LIQUID
|
|
||||||
|
|
||||||
parse_context = Liquid::ParseContext.new(expression_cache: false)
|
|
||||||
Liquid::Template.parse(template, parse_context).render
|
|
||||||
assert(parse_context.instance_variable_get(:@expression_cache).nil?)
|
|
||||||
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
|
|
||||||
@@ -59,87 +59,84 @@ class FiltersTest < Minitest::Test
|
|||||||
end
|
end
|
||||||
|
|
||||||
def test_size
|
def test_size
|
||||||
assert_template_result("4", "{{var | size}}", { "var" => 'abcd' })
|
@context['var'] = 'abcd'
|
||||||
|
@context.add_filters(MoneyFilter)
|
||||||
|
|
||||||
|
assert_equal('4', Template.parse("{{var | size}}").render(@context))
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_join
|
def test_join
|
||||||
assert_template_result("1 2 3 4", "{{var | join}}", { "var" => [1, 2, 3, 4] })
|
@context['var'] = [1, 2, 3, 4]
|
||||||
|
|
||||||
|
assert_equal("1 2 3 4", Template.parse("{{var | join}}").render(@context))
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_sort
|
def test_sort
|
||||||
assert_template_result("1 2 3 4", "{{numbers | sort | join}}", { "numbers" => [2, 1, 4, 3] })
|
@context['value'] = 3
|
||||||
assert_template_result(
|
@context['numbers'] = [2, 1, 4, 3]
|
||||||
"alphabetic as expected",
|
@context['words'] = ['expected', 'as', 'alphabetic']
|
||||||
"{{words | sort | join}}",
|
@context['arrays'] = ['flower', 'are']
|
||||||
{ "words" => ['expected', 'as', 'alphabetic'] },
|
@context['case_sensitive'] = ['sensitive', 'Expected', 'case']
|
||||||
)
|
|
||||||
assert_template_result("3", "{{value | sort}}", { "value" => 3 })
|
assert_equal('1 2 3 4', Template.parse("{{numbers | sort | join}}").render(@context))
|
||||||
assert_template_result('are flower', "{{arrays | sort | join}}", { 'arrays' => ['flower', 'are'] })
|
assert_equal('alphabetic as expected', Template.parse("{{words | sort | join}}").render(@context))
|
||||||
assert_template_result(
|
assert_equal('3', Template.parse("{{value | sort}}").render(@context))
|
||||||
"Expected case sensitive",
|
assert_equal('are flower', Template.parse("{{arrays | sort | join}}").render(@context))
|
||||||
"{{case_sensitive | sort | join}}",
|
assert_equal('Expected case sensitive', Template.parse("{{case_sensitive | sort | join}}").render(@context))
|
||||||
{ "case_sensitive" => ["sensitive", "Expected", "case"] },
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_sort_natural
|
def test_sort_natural
|
||||||
|
@context['words'] = ['case', 'Assert', 'Insensitive']
|
||||||
|
@context['hashes'] = [{ 'a' => 'A' }, { 'a' => 'b' }, { 'a' => 'C' }]
|
||||||
|
@context['objects'] = [TestObject.new('A'), TestObject.new('b'), TestObject.new('C')]
|
||||||
|
|
||||||
# Test strings
|
# Test strings
|
||||||
assert_template_result(
|
assert_equal('Assert case Insensitive', Template.parse("{{words | sort_natural | join}}").render(@context))
|
||||||
"Assert case Insensitive",
|
|
||||||
"{{words | sort_natural | join}}",
|
|
||||||
{ "words" => ["case", "Assert", "Insensitive"] },
|
|
||||||
)
|
|
||||||
|
|
||||||
# Test hashes
|
# Test hashes
|
||||||
assert_template_result(
|
assert_equal('A b C', Template.parse("{{hashes | sort_natural: 'a' | map: 'a' | join}}").render(@context))
|
||||||
"A b C",
|
|
||||||
"{{hashes | sort_natural: 'a' | map: 'a' | join}}",
|
|
||||||
{ "hashes" => [{ "a" => "A" }, { "a" => "b" }, { "a" => "C" }] },
|
|
||||||
)
|
|
||||||
|
|
||||||
# Test objects
|
# Test objects
|
||||||
@context['objects'] = [TestObject.new('A'), TestObject.new('b'), TestObject.new('C')]
|
|
||||||
assert_equal('A b C', Template.parse("{{objects | sort_natural: 'a' | map: 'a' | join}}").render(@context))
|
assert_equal('A b C', Template.parse("{{objects | sort_natural: 'a' | map: 'a' | join}}").render(@context))
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_compact
|
def test_compact
|
||||||
|
@context['words'] = ['a', nil, 'b', nil, 'c']
|
||||||
|
@context['hashes'] = [{ 'a' => 'A' }, { 'a' => nil }, { 'a' => 'C' }]
|
||||||
|
@context['objects'] = [TestObject.new('A'), TestObject.new(nil), TestObject.new('C')]
|
||||||
|
|
||||||
# Test strings
|
# Test strings
|
||||||
assert_template_result(
|
assert_equal('a b c', Template.parse("{{words | compact | join}}").render(@context))
|
||||||
"a b c",
|
|
||||||
"{{words | compact | join}}",
|
|
||||||
{ "words" => ['a', nil, 'b', nil, 'c'] },
|
|
||||||
)
|
|
||||||
|
|
||||||
# Test hashes
|
# Test hashes
|
||||||
assert_template_result(
|
assert_equal('A C', Template.parse("{{hashes | compact: 'a' | map: 'a' | join}}").render(@context))
|
||||||
"A C",
|
|
||||||
"{{hashes | compact: 'a' | map: 'a' | join}}",
|
|
||||||
{ "hashes" => [{ "a" => "A" }, { "a" => nil }, { "a" => "C" }] },
|
|
||||||
)
|
|
||||||
|
|
||||||
# Test objects
|
# Test objects
|
||||||
@context['objects'] = [TestObject.new('A'), TestObject.new(nil), TestObject.new('C')]
|
|
||||||
assert_equal('A C', Template.parse("{{objects | compact: 'a' | map: 'a' | join}}").render(@context))
|
assert_equal('A C', Template.parse("{{objects | compact: 'a' | map: 'a' | join}}").render(@context))
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_strip_html
|
def test_strip_html
|
||||||
assert_template_result("bla blub", "{{ var | strip_html }}", { "var" => "<b>bla blub</a>" })
|
@context['var'] = "<b>bla blub</a>"
|
||||||
|
|
||||||
|
assert_equal("bla blub", Template.parse("{{ var | strip_html }}").render(@context))
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_strip_html_ignore_comments_with_html
|
def test_strip_html_ignore_comments_with_html
|
||||||
assert_template_result(
|
@context['var'] = "<!-- split and some <ul> tag --><b>bla blub</a>"
|
||||||
"bla blub",
|
|
||||||
"{{ var | strip_html }}",
|
assert_equal("bla blub", Template.parse("{{ var | strip_html }}").render(@context))
|
||||||
{ "var" => "<!-- split and some <ul> tag --><b>bla blub</a>" },
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_capitalize
|
def test_capitalize
|
||||||
assert_template_result("Blub", "{{ var | capitalize }}", { "var" => "blub" })
|
@context['var'] = "blub"
|
||||||
|
|
||||||
|
assert_equal("Blub", Template.parse("{{ var | capitalize }}").render(@context))
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_nonexistent_filter_is_ignored
|
def test_nonexistent_filter_is_ignored
|
||||||
assert_template_result("1000", "{{ var | xyzzy }}", { "var" => 1000 })
|
@context['var'] = 1000
|
||||||
|
|
||||||
|
assert_equal('1000', Template.parse("{{ var | xyzzy }}").render(@context))
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_filter_with_keyword_arguments
|
def test_filter_with_keyword_arguments
|
||||||
@@ -156,15 +153,6 @@ class FiltersTest < Minitest::Test
|
|||||||
# tap still treated as a non-existent filter
|
# tap still treated as a non-existent filter
|
||||||
assert_equal("1000", Template.parse("{{var | tap}}").render!('var' => 1000))
|
assert_equal("1000", Template.parse("{{var | tap}}").render!('var' => 1000))
|
||||||
end
|
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)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
class FiltersInTemplate < Minitest::Test
|
class FiltersInTemplate < Minitest::Test
|
||||||
@@ -172,9 +160,9 @@ class FiltersInTemplate < Minitest::Test
|
|||||||
|
|
||||||
def test_local_global
|
def test_local_global
|
||||||
with_global_filter(MoneyFilter) do
|
with_global_filter(MoneyFilter) do
|
||||||
assert_equal(" 1000$ ", Template.parse("{{1000 | money}}").render!(nil, nil))
|
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$ CAD ", Template.parse("{{1000 | money}}").render!(nil, filters: [CanadianMoneyFilter]))
|
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, filters: [CanadianMoneyFilter])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class HashOrderingTest < Minitest::Test
|
|||||||
|
|
||||||
def test_global_register_order
|
def test_global_register_order
|
||||||
with_global_filter(MoneyFilter, CanadianMoneyFilter) do
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'test_helper'
|
|
||||||
|
|
||||||
class HashRenderingTest < Minitest::Test
|
|
||||||
def test_render_empty_hash
|
|
||||||
assert_template_result("{}", "{{ my_hash }}", { "my_hash" => {} })
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_render_hash_with_string_keys_and_values
|
|
||||||
assert_template_result("{\"key1\"=>\"value1\", \"key2\"=>\"value2\"}", "{{ my_hash }}", { "my_hash" => { "key1" => "value1", "key2" => "value2" } })
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_render_hash_with_symbol_keys_and_integer_values
|
|
||||||
assert_template_result("{:key1=>1, :key2=>2}", "{{ my_hash }}", { "my_hash" => { key1: 1, key2: 2 } })
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_render_nested_hash
|
|
||||||
assert_template_result("{\"outer\"=>{\"inner\"=>\"value\"}}", "{{ my_hash }}", { "my_hash" => { "outer" => { "inner" => "value" } } })
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_render_hash_with_array_values
|
|
||||||
assert_template_result("{\"numbers\"=>[1, 2, 3]}", "{{ my_hash }}", { "my_hash" => { "numbers" => [1, 2, 3] } })
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_render_recursive_hash
|
|
||||||
recursive_hash = { "self" => {} }
|
|
||||||
recursive_hash["self"]["self"] = recursive_hash
|
|
||||||
assert_template_result("{\"self\"=>{\"self\"=>{...}}}", "{{ my_hash }}", { "my_hash" => recursive_hash })
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_hash_with_downcase_filter
|
|
||||||
assert_template_result("{\"key\"=>\"value\", \"anotherkey\"=>\"anothervalue\"}", "{{ my_hash | downcase }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_hash_with_upcase_filter
|
|
||||||
assert_template_result("{\"KEY\"=>\"VALUE\", \"ANOTHERKEY\"=>\"ANOTHERVALUE\"}", "{{ my_hash | upcase }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_hash_with_strip_filter
|
|
||||||
assert_template_result("{\"Key\"=>\"Value\", \"AnotherKey\"=>\"AnotherValue\"}", "{{ my_hash | strip }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_hash_with_escape_filter
|
|
||||||
assert_template_result("{"Key"=>"Value", "AnotherKey"=>"AnotherValue"}", "{{ my_hash | escape }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_hash_with_url_encode_filter
|
|
||||||
assert_template_result("%7B%22Key%22%3D%3E%22Value%22%2C+%22AnotherKey%22%3D%3E%22AnotherValue%22%7D", "{{ my_hash | url_encode }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_hash_with_strip_html_filter
|
|
||||||
assert_template_result("{\"Key\"=>\"Value\", \"AnotherKey\"=>\"AnotherValue\"}", "{{ my_hash | strip_html }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_hash_with_truncate__20_filter
|
|
||||||
assert_template_result("{\"Key\"=>\"Value\", ...", "{{ my_hash | truncate: 20 }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_hash_with_replace___key____replaced_key__filter
|
|
||||||
assert_template_result("{\"Key\"=>\"Value\", \"AnotherKey\"=>\"AnotherValue\"}", "{{ my_hash | replace: 'key', 'replaced_key' }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_hash_with_append____appended_text__filter
|
|
||||||
assert_template_result("{\"Key\"=>\"Value\", \"AnotherKey\"=>\"AnotherValue\"} appended text", "{{ my_hash | append: ' appended text' }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_hash_with_prepend___prepended_text___filter
|
|
||||||
assert_template_result("prepended text {\"Key\"=>\"Value\", \"AnotherKey\"=>\"AnotherValue\"}", "{{ my_hash | prepend: 'prepended text ' }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_render_hash_with_array_values_empty
|
|
||||||
assert_template_result("{\"numbers\"=>[]}", "{{ my_hash }}", { "my_hash" => { "numbers" => [] } })
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_render_hash_with_array_values_hash
|
|
||||||
assert_template_result("{\"numbers\"=>[{:foo=>42}]}", "{{ my_hash }}", { "my_hash" => { "numbers" => [{ foo: 42 }] } })
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_render_hash_with_hash_key
|
|
||||||
assert_template_result("{{\"foo\"=>\"bar\"}=>42}", "{{ my_hash }}", { "my_hash" => { Hash["foo" => "bar"] => 42 } })
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -33,25 +33,31 @@ class OutputTest < Minitest::Test
|
|||||||
|
|
||||||
def setup
|
def setup
|
||||||
@assigns = {
|
@assigns = {
|
||||||
|
'best_cars' => 'bmw',
|
||||||
'car' => { 'bmw' => 'good', 'gm' => 'bad' },
|
'car' => { 'bmw' => 'good', 'gm' => 'bad' },
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_variable
|
def test_variable
|
||||||
assert_template_result(" bmw ", " {{best_cars}} ", { "best_cars" => "bmw" })
|
text = %( {{best_cars}} )
|
||||||
|
|
||||||
|
expected = %( bmw )
|
||||||
|
assert_equal(expected, Template.parse(text).render!(@assigns))
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_variable_traversing_with_two_brackets
|
def test_variable_traversing_with_two_brackets
|
||||||
source = "{{ site.data.menu[include.menu][include.locale] }}"
|
text = %({{ site.data.menu[include.menu][include.locale] }})
|
||||||
assert_template_result("it works!", source, {
|
assert_equal("it works!", Template.parse(text).render!(
|
||||||
"site" => { "data" => { "menu" => { "foo" => { "bar" => "it works!" } } } },
|
"site" => { "data" => { "menu" => { "foo" => { "bar" => "it works!" } } } },
|
||||||
"include" => { "menu" => "foo", "locale" => "bar" },
|
"include" => { "menu" => "foo", "locale" => "bar" }
|
||||||
})
|
))
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_variable_traversing
|
def test_variable_traversing
|
||||||
source = " {{car.bmw}} {{car.gm}} {{car.bmw}} "
|
text = %( {{car.bmw}} {{car.gm}} {{car.bmw}} )
|
||||||
assert_template_result(" good bad good ", source, @assigns)
|
|
||||||
|
expected = %( good bad good )
|
||||||
|
assert_equal(expected, Template.parse(text).render!(@assigns))
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_variable_piping
|
def test_variable_piping
|
||||||
@@ -104,11 +110,10 @@ class OutputTest < Minitest::Test
|
|||||||
end
|
end
|
||||||
|
|
||||||
def test_multiple_pipings
|
def test_multiple_pipings
|
||||||
assigns = { 'best_cars' => 'bmw' }
|
|
||||||
text = %( {{ best_cars | cite_funny | paragraph }} )
|
text = %( {{ best_cars | cite_funny | paragraph }} )
|
||||||
expected = %( <p>LOL: bmw</p> )
|
expected = %( <p>LOL: bmw</p> )
|
||||||
|
|
||||||
assert_equal(expected, Template.parse(text).render!(assigns, filters: [FunnyFilter]))
|
assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_link_to
|
def test_link_to
|
||||||
|
|||||||
+32
-60
@@ -8,245 +8,217 @@ class ParseTreeVisitorTest < Minitest::Test
|
|||||||
def test_variable
|
def test_variable
|
||||||
assert_equal(
|
assert_equal(
|
||||||
["test"],
|
["test"],
|
||||||
visit(%({{ test }})),
|
visit(%({{ test }}))
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_varible_with_filter
|
def test_varible_with_filter
|
||||||
assert_equal(
|
assert_equal(
|
||||||
["test", "infilter"],
|
["test", "infilter"],
|
||||||
visit(%({{ test | split: infilter }})),
|
visit(%({{ test | split: infilter }}))
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_dynamic_variable
|
def test_dynamic_variable
|
||||||
assert_equal(
|
assert_equal(
|
||||||
["test", "inlookup"],
|
["test", "inlookup"],
|
||||||
visit(%({{ test[inlookup] }})),
|
visit(%({{ test[inlookup] }}))
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_echo
|
|
||||||
assert_equal(
|
|
||||||
["test"],
|
|
||||||
visit(%({% echo test %})),
|
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_if_condition
|
def test_if_condition
|
||||||
assert_equal(
|
assert_equal(
|
||||||
["test"],
|
["test"],
|
||||||
visit(%({% if test %}{% endif %})),
|
visit(%({% if test %}{% endif %}))
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_complex_if_condition
|
def test_complex_if_condition
|
||||||
assert_equal(
|
assert_equal(
|
||||||
["test"],
|
["test"],
|
||||||
visit(%({% if 1 == 1 and 2 == test %}{% endif %})),
|
visit(%({% if 1 == 1 and 2 == test %}{% endif %}))
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_if_body
|
def test_if_body
|
||||||
assert_equal(
|
assert_equal(
|
||||||
["test"],
|
["test"],
|
||||||
visit(%({% if 1 == 1 %}{{ test }}{% endif %})),
|
visit(%({% if 1 == 1 %}{{ test }}{% endif %}))
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_unless_condition
|
def test_unless_condition
|
||||||
assert_equal(
|
assert_equal(
|
||||||
["test"],
|
["test"],
|
||||||
visit(%({% unless test %}{% endunless %})),
|
visit(%({% unless test %}{% endunless %}))
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_complex_unless_condition
|
def test_complex_unless_condition
|
||||||
assert_equal(
|
assert_equal(
|
||||||
["test"],
|
["test"],
|
||||||
visit(%({% unless 1 == 1 and 2 == test %}{% endunless %})),
|
visit(%({% unless 1 == 1 and 2 == test %}{% endunless %}))
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_unless_body
|
def test_unless_body
|
||||||
assert_equal(
|
assert_equal(
|
||||||
["test"],
|
["test"],
|
||||||
visit(%({% unless 1 == 1 %}{{ test }}{% endunless %})),
|
visit(%({% unless 1 == 1 %}{{ test }}{% endunless %}))
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_elsif_condition
|
def test_elsif_condition
|
||||||
assert_equal(
|
assert_equal(
|
||||||
["test"],
|
["test"],
|
||||||
visit(%({% if 1 == 1 %}{% elsif test %}{% endif %})),
|
visit(%({% if 1 == 1 %}{% elsif test %}{% endif %}))
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_complex_elsif_condition
|
def test_complex_elsif_condition
|
||||||
assert_equal(
|
assert_equal(
|
||||||
["test"],
|
["test"],
|
||||||
visit(%({% if 1 == 1 %}{% elsif 1 == 1 and 2 == test %}{% endif %})),
|
visit(%({% if 1 == 1 %}{% elsif 1 == 1 and 2 == test %}{% endif %}))
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_elsif_body
|
def test_elsif_body
|
||||||
assert_equal(
|
assert_equal(
|
||||||
["test"],
|
["test"],
|
||||||
visit(%({% if 1 == 1 %}{% elsif 2 == 2 %}{{ test }}{% endif %})),
|
visit(%({% if 1 == 1 %}{% elsif 2 == 2 %}{{ test }}{% endif %}))
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_else_body
|
def test_else_body
|
||||||
assert_equal(
|
assert_equal(
|
||||||
["test"],
|
["test"],
|
||||||
visit(%({% if 1 == 1 %}{% else %}{{ test }}{% endif %})),
|
visit(%({% if 1 == 1 %}{% else %}{{ test }}{% endif %}))
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_case_left
|
def test_case_left
|
||||||
assert_equal(
|
assert_equal(
|
||||||
["test"],
|
["test"],
|
||||||
visit(%({% case test %}{% endcase %})),
|
visit(%({% case test %}{% endcase %}))
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_case_condition
|
def test_case_condition
|
||||||
assert_equal(
|
assert_equal(
|
||||||
["test"],
|
["test"],
|
||||||
visit(%({% case 1 %}{% when test %}{% endcase %})),
|
visit(%({% case 1 %}{% when test %}{% endcase %}))
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_case_when_body
|
def test_case_when_body
|
||||||
assert_equal(
|
assert_equal(
|
||||||
["test"],
|
["test"],
|
||||||
visit(%({% case 1 %}{% when 2 %}{{ test }}{% endcase %})),
|
visit(%({% case 1 %}{% when 2 %}{{ test }}{% endcase %}))
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_case_else_body
|
def test_case_else_body
|
||||||
assert_equal(
|
assert_equal(
|
||||||
["test"],
|
["test"],
|
||||||
visit(%({% case 1 %}{% else %}{{ test }}{% endcase %})),
|
visit(%({% case 1 %}{% else %}{{ test }}{% endcase %}))
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_for_in
|
def test_for_in
|
||||||
assert_equal(
|
assert_equal(
|
||||||
["test"],
|
["test"],
|
||||||
visit(%({% for x in test %}{% endfor %})),
|
visit(%({% for x in test %}{% endfor %}))
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_for_limit
|
def test_for_limit
|
||||||
assert_equal(
|
assert_equal(
|
||||||
["test"],
|
["test"],
|
||||||
visit(%({% for x in (1..5) limit: test %}{% endfor %})),
|
visit(%({% for x in (1..5) limit: test %}{% endfor %}))
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_for_offset
|
def test_for_offset
|
||||||
assert_equal(
|
assert_equal(
|
||||||
["test"],
|
["test"],
|
||||||
visit(%({% for x in (1..5) offset: test %}{% endfor %})),
|
visit(%({% for x in (1..5) offset: test %}{% endfor %}))
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_for_body
|
def test_for_body
|
||||||
assert_equal(
|
assert_equal(
|
||||||
["test"],
|
["test"],
|
||||||
visit(%({% for x in (1..5) %}{{ test }}{% endfor %})),
|
visit(%({% for x in (1..5) %}{{ test }}{% endfor %}))
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_for_range
|
|
||||||
assert_equal(
|
|
||||||
["test"],
|
|
||||||
visit(%({% for x in (1..test) %}{% endfor %})),
|
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_tablerow_in
|
def test_tablerow_in
|
||||||
assert_equal(
|
assert_equal(
|
||||||
["test"],
|
["test"],
|
||||||
visit(%({% tablerow x in test %}{% endtablerow %})),
|
visit(%({% tablerow x in test %}{% endtablerow %}))
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_tablerow_limit
|
def test_tablerow_limit
|
||||||
assert_equal(
|
assert_equal(
|
||||||
["test"],
|
["test"],
|
||||||
visit(%({% tablerow x in (1..5) limit: test %}{% endtablerow %})),
|
visit(%({% tablerow x in (1..5) limit: test %}{% endtablerow %}))
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_tablerow_offset
|
def test_tablerow_offset
|
||||||
assert_equal(
|
assert_equal(
|
||||||
["test"],
|
["test"],
|
||||||
visit(%({% tablerow x in (1..5) offset: test %}{% endtablerow %})),
|
visit(%({% tablerow x in (1..5) offset: test %}{% endtablerow %}))
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_tablerow_body
|
def test_tablerow_body
|
||||||
assert_equal(
|
assert_equal(
|
||||||
["test"],
|
["test"],
|
||||||
visit(%({% tablerow x in (1..5) %}{{ test }}{% endtablerow %})),
|
visit(%({% tablerow x in (1..5) %}{{ test }}{% endtablerow %}))
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_cycle
|
def test_cycle
|
||||||
assert_equal(
|
assert_equal(
|
||||||
["test"],
|
["test"],
|
||||||
visit(%({% cycle test %})),
|
visit(%({% cycle test %}))
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_assign
|
def test_assign
|
||||||
assert_equal(
|
assert_equal(
|
||||||
["test"],
|
["test"],
|
||||||
visit(%({% assign x = test %})),
|
visit(%({% assign x = test %}))
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_capture
|
def test_capture
|
||||||
assert_equal(
|
assert_equal(
|
||||||
["test"],
|
["test"],
|
||||||
visit(%({% capture x %}{{ test }}{% endcapture %})),
|
visit(%({% capture x %}{{ test }}{% endcapture %}))
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_include
|
def test_include
|
||||||
assert_equal(
|
assert_equal(
|
||||||
["test"],
|
["test"],
|
||||||
visit(%({% include test %})),
|
visit(%({% include test %}))
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_include_with
|
def test_include_with
|
||||||
assert_equal(
|
assert_equal(
|
||||||
["test"],
|
["test"],
|
||||||
visit(%({% include "hai" with test %})),
|
visit(%({% include "hai" with test %}))
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_include_for
|
def test_include_for
|
||||||
assert_equal(
|
assert_equal(
|
||||||
["test"],
|
["test"],
|
||||||
visit(%({% include "hai" for test %})),
|
visit(%({% include "hai" for test %}))
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_render_with
|
|
||||||
assert_equal(
|
|
||||||
["test"],
|
|
||||||
visit(%({% render "hai" with test %})),
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_render_for
|
|
||||||
assert_equal(
|
|
||||||
["test"],
|
|
||||||
visit(%({% render "hai" for test %})),
|
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -257,7 +229,7 @@ class ParseTreeVisitorTest < Minitest::Test
|
|||||||
["test", []],
|
["test", []],
|
||||||
["xs", []],
|
["xs", []],
|
||||||
]]],
|
]]],
|
||||||
traversal(%({% for x in xs offset: test %}{{ other }}{% endfor %})).visit,
|
traversal(%({% for x in xs offset: test %}{{ other }}{% endfor %})).visit
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ class ParsingQuirksTest < Minitest::Test
|
|||||||
assert(Template.parse("{{test}}"))
|
assert(Template.parse("{{test}}"))
|
||||||
|
|
||||||
with_error_mode(:lax) do
|
with_error_mode(:lax) do
|
||||||
assert(Template.parse("{{|test}}"))
|
assert Template.parse("{{|test}}")
|
||||||
end
|
end
|
||||||
|
|
||||||
with_error_mode(:strict) do
|
with_error_mode(:strict) do
|
||||||
@@ -118,37 +118,7 @@ class ParsingQuirksTest < Minitest::Test
|
|||||||
end
|
end
|
||||||
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
|
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
|
|
||||||
|
|
||||||
def test_incomplete_expression
|
|
||||||
with_error_mode(:lax) do
|
|
||||||
assert_template_result("false", "{{ false - }}")
|
|
||||||
assert_template_result("false", "{{ false > }}")
|
|
||||||
assert_template_result("false", "{{ false < }}")
|
|
||||||
assert_template_result("false", "{{ false = }}")
|
|
||||||
assert_template_result("false", "{{ false ! }}")
|
|
||||||
assert_template_result("false", "{{ false 1 }}")
|
|
||||||
assert_template_result("false", "{{ false a }}")
|
|
||||||
|
|
||||||
assert_template_result("false", "{% liquid assign foo = false -\n%}{{ foo }}")
|
|
||||||
assert_template_result("false", "{% liquid assign foo = false >\n%}{{ foo }}")
|
|
||||||
assert_template_result("false", "{% liquid assign foo = false <\n%}{{ foo }}")
|
|
||||||
assert_template_result("false", "{% liquid assign foo = false =\n%}{{ foo }}")
|
|
||||||
assert_template_result("false", "{% liquid assign foo = false !\n%}{{ foo }}")
|
|
||||||
assert_template_result("false", "{% liquid assign foo = false 1\n%}{{ foo }}")
|
|
||||||
assert_template_result("false", "{% liquid assign foo = false a\n%}{{ foo }}")
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end # ParsingQuirksTest
|
end # ParsingQuirksTest
|
||||||
|
|||||||
@@ -2,28 +2,7 @@
|
|||||||
|
|
||||||
require 'test_helper'
|
require 'test_helper'
|
||||||
|
|
||||||
class ProfilerTest < Minitest::Test
|
class RenderProfilingTest < Minitest::Test
|
||||||
class TestDrop < Liquid::Drop
|
|
||||||
def initialize(value)
|
|
||||||
super()
|
|
||||||
@value = value
|
|
||||||
end
|
|
||||||
|
|
||||||
def to_s
|
|
||||||
artificial_execution_time
|
|
||||||
|
|
||||||
@value
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
# Monotonic clock precision fluctuate based on the operating system
|
|
||||||
# By introducing a small sleep we ensure ourselves to register a non zero unit of time
|
|
||||||
def artificial_execution_time
|
|
||||||
sleep(Process.clock_getres(Process::CLOCK_MONOTONIC))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
include Liquid
|
include Liquid
|
||||||
|
|
||||||
class ProfilingFileSystem
|
class ProfilingFileSystem
|
||||||
@@ -33,7 +12,7 @@ class ProfilerTest < Minitest::Test
|
|||||||
end
|
end
|
||||||
|
|
||||||
def setup
|
def setup
|
||||||
Liquid::Environment.default.file_system = ProfilingFileSystem.new
|
Liquid::Template.file_system = ProfilingFileSystem.new
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_template_allows_flagging_profiling
|
def test_template_allows_flagging_profiling
|
||||||
@@ -83,17 +62,6 @@ class ProfilerTest < Minitest::Test
|
|||||||
assert_equal(2, included_children[1].line_number)
|
assert_equal(2, included_children[1].line_number)
|
||||||
end
|
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
|
def test_profiling_times_the_rendering_of_tokens
|
||||||
t = Template.parse("{% include 'a_template' %}", profile: true)
|
t = Template.parse("{% include 'a_template' %}", profile: true)
|
||||||
t.render!
|
t.render!
|
||||||
@@ -109,37 +77,6 @@ class ProfilerTest < Minitest::Test
|
|||||||
assert(t.profiler.total_render_time >= 0, "Total render time was not calculated")
|
assert(t.profiler.total_render_time >= 0, "Total render time was not calculated")
|
||||||
end
|
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
|
def test_profiling_uses_include_to_mark_children
|
||||||
t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}", profile: true)
|
t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}", profile: true)
|
||||||
t.render!
|
t.render!
|
||||||
@@ -154,7 +91,7 @@ class ProfilerTest < Minitest::Test
|
|||||||
|
|
||||||
include_node = t.profiler[1]
|
include_node = t.profiler[1]
|
||||||
include_node.children.each do |child|
|
include_node.children.each do |child|
|
||||||
assert_equal("a_template", child.partial)
|
assert_equal "a_template", child.partial
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -164,12 +101,12 @@ class ProfilerTest < Minitest::Test
|
|||||||
|
|
||||||
a_template = t.profiler[1]
|
a_template = t.profiler[1]
|
||||||
a_template.children.each do |child|
|
a_template.children.each do |child|
|
||||||
assert_equal("a_template", child.partial)
|
assert_equal "a_template", child.partial
|
||||||
end
|
end
|
||||||
|
|
||||||
b_template = t.profiler[2]
|
b_template = t.profiler[2]
|
||||||
b_template.children.each do |child|
|
b_template.children.each do |child|
|
||||||
assert_equal("b_template", child.partial)
|
assert_equal "b_template", child.partial
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -179,12 +116,12 @@ class ProfilerTest < Minitest::Test
|
|||||||
|
|
||||||
a_template1 = t.profiler[1]
|
a_template1 = t.profiler[1]
|
||||||
a_template1.children.each do |child|
|
a_template1.children.each do |child|
|
||||||
assert_equal("a_template", child.partial)
|
assert_equal "a_template", child.partial
|
||||||
end
|
end
|
||||||
|
|
||||||
a_template2 = t.profiler[2]
|
a_template2 = t.profiler[2]
|
||||||
a_template2.children.each do |child|
|
a_template2.children.each do |child|
|
||||||
assert_equal("a_template", child.partial)
|
assert_equal "a_template", child.partial
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -219,22 +156,16 @@ class ProfilerTest < Minitest::Test
|
|||||||
|
|
||||||
def test_profiling_supports_self_time
|
def test_profiling_supports_self_time
|
||||||
t = Template.parse("{% for item in collection %} {{ item }} {% endfor %}", profile: true)
|
t = Template.parse("{% for item in collection %} {{ item }} {% endfor %}", profile: true)
|
||||||
collection = [
|
t.render!("collection" => ["one", "two"])
|
||||||
TestDrop.new("one"),
|
|
||||||
TestDrop.new("two"),
|
|
||||||
]
|
|
||||||
output = t.render!("collection" => collection)
|
|
||||||
assert_equal(" one two ", output)
|
|
||||||
|
|
||||||
leaf = t.profiler[0].children[0]
|
leaf = t.profiler[0].children[0]
|
||||||
assert_operator(leaf.self_time, :>, 0.0)
|
|
||||||
|
assert_operator(leaf.self_time, :>, 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_profiling_supports_total_time
|
def test_profiling_supports_total_time
|
||||||
t = Template.parse("{% if true %} {{ test }} {% endif %}", profile: true)
|
t = Template.parse("{% if true %} {% increment test %} {{ test }} {% endif %}", profile: true)
|
||||||
output = t.render!("test" => TestDrop.new("one"))
|
t.render!
|
||||||
assert_equal(" one ", output)
|
|
||||||
|
|
||||||
assert_operator(t.profiler[0].total_time, :>, 0.0)
|
assert_operator(t.profiler[0].total_time, :>, 0)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -43,22 +43,15 @@ class SecurityTest < Minitest::Test
|
|||||||
assert_equal(expected, Template.parse(text).render!(@assigns, filters: SecurityFilter))
|
assert_equal(expected, Template.parse(text).render!(@assigns, filters: SecurityFilter))
|
||||||
end
|
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
|
current_symbols = Symbol.all_symbols
|
||||||
|
|
||||||
# MRI imprecisely marks objects found on the C stack, which can result
|
test = %( {{ "some_string" | a_bad_filter }} )
|
||||||
# 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
|
|
||||||
|
|
||||||
GC.start
|
template = Template.parse(test)
|
||||||
|
assert_equal([], (Symbol.all_symbols - current_symbols))
|
||||||
|
|
||||||
|
template.render!
|
||||||
assert_equal([], (Symbol.all_symbols - current_symbols))
|
assert_equal([], (Symbol.all_symbols - current_symbols))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user