Compare commits

..
Author SHA1 Message Date
Mike Angell 56a1034ac2 Remove forloop guard 2019-10-15 20:37:12 +10:00
Mike Angell 1c3dcb0ddc Add forloop to render for syntax 2019-10-14 17:24:34 +10:00
170 changed files with 4698 additions and 12070 deletions
-6
View File
@@ -1,6 +0,0 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: weekly
+2
View File
@@ -0,0 +1,2 @@
enabled:
- cla
-22
View File
@@ -1,22 +0,0 @@
name: Contributor License Agreement (CLA)
on:
pull_request_target:
types: [opened, synchronize]
issue_comment:
types: [created]
jobs:
cla:
runs-on: ubuntu-latest
if: |
(github.event.issue.pull_request
&& !github.event.issue.pull_request.merged_at
&& contains(github.event.comment.body, 'signed')
)
|| (github.event.pull_request && !github.event.pull_request.merged)
steps:
- uses: Shopify/shopify-cla-action@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
cla-token: ${{ secrets.CLA_TOKEN }}
-73
View File
@@ -1,73 +0,0 @@
name: Liquid
on: [push]
env:
BUNDLE_JOBS: 4
BUNDLE_RETRY: 3
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
entry:
- { ruby: 3.3, allowed-failure: false } # minimum supported
- { ruby: 3.4, allowed-failure: false, rubyopt: "--yjit" }
- { ruby: 4.0, allowed-failure: false } # latest stable
- {
ruby: 4.0,
allowed-failure: false,
rubyopt: "--enable-frozen-string-literal",
}
- { ruby: 4.0, allowed-failure: false, rubyopt: "--yjit" }
- { ruby: 4.0, allowed-failure: false, rubyopt: "--zjit" }
- { ruby: truffleruby, allowed-failure: false }
# Head can have failures due to being in development
- { ruby: head, allowed-failure: true }
- {
ruby: head,
allowed-failure: true,
rubyopt: "--enable-frozen-string-literal",
}
- { ruby: head, allowed-failure: true, rubyopt: "--yjit" }
- { ruby: head, allowed-failure: true, rubyopt: "--zjit" }
name: Test Ruby ${{ matrix.entry.ruby }} ${{ matrix.entry.rubyopt }} --${{ matrix.entry.allowed-failure && 'allowed-failure' || 'strict' }}
steps:
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- uses: ruby/setup-ruby@319994f95fa847cf3fb3cd3dbe89f6dcde9f178f # v1.295.0
with:
ruby-version: ${{ matrix.entry.ruby }}
bundler-cache: true
bundler: latest
- run: bundle exec rake
continue-on-error: ${{ matrix.entry.allowed-failure }}
env:
RUBYOPT: ${{ matrix.entry.rubyopt }}
spec:
runs-on: ubuntu-latest
env:
BUNDLE_WITH: spec
steps:
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- uses: ruby/setup-ruby@319994f95fa847cf3fb3cd3dbe89f6dcde9f178f # v1.295.0
with:
bundler-cache: true
bundler: latest
- name: Run liquid-spec for all adapters
run: |
for adapter in spec/*.rb; do
echo "=== Running $adapter ==="
bundle exec liquid-spec run "$adapter" --no-max-failures
done
memory_profile:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- uses: ruby/setup-ruby@319994f95fa847cf3fb3cd3dbe89f6dcde9f178f # v1.295.0
with:
bundler-cache: true
- run: bundle exec rake memory_profile:run
+2 -1
View File
@@ -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
View File
@@ -1,7 +1,5 @@
inherit_gem:
rubocop-shopify: rubocop.yml
inherit_from: inherit_from:
- 'https://shopify.github.io/ruby-style-guide/rubocop.yml'
- .rubocop_todo.yml - .rubocop_todo.yml
require: rubocop-performance require: rubocop-performance
@@ -10,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 -158
View File
@@ -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' Metrics/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,92 +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, AllowCopDirectives, AllowedPatterns.
# URISchemes: http, https
Layout/LineLength:
Max: 260
Naming/PredicatePrefix:
Enabled: false
# Offense count: 1
# This is intentional - early return from begin/rescue in assignment context
Lint/NoReturnInBeginEndBlocks:
Exclude:
- 'lib/liquid/standardfilters.rb'
-1
View File
@@ -1 +0,0 @@
4.0.2
+26
View File
@@ -0,0 +1,26 @@
language: ruby
cache: bundler
rvm:
- 2.4
- 2.5
- &latest_ruby 2.6
- 2.7
- ruby-head
matrix:
include:
- rvm: *latest_ruby
script: bundle exec rake memory_profile:run
name: Profiling Memory Usage
allow_failures:
- rvm: ruby-head
branches:
only:
- master
- gh-pages
- /.*-stable/
notifications:
disable: true
-11
View File
@@ -5,7 +5,6 @@
* Bugfixes * Bugfixes
* Performance improvements * Performance improvements
* Features that are likely to be useful to the majority of Liquid users * Features that are likely to be useful to the majority of Liquid users
* Documentation updates that are concise and likely to be useful to the majority of Liquid users
## Things we won't merge ## Things we won't merge
@@ -15,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)
+4 -15
View File
@@ -7,32 +7,21 @@ 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'
end end
end end
group :development do
gem "webrick"
end
group :test do group :test do
gem 'benchmark' gem 'rubocop', '~> 0.74.0', require: false
gem 'rubocop', '~> 1.82.0'
gem 'rubocop-shopify', '~> 2.18.0', require: false
gem 'rubocop-performance', require: false gem 'rubocop-performance', require: false
end
group :spec do platform :mri, :truffleruby do
# Includes the range resource-limit specs from https://github.com/Shopify/liquid-spec/pull/165. gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'master'
gem 'liquid-spec', github: 'Shopify/liquid-spec', ref: '84bf25e0edbca5f7e530574788b0f875ab331f50' end
gem 'activesupport', require: false
end end
-191
View File
@@ -1,196 +1,5 @@
# Liquid Change Log # Liquid Change Log
## 5.14.0
* Avoid materializing integer ranges in `for` and `tablerow` loops, and account for each visited range item [Ian Ker-Seymer]
## 5.13.0
* Add TruffleRuby in CI [Benoit Daloze]
* Skip slow test raising many exceptions on non-CRuby [Benoit Daloze]
* Reject bare-bracket syntax in strict2 and introduce `self` keyword by [Alok Swamy]
* Add strict2_parse to assign and capture tags by [Alok Swamy]
* Add strict2_parse to increment and decrement tags by [Alok Swamy]
* Update liquid-spec adapters for `missing_features` [Ian Ker-Seymer]
* Prevent `SelfDrop` context mutation across render boundaries [Guilherme Carreiro]
* Fix `SelfDrop` equality [Guilherme Carreiro]
* Let environment `self` shadow `SelfDrop` [Ian Ker-Seymer]
## 5.11.0
* Revert the Inline Snippets tag (#2001), treat its inclusion in the latest Liquid release as a bug, and allow for feedback on RFC#1916 to better support Liquid developers [Guilherme Carreiro]
* Rename the `:rigid` error mode to `:strict2` and display a warning when users attempt to use the `:rigid` mode [Guilherme Carreiro]
## 5.10.0
* Introduce support for Inline Snippets [Julia Boutin]
## 5.9.0
* Introduce `:rigid` error mode for stricter, safer parsing of all tags [CP Clermont, Guilherme Carreiro]
## 5.8.7
* Expose body content in the `Doc` tag [James Meng]
## 5.8.1
* Fix `{% doc %}` tag to be visitable [Guilherme Carreiro]
## 5.8.0
* Introduce the new `{% doc %}` tag [Guilherme Carreiro]
## 5.7.3
* Raise Liquid::SyntaxError when parsing invalidly encoded strings [Chris AtLee]
## 5.7.2 2025-01-31
* Fix array filters to not support nested properties [Guilherme Carreiro]
## 5.7.1 2025-01-24
* Fix the `find` and `find_index`filters to return `nil` when filtering empty arrays [Guilherme Carreiro]
* Fix the `has` filter to return `false` when filtering empty arrays [Guilherme Carreiro]
## 5.7.0 2025-01-16
### Features
* Add `find`, `find_index`, `has`, and `reject` filters to arrays [Guilherme Carreiro]
* Compatibility with Ruby 3.4 [Ian Ker-Seymer]
## 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]
* Handle `BlockBody#blank?` at parse time (#1287) [Dylan Thacker-Smith]
* Pass the tag markup and tokenizer to `Document#unknown_tag` (#1290) [Dylan Thacker-Smith]
* And several internal changes
### Performance Improvements
* Reduce allocations (#1073, #1091, #1115, #1099, #1117, #1141, #1322, #1341) [Richard Monette, Florian Weingarten, Ashwin Maroli]
* Improve resources limits performance (#1093, #1323) [Florian Weingarten, Dylan Thacker-Smith]
## 4.0.3 / 2019-03-12 ## 4.0.3 / 2019-03-12
### Fixed ### Fixed
+7 -56
View File
@@ -1,11 +1,11 @@
[![Build status](https://github.com/Shopify/liquid/actions/workflows/liquid.yml/badge.svg)](https://github.com/Shopify/liquid/actions/workflows/liquid.yml) [![Build Status](https://api.travis-ci.org/Shopify/liquid.svg?branch=master)](http://travis-ci.org/Shopify/liquid)
[![Inline docs](http://inch-ci.org/github/Shopify/liquid.svg?branch=master)](http://inch-ci.org/github/Shopify/liquid) [![Inline docs](http://inch-ci.org/github/Shopify/liquid.svg?branch=master)](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 different parsers 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 = :strict2 # Raises a SyntaxError when invalid syntax is used in all tags 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 in some tags 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.
@@ -149,13 +107,6 @@ template.render!({ 'x' => 1}, { strict_variables: true })
#=> Liquid::UndefinedVariable: Liquid error: undefined variable y #=> Liquid::UndefinedVariable: Liquid error: undefined variable y
``` ```
### Resource limits
`render_score_limit` and `cumulative_render_score_limit` account for each item visited by
integer-range `for` and `tablerow` loops, including loops with empty bodies. This bounds range
iteration work when a score limit is configured; `render_length_limit` only bounds generated
output and does not by itself limit CPU work for output-free loops.
### Usage tracking ### Usage tracking
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.
+9 -61
View File
@@ -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'
@@ -33,7 +27,7 @@ task :rubocop do
end end
end end
desc('runs test suite with lax, strict, and strict2 parsers') desc('runs test suite with both strict and lax parsers')
task :test do task :test do
ENV['LIQUID_PARSER_MODE'] = 'lax' ENV['LIQUID_PARSER_MODE'] = 'lax'
Rake::Task['base_test'].invoke Rake::Task['base_test'].invoke
@@ -42,22 +36,16 @@ task :test do
Rake::Task['base_test'].reenable Rake::Task['base_test'].reenable
Rake::Task['base_test'].invoke Rake::Task['base_test'].invoke
ENV['LIQUID_PARSER_MODE'] = 'strict2' if RUBY_ENGINE == 'ruby' || RUBY_ENGINE == 'truffleruby'
ENV['LIQUID_C'] = '1'
ENV['LIQUID_PARSER_MODE'] = 'lax'
Rake::Task['base_test'].reenable Rake::Task['base_test'].reenable
Rake::Task['base_test'].invoke Rake::Task['base_test'].invoke
if RUBY_ENGINE == 'ruby' || RUBY_ENGINE == 'truffleruby'
ENV['LIQUID_PARSER_MODE'] = 'lax'
Rake::Task['integration_test'].reenable
Rake::Task['integration_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
ENV['LIQUID_PARSER_MODE'] = 'strict2'
Rake::Task['integration_test'].reenable
Rake::Task['integration_test'].invoke
end end
end end
@@ -79,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
@@ -87,38 +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 strict2 parsing"
task :strict2 do
ruby "./performance/benchmark.rb strict2"
end
desc "Run the liquid benchmark with lax, strict, and strict2 parsing"
task run: [:lax, :strict, :strict2]
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
@@ -148,11 +104,3 @@ end
task :console do task :console do
exec 'irb -I lib -r liquid' exec 'irb -I lib -r liquid'
end end
desc('run liquid-spec suite across all adapters')
task :spec do
Dir['./spec/*.rb'].sort.each do |adapter|
puts "=== Running #{adapter} ==="
sh 'bundle', 'exec', 'liquid-spec', 'run', adapter, '--no-max-failures'
end
end
-46
View File
@@ -1,46 +0,0 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require 'bundler/setup'
require 'liquid'
class VirtualFileSystem
def initialize
snippet_1 = <<~LIQUID
<h1>
{{- greating | default: 'Hello' }}, {{ name | default: 'world' -}}!
</h1>
LIQUID
snippet_2 = <<~LIQUID
{%- for i in (1..5) -%}
> {{ i }}
{%- endfor -%}
LIQUID
@templates = {
'snippet-1' => snippet_1,
'snippet-2' => snippet_2,
}
end
def read_template_file(key)
@templates[key] || raise(Liquid::FileSystemError, "No such template '#{key}'")
end
end
def source
File.read(ARGV[0])
rescue StandardError
'Usage: bin/render example/server/templates/index.liquid'
end
def assigns
{
'date' => Time.now,
}
end
puts Liquid::Template
.parse(source, error_mode: :strict2)
.tap { |t| t.registers[:file_system] = VirtualFileSystem.new }
.render(assigns)
+4 -8
View File
@@ -30,18 +30,14 @@ class Servlet < LiquidServlet
private private
def products_list def products_list
[ [{ 'name' => 'Arbor Draft', 'price' => 39900, 'description' => 'the *arbor draft* is a excellent product' },
{ 'name' => 'Arbor Draft', 'price' => 39900, 'description' => 'the *arbor draft* is a excellent product' },
{ 'name' => 'Arbor Element', 'price' => 40000, 'description' => 'the *arbor element* rocks for freestyling' }, { 'name' => 'Arbor Element', 'price' => 40000, 'description' => 'the *arbor element* rocks for freestyling' },
{ 'name' => 'Arbor Diamond', 'price' => 59900, 'description' => 'the *arbor diamond* is a made up product because im obsessed with arbor and have no creativity' } { 'name' => 'Arbor Diamond', 'price' => 59900, 'description' => 'the *arbor diamond* is a made up product because im obsessed with arbor and have no creativity' }]
]
end end
def more_products_list def more_products_list
[ [{ 'name' => 'Arbor Catalyst', 'price' => 39900, 'description' => 'the *arbor catalyst* is an advanced drop-through for freestyle and flatground performance and versatility' },
{ 'name' => 'Arbor Catalyst', 'price' => 39900, 'description' => 'the *arbor catalyst* is an advanced drop-through for freestyle and flatground performance and versatility' }, { 'name' => 'Arbor Fish', 'price' => 40000, 'description' => 'the *arbor fish* is a compact pin that features an extended wheelbase and time-honored teardrop shape' }]
{ 'name' => 'Arbor Fish', 'price' => 40000, 'description' => 'the *arbor fish* is a compact pin that features an extended wheelbase and time-honored teardrop shape' }
]
end end
def description def description
+17 -23
View File
@@ -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,55 +36,52 @@ 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'
require 'liquid/drop' require 'liquid/drop'
require 'liquid/self_drop'
require 'liquid/tablerowloop_drop' require 'liquid/tablerowloop_drop'
require 'liquid/forloop_drop' require 'liquid/forloop_drop'
require 'liquid/extensions' require 'liquid/extensions'
require 'liquid/errors' require 'liquid/errors'
require 'liquid/interrupts' require 'liquid/interrupts'
require 'liquid/strainer_template' require 'liquid/strainer'
require 'liquid/expression'
require 'liquid/context' require 'liquid/context'
require 'liquid/parser_switching'
require 'liquid/tag' require 'liquid/tag'
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/range_slice'
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/template_factory' require 'liquid/static_registers'
# Load all the tags of the standard library
#
Dir["#{__dir__}/liquid/tags/*.rb"].each { |f| require f }
Dir["#{__dir__}/liquid/registers/*.rb"].each { |f| require f }
+10 -28
View File
@@ -10,10 +10,9 @@ module Liquid
end end
def parse(tokens) def parse(tokens)
@body = new_body @body = BlockBody.new
while parse_body(@body, tokens) while parse_body(@body, tokens)
end end
@body.freeze
end end
# For backwards compatibility # For backwards compatibility
@@ -29,33 +28,20 @@ module Liquid
@body.nodelist @body.nodelist
end end
def unknown_tag(tag_name, _markup, _tokenizer) def unknown_tag(tag, _params, _tokens)
Block.raise_unknown_tag(tag_name, block_name, block_delimiter, parse_context)
end
# @api private
def self.raise_unknown_tag(tag, block_name, block_delimiter, parse_context)
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
@@ -64,14 +50,8 @@ module Liquid
@block_delimiter ||= "end#{block_name}" @block_delimiter ||= "end#{block_name}"
end end
private protected
# @api public
def new_body
parse_context.new_block_body
end
# @api public
def parse_body(body, tokens) def parse_body(body, tokens)
if parse_context.depth >= MAX_DEPTH if parse_context.depth >= MAX_DEPTH
raise StackLevelError, "Nesting too deep" raise StackLevelError, "Nesting too deep"
@@ -82,7 +62,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
+58 -131
View File
@@ -1,12 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
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 +17,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 +26,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 +36,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
@@ -67,65 +51,6 @@ module Liquid
yield nil, nil yield nil, nil
end end
# @api private
def self.unknown_tag_in_liquid_tag(tag, parse_context)
Block.raise_unknown_tag(tag, 'liquid', '%}', parse_context)
end
# @api private
def self.raise_missing_tag_terminator(token, parse_context)
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_termination", token: token, tag_end: TagEnd.inspect)
end
# @api private
def self.raise_missing_variable_terminator(token, parse_context)
raise SyntaxError, parse_context.locale.t("errors.syntax.variable_termination", token: token, tag_end: VariableEnd.inspect)
end
# @api private
def self.render_node(context, output, node)
node.render_to_output_buffer(context, output)
rescue => exc
blank_tag = !node.instance_of?(Variable) && node.blank?
rescue_render_node(context, output, node.line_number, exc, blank_tag)
end
# @api private
def self.rescue_render_node(context, output, line_number, exc, blank_tag)
case exc
when MemoryError
raise
when UndefinedVariable, UndefinedDropMethod, UndefinedFilter
context.handle_error(exc, line_number)
else
error_message = context.handle_error(exc, line_number)
error_mode = context.registers.static[:template_error_mode]
suppress_error_text = blank_tag && error_mode != :strict2 && error_mode != :rigid
unless suppress_error_text # blank-tag suppression is kept for backwards compatibility outside strict2
output << error_message
end
end
end
private def parse_liquid_tag(markup, parse_context)
liquid_tag_tokenizer = parse_context.new_tokenizer(
markup, start_line_number: parse_context.line_number, for_liquid_tag: true
)
parse_for_liquid_tag(liquid_tag_tokenizer, parse_context) do |end_tag_name, _end_tag_markup|
if end_tag_name
BlockBody.unknown_tag_in_liquid_tag(end_tag_name, parse_context)
end
end
end
private def handle_invalid_tag_token(token, parse_context)
if token.end_with?('%}')
yield token, token
else
BlockBody.raise_missing_tag_terminator(token, parse_context)
end
end
private def parse_for_document(tokenizer, parse_context, &block) 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?
@@ -133,7 +58,7 @@ module Liquid
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) 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)
@@ -145,11 +70,11 @@ module Liquid
end end
if tag_name == 'liquid' if tag_name == 'liquid'
parse_liquid_tag(markup, parse_context) liquid_tag_tokenizer = Tokenizer.new(markup, line_number: parse_context.line_number, for_liquid_tag: true)
next next parse_for_liquid_tag(liquid_tag_tokenizer, parse_context, &block)
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
@@ -167,7 +92,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
@@ -179,11 +104,7 @@ module Liquid
if token[2] == WhitespaceControl if token[2] == WhitespaceControl
previous_token = @nodelist.last previous_token = @nodelist.last
if previous_token.is_a?(String) if previous_token.is_a?(String)
first_byte = previous_token.getbyte(0)
previous_token.rstrip! previous_token.rstrip!
if previous_token.empty? && parse_context[:bug_compatible_whitespace_trimming] && first_byte
previous_token << first_byte
end
end end
end end
parse_context.trim_whitespace = (token[-3] == WhitespaceControl) parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
@@ -193,49 +114,38 @@ module Liquid
@blank @blank
end end
# Remove blank strings in the block body for a control flow tag (e.g. `if`, `for`, `case`, `unless`)
# with a blank body.
#
# For example, in a conditional assignment like the following
#
# ```
# {% if size > max_size %}
# {% assign size = max_size %}
# {% endif %}
# ```
#
# we assume the intention wasn't to output the blank spaces in the `if` tag's block body, so this method
# will remove them to reduce the render output size.
#
# Note that it is now preferred to use the `liquid` tag for this use case.
def remove_blank_strings
raise "remove_blank_strings only support being called on a blank block body" unless @blank
@nodelist.reject! { |node| node.instance_of?(String) }
end
def render(context) def render(context)
render_to_output_buffer(context, +'') render_to_output_buffer(context, +'')
end end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
freeze unless frozen? context.resource_limits.render_score += @nodelist.length
context.resource_limits.increment_render_score(@nodelist.length)
idx = 0 idx = 0
while (node = @nodelist[idx]) while (node = @nodelist[idx])
if node.instance_of?(String) previous_output_size = output.bytesize
case node
when String
output << node output << node
else when Variable
render_node(context, output, node) render_node(context, output, node)
when Block
render_node(context, node.blank? ? +'' : output, node)
break if context.interrupt? # might have happened in a for-block
when Continue, Break
# If we get an Interrupt that means the block must stop processing. An # If we get an Interrupt that means the block must stop processing. An
# Interrupt is any command that stops block execution such as {% break %} # Interrupt is any command that stops block execution such as {% break %}
# or {% continue %}. These tags may also occur through Block or Include tags. # or {% continue %}
break if context.interrupt? # might have happened in a for-block context.push_interrupt(node.interrupt)
break
else # Other non-Block tags
render_node(context, output, node)
break if context.interrupt? # might have happened through an include
end end
idx += 1 idx += 1
context.resource_limits.increment_write_score(output) raise_if_resource_limits_reached(context, output.bytesize - previous_output_size)
end end
output output
@@ -244,32 +154,49 @@ module Liquid
private private
def render_node(context, output, node) def render_node(context, output, node)
BlockBody.render_node(context, output, node) if node.disabled?(context)
output << node.disabled_error_message
return
end
disable_tags(context, node.disabled_tags) do
node.render_to_output_buffer(context, output)
end
rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
context.handle_error(e, node.line_number)
rescue ::StandardError => e
line_number = node.is_a?(String) ? nil : node.line_number
output << context.handle_error(e, line_number)
end
def disable_tags(context, tags, &block)
return yield if tags.empty?
context.registers[:disabled_tags].disable(tags, &block)
end
def raise_if_resource_limits_reached(context, length)
context.resource_limits.render_length += length
return unless context.resource_limits.reached?
raise MemoryError, "Memory limits exceeded"
end 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
raise_missing_variable_terminator(token, parse_context)
BlockBody.raise_missing_variable_terminator(token, parse_context)
end end
# @deprecated Use {.raise_missing_tag_terminator} instead
def raise_missing_tag_terminator(token, parse_context) def raise_missing_tag_terminator(token, parse_context)
BlockBody.raise_missing_tag_terminator(token, parse_context) raise SyntaxError, parse_context.locale.t("errors.syntax.tag_termination", token: token, tag_end: TagEnd.inspect)
end end
# @deprecated Use {.raise_missing_variable_terminator} instead
def raise_missing_variable_terminator(token, parse_context) def raise_missing_variable_terminator(token, parse_context)
BlockBody.raise_missing_variable_terminator(token, parse_context) raise SyntaxError, parse_context.locale.t("errors.syntax.variable_termination", token: token, tag_end: VariableEnd.inspect)
end
def registered_tags
Template.tags
end end
end end
end end
+19 -92
View File
@@ -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, safe: false)
@@method_literals[markup] || parse_context.parse_expression(markup, safe: safe)
end
attr_reader :attachment, :child_condition attr_reader :attachment, :child_condition
attr_accessor :left, :operator, :right attr_accessor :left, :operator, :right
@@ -59,12 +38,11 @@ module Liquid
@left = left @left = left
@operator = operator @operator = operator
@right = right @right = right
@child_relation = nil @child_relation = nil
@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 +50,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,76 +90,33 @@ 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)
return call_method_literal(left, right) if right.respond_to?(left.method_name)
return right.send(left.method_name)
else
return nil
end
end end
if right.is_a?(MethodLiteral) if right.is_a?(Liquid::Expression::MethodLiteral)
return call_method_literal(right, left) if left.respond_to?(right.method_name)
return left.send(right.method_name)
else
return nil
end
end end
left == right left == right
end end
def call_method_literal(literal, value)
method_name = literal.method_name
# If the object responds to the method (e.g., ActiveSupport is loaded), use it
if value.respond_to?(method_name)
value.send(method_name)
else
# Emulate ActiveSupport's blank?/empty? to make Liquid invariant
# to whether ActiveSupport is loaded or not
case method_name
when :blank?
liquid_blank?(value)
when :empty?
liquid_empty?(value)
else
false
end
end
end
# Implement blank? semantics matching ActiveSupport
# blank? returns true for nil, false, empty strings, whitespace-only strings,
# empty arrays, and empty hashes
def liquid_blank?(value)
case value
when NilClass, FalseClass
true
when TrueClass, Numeric
false
when String
# Blank if empty or whitespace only (matches ActiveSupport)
value.empty? || value.match?(/\A\s*\z/)
when Array, Hash
value.empty?
else
# Fall back to empty? if available, otherwise false
value.respond_to?(:empty?) ? value.empty? : false
end
end
# Implement empty? semantics
# Note: nil is NOT empty. empty? checks if a collection has zero elements.
def liquid_empty?(value)
case value
when String, Array, Hash
value.empty?
else
value.respond_to?(:empty?) ? value.empty? : false
end
end
def interpret_condition(left, right, op, context) def interpret_condition(left, right, op, context)
# If the operator is empty this means that the decision statement is just # If the operator is empty this means that the decision statement is just
# a single variable. We can just poll this variable from the context and # a single variable. We can just poll this variable from the context and
# 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}")
@@ -196,19 +131,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
-8
View File
@@ -1,8 +0,0 @@
# frozen_string_literal: true
module Liquid
module Const
EMPTY_HASH = {}.freeze
EMPTY_ARRAY = [].freeze
end
end
+25 -77
View File
@@ -15,48 +15,35 @@ 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
squash_instance_assigns_with_environments
self.exception_renderer = Template.default_exception_renderer
if rethrow_errors
self.exception_renderer = ->(_e) { raise }
end
@interrupts = [] @interrupts = []
@filters = [] @filters = []
@global_filter = nil @global_filter = nil
@disabled_tags = {}
# Instead of constructing new StringScanner objects for each Expression parse,
# 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
self.exception_renderer = Liquid::RAISE_EXCEPTION_LAMBDA
end
yield self if block_given?
# Do this last, since it could result in this object being passed to a Proc in the environment
squash_instance_assigns_with_environments
end end
# rubocop:enable Metrics/ParameterLists # rubocop:enable Metrics/ParameterLists
@@ -65,7 +52,7 @@ module Liquid
end end
def strainer def strainer
@strainer ||= @environment.create_strainer(self, @filters) @strainer ||= Strainer.create(self, @filters)
end end
# Adds filters to this context. # Adds filters to this context.
@@ -133,7 +120,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 +133,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
@@ -158,7 +144,6 @@ module Liquid
subcontext.strainer = nil subcontext.strainer = nil
subcontext.errors = errors subcontext.errors = errors
subcontext.warnings = warnings subcontext.warnings = warnings
subcontext.disabled_tags = @disabled_tags
end end
end end
@@ -180,20 +165,11 @@ 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)
find_variable(key, raise_on_not_found: false) != nil self[key] != nil
end
# Checks whether a variable is defined in any scope, including nil-valued keys.
# Unlike #key?, this uses Hash#key? so that variables explicitly set to nil
# are still considered defined.
def variable_defined?(key)
@scopes.any? { |s| s.key?(key) } ||
@environments.any? { |e| e.key?(key) } ||
@static_environments.any? { |e| e.key?(key) }
end end
def evaluate(object) def evaluate(object)
@@ -206,29 +182,16 @@ module Liquid
# path and find_index() is optimized in MRI to reduce object allocation # path and find_index() is optimized in MRI to reduce object allocation
index = @scopes.find_index { |s| s.key?(key) } index = @scopes.find_index { |s| s.key?(key) }
fallback_to_self_drop = key == Expression::SELF && index.nil?
variable = if index variable = if index
lookup_and_evaluate(@scopes[index], key, raise_on_not_found: raise_on_not_found) lookup_and_evaluate(@scopes[index], key, raise_on_not_found: raise_on_not_found)
else else
try_variable_find_in_environments( try_variable_find_in_environments(key, raise_on_not_found: raise_on_not_found)
key,
raise_on_not_found: raise_on_not_found && !fallback_to_self_drop,
)
end end
# `self` resolves to a SelfDrop (enabling `self['var']` lookups), variable = variable.to_liquid
# but only after the normal environment lookup doesn't find a value.
return @self_drop ||= SelfDrop.new(self) if fallback_to_self_drop && variable.nil?
# update variable's context before invoking #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)
@@ -245,24 +208,9 @@ module Liquid
end end
end end
def with_disabled_tags(tag_names)
tag_names.each do |name|
@disabled_tags[name] = @disabled_tags.fetch(name, 0) + 1
end
yield
ensure
tag_names.each do |name|
@disabled_tags[name] -= 1
end
end
def tag_disabled?(tag_name)
@disabled_tags.fetch(tag_name, 0) > 0
end
protected protected
attr_writer :base_scope_depth, :warnings, :errors, :strainer, :filters, :disabled_tags attr_writer :base_scope_depth, :warnings, :errors, :strainer, :filters
private private
-22
View File
@@ -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
+6 -42
View File
@@ -1,34 +1,23 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
class Document class Document < BlockBody
def self.parse(tokens, parse_context) def self.parse(tokens, parse_context)
doc = new(parse_context) doc = new
doc.parse(tokens, parse_context) doc.parse(tokens, parse_context)
doc doc
end end
attr_reader :parse_context, :body def parse(tokens, parse_context)
super do |end_tag_name, _end_tag_params|
def initialize(parse_context) unknown_tag(end_tag_name, parse_context) if end_tag_name
@parse_context = parse_context
@body = new_body
end end
def nodelist
@body.nodelist
end
def parse(tokenizer, parse_context)
while parse_body(tokenizer)
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
end end
def unknown_tag(tag, _markup, _tokenizer) def unknown_tag(tag, parse_context)
case tag case tag
when 'else', 'end' when 'else', 'end'
raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_outer_tag", tag: tag) raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_outer_tag", tag: tag)
@@ -36,30 +25,5 @@ module Liquid
raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag", tag: tag) raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag", tag: tag)
end end
end end
def render_to_output_buffer(context, output)
@body.render_to_output_buffer(context, output)
end
def render(context)
render_to_output_buffer(context, +'')
end
private
def new_body
parse_context.new_block_body
end
def parse_body(tokenizer)
@body.parse(tokenizer, parse_context) do |unknown_tag_name, unknown_tag_markup|
if unknown_tag_name
unknown_tag(unknown_tag_name, unknown_tag_markup, tokenizer)
true
else
false
end
end
end
end end
end end
+1 -5
View File
@@ -25,13 +25,9 @@ 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 unless @context&.strict_variables return nil unless @context&.strict_variables
raise Liquid::UndefinedDropMethod, "undefined method #{method}" raise Liquid::UndefinedDropMethod, "undefined method #{method}"
end end
-159
View File
@@ -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 :strict2, :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
+1 -2
View File
@@ -46,6 +46,7 @@ module Liquid
StandardError = Class.new(Error) StandardError = Class.new(Error)
SyntaxError = Class.new(Error) SyntaxError = Class.new(Error)
StackLevelError = Class.new(Error) StackLevelError = Class.new(Error)
TaintedError = 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)
@@ -53,7 +54,5 @@ module Liquid
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)
InternalError = Class.new(Error) InternalError = Class.new(Error)
TemplateEncodingError = Class.new(Error)
end end
+32 -111
View File
@@ -2,127 +2,48 @@
module Liquid module Liquid
class Expression class Expression
SELF = 'self' 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/
FLOAT_REGEX = /\A(-?\d+)\.\d+\z/
class << self
def safe_parse(parser, ss = StringScanner.new(""), cache = nil)
parse(parser.expression, ss, cache)
end
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 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 else
# we found another dot, so we know that the number ends here VariableLookup.parse(markup)
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
# number ends with a dot "123."
markup.byteslice(0, first_dot_pos).to_f
end end
end end
end end
+1 -46
View File
@@ -1,11 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs
# @liquid_type object
# @liquid_name forloop
# @liquid_summary
# Information about a parent [`for` loop](/docs/api/liquid/tags/for).
class ForloopDrop < Drop class ForloopDrop < Drop
def initialize(name, length, parentloop) def initialize(name, length, parentloop)
@name = name @name = name
@@ -14,68 +9,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
+1 -1
View File
@@ -28,7 +28,7 @@ module Liquid
def interpolate(name, vars) def interpolate(name, vars)
name.gsub(/%\{(\w+)\}/) do name.gsub(/%\{(\w+)\}/) do
# raise TranslationError, "Undefined key #{$1} for interpolation in translation #{name}" unless vars[$1.to_sym] # raise TranslationError, "Undefined key #{$1} for interpolation in translation #{name}" unless vars[$1.to_sym]
vars[Regexp.last_match(1).to_sym].to_s (vars[Regexp.last_match(1).to_sym]).to_s
end end
end end
+42 -159
View File
@@ -1,179 +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
table[c.upcase.ord] = [:id, IDENTIFIER].freeze
end
table["_".ord] = [:id, IDENTIFIER].freeze
"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 else
output << special c = @ss.getch
end if (s = SPECIALS[c])
[s, c]
else else
output << special raise SyntaxError, "Unexpected character #{c}"
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
type, pattern = NEXT_MATCHER_JUMP_TABLE[peeked]
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 @output << tok
# rubocop:enable Metrics/BlockNesting
output << EOS
rescue ::ArgumentError => e
if e.message == "invalid byte sequence in #{ss.string.encoding}"
raise SyntaxError, "Invalid byte sequence in #{ss.string.encoding}"
else
raise
end
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
+6 -10
View File
@@ -2,31 +2,27 @@
errors: errors:
syntax: syntax:
tag_unexpected_args: "Syntax Error in '%{tag}' - Valid syntax: %{tag}" tag_unexpected_args: "Syntax Error in '%{tag}' - Valid syntax: %{tag}"
block_tag_unexpected_args: "Syntax Error in '%{tag}' - Valid syntax: {% %{tag} %}{% end%{tag} %}"
assign: "Syntax Error in 'assign' - Valid syntax: assign [var] = [source]" assign: "Syntax Error in 'assign' - Valid syntax: assign [var] = [source]"
capture: "Syntax Error in 'capture' - Valid syntax: capture [var]" capture: "Syntax Error in 'capture' - Valid syntax: capture [var]"
case: "Syntax Error in 'case' - Valid syntax: case [condition]" case: "Syntax Error in 'case' - Valid syntax: case [condition]"
case_invalid_when: "Syntax Error in tag 'case' - Valid when condition: {% when [condition] [or condition2...] %}" case_invalid_when: "Syntax Error in tag 'case' - Valid when condition: {% when [condition] [or condition2...] %}"
case_invalid_else: "Syntax Error in tag 'case' - Valid else condition: {% else %} (no parameters) " case_invalid_else: "Syntax Error in tag 'case' - Valid else condition: {% else %} (no parameters) "
cycle: "Syntax Error in 'cycle' - Valid syntax: cycle [name :] var [, var2, var3 ...]" cycle: "Syntax Error in 'cycle' - Valid syntax: cycle [name :] var [, var2, var3 ...]"
doc_invalid_nested: "Syntax Error in 'doc' - Nested doc tags are not allowed"
for: "Syntax Error in 'for loop' - Valid syntax: for [item] in [collection]" for: "Syntax Error in 'for loop' - Valid syntax: for [item] in [collection]"
for_invalid_in: "For loops require an 'in' clause" for_invalid_in: "For loops require an 'in' clause"
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"
table_row_invalid_attribute: "Invalid attribute '%{attribute}' in tablerow loop. Valid attributes are cols, limit, offset, and range"
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 -56
View File
@@ -3,27 +3,12 @@
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,48 +17,10 @@ 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, reject_bare_brackets: @error_mode == :strict2 || @error_mode == :rigid)
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 safe_parse_expression(parser)
Expression.safe_parse(parser, @string_scanner, @expression_cache)
end
def parse_expression(markup, safe: false)
if !safe && @error_mode == :strict2
# parse_expression is a widely used API. To maintain backward
# compatibility while raising awareness about strict2 parser standards,
# the safe flag supports API users make a deliberate decision.
#
# In strict2 mode, markup MUST come from a string returned by the parser
# (e.g., parser.expression). We're not calling the parser here to
# prevent redundant parser overhead.
raise Liquid::InternalError, "unsafe parse_expression cannot be used in strict2 mode"
end
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] || Template.error_mode
@error_mode = @options[:error_mode] || @environment.error_mode
end end
def partial_options def partial_options
+1 -1
View File
@@ -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
+16 -27
View File
@@ -2,11 +2,10 @@
module Liquid module Liquid
class Parser class Parser
def initialize(input, reject_bare_brackets: false) 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
@reject_bare_brackets = reject_bare_brackets
end end
def jump(point) def jump(point)
@@ -47,23 +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
if @reject_bare_brackets
raise SyntaxError, "Bare bracket access is not allowed. Use #{Expression::SELF}['...'] instead"
end
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)
@@ -86,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)
elsif look(:dot)
str << consume
str << consume(:id)
else
break
end end
if look(:dot)
str << consume
str << variable_signature
end end
str str
end end
+2 -47
View File
@@ -2,67 +2,22 @@
module Liquid module Liquid
module ParserSwitching module ParserSwitching
# Do not use this.
#
# It's basically doing the same thing the {#parse_with_selected_parser},
# except this will try the strict parser regardless of the error mode,
# and fall back to the lax parser if the error mode is lax or warn,
# except when in strict2 mode where it uses the strict2 parser.
#
# @deprecated Use {#parse_with_selected_parser} instead.
def strict_parse_with_error_mode_fallback(markup)
return strict2_parse_with_error_context(markup) if strict2_mode?
strict_parse_with_error_context(markup)
rescue SyntaxError => e
case parse_context.error_mode
when :rigid
rigid_warn
raise
when :strict2
raise
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 :rigid then rigid_warn && strict2_parse_with_error_context(markup)
when :strict2 then strict2_parse_with_error_context(markup)
when :strict then strict_parse_with_error_context(markup) when :strict then strict_parse_with_error_context(markup)
when :lax then lax_parse(markup) when :lax then lax_parse(markup)
when :warn when :warn
begin begin
strict2_parse_with_error_context(markup) return strict_parse_with_error_context(markup)
rescue SyntaxError => e rescue SyntaxError => e
parse_context.warnings << e parse_context.warnings << e
lax_parse(markup) return lax_parse(markup)
end end
end end
end end
def strict2_mode?
parse_context.error_mode == :strict2 || parse_context.error_mode == :rigid
end
private private
def rigid_warn
Deprecations.warn(':rigid', ':strict2')
end
def strict2_parse_with_error_context(markup)
strict2_parse(markup)
rescue SyntaxError => e
e.line_number = line_number
e.markup_context = markup_context(markup)
raise e
end
def strict_parse_with_error_context(markup) def strict_parse_with_error_context(markup)
strict_parse(markup) strict_parse(markup)
rescue SyntaxError => e rescue SyntaxError => e
+5 -18
View File
@@ -3,29 +3,16 @@
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] partial = Liquid::Template.parse(source, parse_context)
template = template_factory.for(template_name) cached_partials[template_name] = partial
begin
partial = template.parse(source, parse_context)
rescue Liquid::Error => e
e.template_name = template&.name || template_name
raise e
end
partial.name ||= template_name
cached_partials[cache_key] = partial
ensure ensure
parse_context.partial = false parse_context.partial = false
end end
+92 -60
View File
@@ -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 end
attr_reader :total_time def render_time
alias_method :total_render_time, :total_time @end_time - @start_time
def initialize
@root_children = []
@current_children = nil
@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
end end
def children def self.profile_node_render(node)
children = @root_children if Profiler.current_profile && node.respond_to?(:render)
if children.length == 1 Profiler.current_profile.start_node(node)
children.first.children output = yield
Profiler.current_profile.end_node(node)
output
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
+14 -24
View File
@@ -1,35 +1,25 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
module BlockBodyProfilingHook class BlockBody
def render_node(context, output, node) def render_node_with_profiling(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 render_node_without_profiling(context, output, node)
super
end
else
super
end end
end end
end
BlockBody.prepend(BlockBodyProfilingHook)
module DocumentProfilingHook alias_method :render_node_without_profiling, :render_node
def render_to_output_buffer(context, output) alias_method :render_node, :render_node_with_profiling
return super unless context.profiler
context.profiler.profile(context.template_name) { super }
end end
end
Document.prepend(DocumentProfilingHook)
module ContextProfilingHook class Include < Tag
attr_accessor :profiler def render_to_output_buffer_with_profiling(context, output)
Profiler.profile_children(context.evaluate(@template_name_expr).to_s) do
render_to_output_buffer_without_profiling(context, output)
end
end
def new_isolated_subcontext alias_method :render_to_output_buffer_without_profiling, :render_to_output_buffer
new_context = super alias_method :render_to_output_buffer, :render_to_output_buffer_with_profiling
new_context.profiler = profiler
new_context
end end
end end
Context.prepend(ContextProfilingHook)
end
+3 -21
View File
@@ -2,27 +2,15 @@
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
@@ -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
-44
View File
@@ -1,44 +0,0 @@
# frozen_string_literal: true
module Liquid
class RangeSlice
attr_reader :length
def initialize(range, from, to, resource_limits)
range_length = range.end - range.begin
range_length += 1 unless range.exclude_end?
range_length = 0 if range_length.negative?
start = [from, 0].max
finish = [to || range_length, range_length].min
@first = range.begin + start
@length = [finish - start, 0].max
@direction = 1
@resource_limits = resource_limits
end
def empty?
@length.zero?
end
def each
return enum_for(:each) unless block_given?
value = @first
@length.times do
@resource_limits.increment_render_score(1)
yield value
value += @direction
end
end
def reverse!
unless empty?
@first += @direction * (@length - 1)
@direction = -@direction
end
self
end
end
end
+6
View File
@@ -0,0 +1,6 @@
# frozen_string_literal: true
module Liquid
class Register
end
end
-51
View File
@@ -1,51 +0,0 @@
# frozen_string_literal: true
module Liquid
class Registers
attr_reader :static
def initialize(registers = {})
@static = registers.is_a?(Registers) ? registers.static : registers
@changes = {}
end
def []=(key, value)
@changes[key] = value
end
def [](key)
if @changes.key?(key)
@changes[key]
else
@static[key]
end
end
def delete(key)
@changes.delete(key)
end
UNDEFINED = Object.new
def fetch(key, default = UNDEFINED, &block)
if @changes.key?(key)
@changes.fetch(key)
elsif default != UNDEFINED
if block_given?
@static.fetch(key, &block)
else
@static.fetch(key, default)
end
else
@static.fetch(key, &block)
end
end
def key?(key)
@changes.key?(key) || @static.key?(key)
end
end
# Alias for backwards compatibility
StaticRegisters = Registers
end
+32
View File
@@ -0,0 +1,32 @@
# frozen_string_literal: true
module Liquid
class DisabledTags < Register
def initialize
@disabled_tags = {}
end
def disabled?(tag)
@disabled_tags.key?(tag) && @disabled_tags[tag] > 0
end
def disable(tags)
tags.each(&method(:increment))
yield
ensure
tags.each(&method(:decrement))
end
private
def increment(tag)
@disabled_tags[tag] ||= 0
@disabled_tags[tag] += 1
end
def decrement(tag)
@disabled_tags[tag] -= 1
end
end
Template.add_register(:disabled_tags, DisabledTags.new)
end
+6 -60
View File
@@ -2,78 +2,24 @@
module Liquid module Liquid
class ResourceLimits class ResourceLimits
attr_accessor :render_length_limit, attr_accessor :render_length, :render_score, :assign_score,
:render_score_limit, :render_length_limit, :render_score_limit, :assign_score_limit
:assign_score_limit,
:cumulative_render_score_limit,
:cumulative_assign_score_limit
attr_reader :render_score,
:assign_score,
:cumulative_render_score,
:cumulative_assign_score
def initialize(limits) def initialize(limits)
@render_length_limit = limits[:render_length_limit] @render_length_limit = limits[:render_length_limit]
@render_score_limit = limits[:render_score_limit] @render_score_limit = limits[:render_score_limit]
@assign_score_limit = limits[:assign_score_limit] @assign_score_limit = limits[:assign_score_limit]
@cumulative_render_score_limit = limits[:cumulative_render_score_limit]
@cumulative_assign_score_limit = limits[:cumulative_assign_score_limit]
@cumulative_render_score = 0
@cumulative_assign_score = 0
reset reset
end end
def increment_render_score(amount)
@render_score += amount
@cumulative_render_score += amount
raise_limits_reached if @render_score_limit && @render_score > @render_score_limit
raise_limits_reached if @cumulative_render_score_limit && @cumulative_render_score > @cumulative_render_score_limit
end
def increment_assign_score(amount)
@assign_score += amount
@cumulative_assign_score += amount
raise_limits_reached if @assign_score_limit && @assign_score > @assign_score_limit
raise_limits_reached if @cumulative_assign_score_limit && @cumulative_assign_score > @cumulative_assign_score_limit
end
# update either render_length or assign_score based on whether or not the writes are captured
def increment_write_score(output)
if (last_captured = @last_capture_length)
captured = output.bytesize
increment = captured - last_captured
@last_capture_length = captured
increment_assign_score(increment)
elsif @render_length_limit && output.bytesize > @render_length_limit
raise_limits_reached
end
end
def raise_limits_reached
@reached_limit = true
raise MemoryError, "Memory limits exceeded"
end
def reached? def reached?
@reached_limit (@render_length_limit && @render_length > @render_length_limit) ||
(@render_score_limit && @render_score > @render_score_limit) ||
(@assign_score_limit && @assign_score > @assign_score_limit)
end end
def reset def reset
@reached_limit = false @render_length = @render_score = @assign_score = 0
@last_capture_length = nil
@render_score = @assign_score = 0
raise_limits_reached if @cumulative_render_score_limit && @cumulative_render_score > @cumulative_render_score_limit
raise_limits_reached if @cumulative_assign_score_limit && @cumulative_assign_score > @cumulative_assign_score_limit
end
def with_capture
old_capture_length = @last_capture_length
begin
@last_capture_length = 0
yield
ensure
@last_capture_length = old_capture_length
end
end end
end end
end end
-54
View File
@@ -1,54 +0,0 @@
# frozen_string_literal: true
module Liquid
# @liquid_public_docs
# @liquid_type object
# @liquid_name self
# @liquid_summary
# Provides access to variables through the current scope chain.
# @liquid_description
# The `self` object resolves variables through the normal lookup hierarchy
# (local > file > global) without exposing filters, interrupts, errors,
# or other context internals. It's used when bare bracket notation
# (`['variable']`) needs to be replaced with an explicit variable lookup.
#
# If `self` is explicitly assigned as a local variable (e.g. `{% assign self = 'value' %}`),
# then the local value takes precedence over the `self` object.
# @liquid_access global
class SelfDrop < Drop
def initialize(self_context)
super()
@self_context = self_context
end
def [](key)
@self_context.find_variable(key)
rescue UndefinedVariable
nil
end
def key?(key)
@self_context.variable_defined?(key)
end
def to_liquid
self
end
def ==(other)
other.is_a?(SelfDrop) && other.self_context.equal?(@self_context)
end
alias_method :eql?, :==
def hash
@self_context.object_id.hash
end
protected
attr_reader :self_context
undef context=
end
end
File diff suppressed because it is too large Load Diff
+36
View File
@@ -0,0 +1,36 @@
# frozen_string_literal: true
module Liquid
class StaticRegisters
attr_reader :static, :registers
def initialize(registers = {})
@static = registers.is_a?(StaticRegisters) ? registers.static : registers
@registers = {}
end
def []=(key, value)
@registers[key] = value
end
def [](key)
if @registers.key?(key)
@registers[key]
else
@static[key]
end
end
def delete(key)
@registers.delete(key)
end
def fetch(key, default = nil)
key?(key) ? self[key] : default
end
def key?(key)
@registers.key?(key) || @static.key?(key)
end
end
end
+68
View File
@@ -0,0 +1,68 @@
# frozen_string_literal: true
require 'set'
module Liquid
# Strainer is the parent class for the filters system.
# New filters are mixed into the strainer class which is then instantiated for each liquid template render run.
#
# The Strainer only allows method calls defined in filters given to it via Strainer.global_filter,
# Context#add_filters or Template.register_filter
class Strainer #:nodoc:
@@global_strainer = Class.new(Strainer) do
@filter_methods = Set.new
end
@@strainer_class_cache = Hash.new do |hash, filters|
hash[filters] = Class.new(@@global_strainer) do
@filter_methods = @@global_strainer.filter_methods.dup
filters.each { |f| add_filter(f) }
end
end
def initialize(context)
@context = context
end
class << self
attr_reader :filter_methods
end
def self.add_filter(filter)
raise ArgumentError, "Expected module but got: #{filter.class}" unless filter.is_a?(Module)
unless include?(filter)
invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) }
if invokable_non_public_methods.any?
raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}"
else
send(:include, filter)
@filter_methods.merge(filter.public_instance_methods.map(&:to_s))
end
end
end
def self.global_filter(filter)
@@strainer_class_cache.clear
@@global_strainer.add_filter(filter)
end
def self.invokable?(method)
@filter_methods.include?(method.to_s)
end
def self.create(context, filters = [])
@@strainer_class_cache[filters].new(context)
end
def invoke(method, *args)
if self.class.invokable?(method)
send(method, *args)
elsif @context&.strict_filters
raise Liquid::UndefinedFilter, "undefined filter #{method}"
else
args.first
end
rescue ::ArgumentError => e
raise Liquid::ArgumentError, e.message, e.backtrace
end
end
end
-62
View File
@@ -1,62 +0,0 @@
# frozen_string_literal: true
require 'set'
module Liquid
# StrainerTemplate is the computed class for the filters system.
# New filters are mixed into the strainer class which is then instantiated for each liquid template render run.
#
# The Strainer only allows method calls defined in filters given to it via StrainerFactory.add_global_filter,
# Context#add_filters or Template.register_filter
class StrainerTemplate
def initialize(context)
@context = context
end
class << self
def add_filter(filter)
return if include?(filter)
invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) }
if invokable_non_public_methods.any?
raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}"
end
include(filter)
filter_methods.merge(filter.public_instance_methods.map(&:to_s))
end
def invokable?(method)
filter_methods.include?(method.to_s)
end
def inherited(subclass)
super
subclass.instance_variable_set(:@filter_methods, @filter_methods.dup)
end
def filter_method_names
filter_methods.map(&:to_s).to_a
end
private
def filter_methods
@filter_methods ||= Set.new
end
end
def invoke(method, *args)
if self.class.invokable?(method)
send(method, *args)
elsif @context.strict_filters
raise Liquid::UndefinedFilter, "undefined filter #{method}"
else
args.first
end
rescue ::ArgumentError => e
raise Liquid::ArgumentError, e.message, e.backtrace
end
end
end
+1 -58
View File
@@ -1,11 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs
# @liquid_type object
# @liquid_name tablerowloop
# @liquid_summary
# Information about a parent [`tablerow` loop](/docs/api/liquid/tags/tablerow).
class TablerowloopDrop < Drop class TablerowloopDrop < Drop
def initialize(length, cols) def initialize(length, cols)
@length = length @length = length
@@ -15,92 +10,40 @@ module Liquid
@index = 0 @index = 0
end end
# @liquid_public_docs attr_reader :length, :col, :row
# @liquid_summary
# The total number of iterations in the loop.
# @liquid_return [number]
attr_reader :length
# @liquid_public_docs
# @liquid_summary
# The 1-based index of the current column.
# @liquid_return [number]
attr_reader :col
# @liquid_public_docs
# @liquid_summary
# The 1-based index of current row.
# @liquid_return [number]
attr_reader :row
# @liquid_public_docs
# @liquid_summary
# The 1-based index of the current iteration.
# @liquid_return [number]
def index def index
@index + 1 @index + 1
end end
# @liquid_public_docs
# @liquid_summary
# The 0-based index of the current iteration.
# @liquid_return [number]
def index0 def index0
@index @index
end end
# @liquid_public_docs
# @liquid_summary
# The 0-based index of the current column.
# @liquid_return [number]
def col0 def col0
@col - 1 @col - 1
end end
# @liquid_public_docs
# @liquid_summary
# The 1-based index of the current iteration, in reverse order.
# @liquid_return [number]
def rindex def rindex
@length - @index @length - @index
end end
# @liquid_public_docs
# @liquid_summary
# The 0-based index of the current iteration, in reverse order.
# @liquid_return [number]
def rindex0 def rindex0
@length - @index - 1 @length - @index - 1
end end
# @liquid_public_docs
# @liquid_summary
# Returns `true` if the current iteration is the first. Returns `false` if not.
# @liquid_return [boolean]
def first def first
@index == 0 @index == 0
end end
# @liquid_public_docs
# @liquid_summary
# Returns `true` if the current iteration is the last. Returns `false` if not.
# @liquid_return [boolean]
def last def last
@index == @length - 1 @index == @length - 1
end end
# @liquid_public_docs
# @liquid_summary
# Returns `true` if the current column is the first in the row. Returns `false` if not.
# @liquid_return [boolean]
def col_first def col_first
@col == 1 @col == 1
end end
# @liquid_public_docs
# @liquid_summary
# Returns `true` if the current column is the last in the row. Returns `false` if not.
# @liquid_return [boolean]
def col_last def col_last
@col == @cols @col == @cols
end end
+14 -20
View File
@@ -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
@@ -16,18 +13,14 @@ module Liquid
tag tag
end end
def disable_tags(*tag_names) def disable_tags(*tags)
tag_names += disabled_tags disabled_tags.push(*tags)
define_singleton_method(:disabled_tags) { tag_names }
prepend(Disabler)
end end
private :new private :new
protected
def disabled_tags def disabled_tags
[] @disabled_tags ||= []
end end
end end
@@ -53,12 +46,19 @@ module Liquid
'' ''
end end
def disabled?(context)
context.registers[:disabled_tags].disabled?(tag_name)
end
def disabled_error_message
"#{tag_name} #{options[:locale].t('errors.disabled.tag')}"
end
# For backwards compatibility with custom tags. In a future release, the semantics # For backwards compatibility with custom tags. In a future release, the semantics
# 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
@@ -66,14 +66,8 @@ module Liquid
false false
end end
private def disabled_tags
self.class.disabled_tags
def safe_parse_expression(parser)
parse_context.safe_parse_expression(parser)
end
def parse_expression(markup, safe: false)
parse_context.parse_expression(markup, safe: safe)
end end
end end
end end
-22
View File
@@ -1,22 +0,0 @@
# frozen_string_literal: true
module Liquid
class Tag
module Disableable
def render_to_output_buffer(context, output)
if context.tag_disabled?(tag_name)
output << disabled_error(context)
return
end
super
end
def disabled_error(context)
# raise then rescue the exception so that the Context#exception_renderer can re-raise it
raise DisabledError, "#{tag_name} #{parse_context[:locale].t('errors.disabled.tag')}"
rescue DisabledError => exc
context.handle_error(exc, line_number)
end
end
end
end
-13
View File
@@ -1,13 +0,0 @@
# frozen_string_literal: true
module Liquid
class Tag
module Disabler
def render_to_output_buffer(context, output)
context.with_disabled_tags(self.class.disabled_tags) do
super
end
end
end
end
end
-49
View File
@@ -1,49 +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"
require_relative "tags/doc"
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,
'doc' => Doc,
}.freeze
end
end
+16 -55
View File
@@ -1,71 +1,37 @@
# 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.
# #
# > Caution:
# > Predefined Liquid objects can be overridden by variables with the same name.
# > To make sure that you can access all Liquid objects, make sure that your variable name doesn't match a predefined object's name.
# @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
include ParserSwitching
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
# @api private def self.syntax_error_translation_key
def self.raise_syntax_error(parse_context) "errors.syntax.assign"
raise Liquid::SyntaxError, parse_context.locale.t('errors.syntax.assign')
end end
attr_reader :to, :from attr_reader :to, :from
def initialize(tag_name, markup, parse_context) def initialize(tag_name, markup, options)
super super
parse_with_selected_parser(markup)
end
def lax_parse(markup)
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(self.class.syntax_error_translation_key)
end end
end end
def strict_parse(markup)
lax_parse(markup)
end
def strict2_parse(markup)
unless markup =~ Syntax
self.class.raise_syntax_error(parse_context)
end
lhs = Regexp.last_match(1).strip
rhs = Regexp.last_match(2)
p = @parse_context.new_parser(lhs)
@to = p.consume(:id)
p.consume(:end_of_string)
@from = Variable.new(rhs, parse_context)
end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
val = @from.render(context) val = @from.render(context)
context.scopes.last[@to] = val context.scopes.last[@to] = val
context.resource_limits.increment_assign_score(assign_score_of(val)) context.resource_limits.assign_score += assign_score_of(val)
output output
end end
@@ -78,18 +44,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
@@ -101,4 +60,6 @@ module Liquid
end end
end end
end end
Template.register_tag('assign', Assign)
end end
+5 -14
View File
@@ -10,20 +10,11 @@ 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 def interrupt
BreakInterrupt.new
end
end
def render_to_output_buffer(context, output) Template.register_tag('break', Break)
context.push_interrupt(INTERRUPT)
output
end
end
end end
+15 -38
View File
@@ -1,37 +1,22 @@
# 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
# @liquid_name capture
# @liquid_summary
# Creates a new variable with a string value.
# @liquid_description
# You can create complex strings with Liquid logic and variables.
# #
# > Caution: # {% capture heading %}
# > Predefined Liquid objects can be overridden by variables with the same name. # Monkeys!
# > To make sure that you can access all Liquid objects, make sure that your variable name doesn't match a predefined object's name.
# @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
include ParserSwitching
Syntax = /(#{VariableSignature}+)/o Syntax = /(#{VariableSignature}+)/o
attr_reader :to
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
parse_with_selected_parser(markup)
end
def lax_parse(markup)
if markup =~ Syntax if markup =~ Syntax
@to = Regexp.last_match(1) @to = Regexp.last_match(1)
else else
@@ -39,21 +24,11 @@ module Liquid
end end
end end
def strict_parse(markup)
lax_parse(markup)
end
def strict2_parse(markup)
p = @parse_context.new_parser(markup.strip)
@to = p.consume(:id)
p.consume(:end_of_string)
end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
context.resource_limits.with_capture do previous_output_size = output.bytesize
capture_output = render(context) super
context.scopes.last[@to] = capture_output context.scopes.last[@to] = output
end context.resource_limits.assign_score += (output.bytesize - previous_output_size)
output output
end end
@@ -61,4 +36,6 @@ module Liquid
true true
end end
end end
Template.register_tag('capture', Capture)
end end
+13 -83
View File
@@ -1,27 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category conditional
# @liquid_name case
# @liquid_summary
# Renders a specific expression depending on the value of a specific variable.
# @liquid_syntax
# {% case variable %}
# {% when first_value %}
# first_expression
# {% when second_value %}
# second_expression
# {% else %}
# third_expression
# {% endcase %}
# @liquid_syntax_keyword variable The name of the variable you want to base your case statement on.
# @liquid_syntax_keyword first_value A specific value to check for.
# @liquid_syntax_keyword second_value A specific value to check for.
# @liquid_syntax_keyword first_expression An expression to be rendered when the variable's value matches `first_value`.
# @liquid_syntax_keyword second_expression An expression to be rendered when the variable's value matches `second_value`.
# @liquid_syntax_keyword third_expression An expression to be rendered when the variable's value has no match.
class Case < Block class Case < Block
Syntax = /(#{QuotedFragment})/o Syntax = /(#{QuotedFragment})/o
WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om
@@ -31,20 +10,17 @@ module Liquid
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
@blocks = [] @blocks = []
parse_with_selected_parser(markup)
if markup =~ Syntax
@left = Expression.parse(Regexp.last_match(1))
else
raise SyntaxError, options[:locale].t("errors.syntax.case")
end
end end
def parse(tokens) def parse(tokens)
body = case_body = new_body body = BlockBody.new
body = @blocks.last.attachment while parse_body(body, tokens) body = @blocks.last.attachment while parse_body(body, tokens)
@blocks.reverse_each do |condition|
body = condition.attachment
unless body.frozen?
body.remove_blank_strings if blank?
body.freeze
end
end
case_body.freeze
end end
def nodelist def nodelist
@@ -68,14 +44,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
@@ -86,50 +55,9 @@ module Liquid
private private
def strict2_parse(markup)
parser = @parse_context.new_parser(markup)
@left = safe_parse_expression(parser)
parser.consume(:end_of_string)
end
def strict_parse(markup)
lax_parse(markup)
end
def lax_parse(markup)
if markup =~ Syntax
@left = parse_expression(Regexp.last_match(1))
else
raise SyntaxError, options[:locale].t("errors.syntax.case")
end
end
def record_when_condition(markup) def record_when_condition(markup)
body = new_body body = BlockBody.new
if strict2_mode?
parse_strict2_when(markup, body)
else
parse_lax_when(markup, body)
end
end
def parse_strict2_when(markup, body)
parser = @parse_context.new_parser(markup)
loop do
expr = Condition.parse_expression(parse_context, parser.expression, safe: true)
block = Condition.new(@left, '==', expr)
block.attach(body)
@blocks << block
break unless parser.id?('or') || parser.consume?(:comma)
end
parser.consume(:end_of_string)
end
def parse_lax_when(markup, body)
while markup while markup
unless markup =~ WhenSyntax unless markup =~ WhenSyntax
raise SyntaxError, options[:locale].t("errors.syntax.case_invalid_when") raise SyntaxError, options[:locale].t("errors.syntax.case_invalid_when")
@@ -137,7 +65,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
@@ -149,7 +77,7 @@ module Liquid
end end
block = ElseCondition.new block = ElseCondition.new
block.attach(new_body) block.attach(BlockBody.new)
@blocks << block @blocks << block
end end
@@ -159,4 +87,6 @@ module Liquid
end end
end end
end end
Template.register_tag('case', Case)
end end
+1 -71
View File
@@ -1,19 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category syntax
# @liquid_name comment
# @liquid_summary
# Prevents an expression from being rendered or output.
# @liquid_description
# Any text inside `comment` tags won't be output, and any Liquid code will be parsed, but not executed.
# @liquid_syntax
# {% comment %}
# content
# {% endcomment %}
# @liquid_syntax_keyword content The content of the comment.
class Comment < Block class Comment < Block
def render_to_output_buffer(_context, output) def render_to_output_buffer(_context, output)
output output
@@ -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 end
parse_context.depth += 1 Template.register_tag('comment', Comment)
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 end
+13 -13
View File
@@ -1,20 +1,20 @@
# 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.
# @liquid_syntax
# {% continue %} # {% continue %}
# {% endif %}
# {% endfor %}
#
class Continue < Tag class Continue < Tag
INTERRUPT = ContinueInterrupt.new.freeze def interrupt
ContinueInterrupt.new
end
end
def render_to_output_buffer(context, output) Template.register_tag('continue', Continue)
context.push_interrupt(INTERRUPT)
output
end
end
end end
+24 -88
View File
@@ -1,33 +1,36 @@
# 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
UNNAMED_CYCLE_PATTERN = /\w+:0x\h{8}/
attr_reader :variables attr_reader :variables
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
parse_with_selected_parser(markup) case markup
when NamedSyntax
@variables = variables_from_string(Regexp.last_match(2))
@name = Expression.parse(Regexp.last_match(1))
when SimpleSyntax
@variables = variables_from_string(markup)
@name = @variables.to_s
else
raise SyntaxError, options[:locale].t("errors.syntax.cycle")
end end
def named?
@is_named
end end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
@@ -48,93 +51,26 @@ module Liquid
iteration += 1 iteration += 1
iteration = 0 if iteration >= @variables.size iteration = 0 if iteration >= @variables.size
context.registers[:cycle][key] = iteration context.registers[:cycle][key] = iteration
output output
end end
private private
# cycle [name:] expression(, expression)*
def strict2_parse(markup)
p = @parse_context.new_parser(markup)
@variables = []
raise SyntaxError, options[:locale].t("errors.syntax.cycle") if p.look(:end_of_string)
first_expression = safe_parse_expression(p)
if p.look(:colon)
# cycle name: expr1, expr2, ...
@name = first_expression
@is_named = true
p.consume(:colon)
# After the colon, parse the first variable (required for named cycles)
@variables << maybe_dup_lookup(safe_parse_expression(p))
else
# cycle expr1, expr2, ...
@variables << maybe_dup_lookup(first_expression)
end
# Parse remaining comma-separated expressions
while p.consume?(:comma)
break if p.look(:end_of_string)
@variables << maybe_dup_lookup(safe_parse_expression(p))
end
p.consume(:end_of_string)
unless @is_named
@name = @variables.to_s
@is_named = !@name.match?(UNNAMED_CYCLE_PATTERN)
end
end
def strict_parse(markup)
lax_parse(markup)
end
def lax_parse(markup)
case markup
when NamedSyntax
@variables = variables_from_string(Regexp.last_match(2))
@name = parse_expression(Regexp.last_match(1))
@is_named = true
when SimpleSyntax
@variables = variables_from_string(markup)
@name = @variables.to_s
@is_named = !@name.match?(UNNAMED_CYCLE_PATTERN)
else
raise SyntaxError, options[:locale].t("errors.syntax.cycle")
end
end
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
var = parse_expression(Regexp.last_match(1))
maybe_dup_lookup(var)
end.compact end.compact
end end
# For backwards compatibility, whenever a lookup is used in an unnamed cycle,
# we make it so that the @variables.to_s produces different strings for cycles
# called with the same arguments (since @variables.to_s is used as the cycle counter key)
# This makes it so {% cycle a, b %} and {% cycle a, b %} have independent counters even if a and b share value.
# This is not true for literal values, {% cycle "a", "b" %} and {% cycle "a", "b" %} share the same counter.
# I was really scratching my head about this one, but migrating away from this would be more headache
# than it's worth. So we're keeping this quirk for now.
def maybe_dup_lookup(var)
var.is_a?(VariableLookup) ? var.dup : var
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children def children
Array(@node.variables) Array(@node.variables)
end end
end end
end end
Template.register_tag('cycle', Cycle)
end end
+21 -41
View File
@@ -1,58 +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.
# #
# > Caution: # (To achieve the survival, the application must keep the context)
# > Predefined Liquid objects can be overridden by variables with the same name. #
# > To make sure that you can access all Liquid objects, make sure that your variable name doesn't match a predefined object's name. # if the variable does not exist, it is created with value 0.
# @liquid_description
# Variables that are declared with `decrement` are unique to the [layout](/themes/architecture/layouts), [template](/themes/architecture/templates), # Hello: {% decrement 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: -1
# Hello: -2
# Hello: -3
# #
# Similarly, variables that are created with `decrement` are independent from those created with [`assign`](/docs/api/liquid/tags/assign)
# and [`capture`](/docs/api/liquid/tags/capture). However, `decrement` and [`increment`](/docs/api/liquid/tags/increment) share
# variables.
# @liquid_syntax
# {% decrement variable_name %}
# @liquid_syntax_keyword variable_name The name of the variable being decremented.
class Decrement < Tag class Decrement < Tag
include ParserSwitching
attr_reader :variable_name
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
parse_with_selected_parser(markup) @variable = markup.strip
end
def lax_parse(markup)
@variable_name = markup.strip
end
def strict_parse(markup)
lax_parse(markup)
end
def strict2_parse(markup)
p = @parse_context.new_parser(markup.strip)
@variable_name = p.consume(:id)
p.consume(:end_of_string)
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
-81
View File
@@ -1,81 +0,0 @@
# frozen_string_literal: true
module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category syntax
# @liquid_name doc
# @liquid_summary
# Documents template elements with annotations.
# @liquid_description
# The `doc` tag allows developers to include documentation within Liquid
# templates. Any content inside `doc` tags is not rendered or outputted.
# Liquid code inside will be parsed but not executed. This facilitates
# tooling support for features like code completion, linting, and inline
# documentation.
#
# For detailed documentation syntax and examples, see the
# [`LiquidDoc` reference](/docs/storefronts/themes/tools/liquid-doc).
#
# @liquid_syntax
# {% doc %}
# Renders a message.
#
# @param {string} foo - A string value.
# @param {string} [bar] - An optional string value.
#
# @example
# {% render 'message', foo: 'Hello', bar: 'World' %}
# {% enddoc %}
class Doc < Block
NO_UNEXPECTED_ARGS = /\A\s*\z/
def initialize(tag_name, markup, parse_context)
super
ensure_valid_markup(tag_name, markup, parse_context)
end
def parse(tokens)
@body = +""
while (token = tokens.shift)
tag_name = token =~ BlockBody::FullTokenPossiblyInvalid && Regexp.last_match(2)
raise_nested_doc_error if tag_name == @tag_name
if tag_name == block_delimiter
parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
@body << Regexp.last_match(1) if Regexp.last_match(1) != ""
return
end
@body << token unless token.empty?
end
raise_tag_never_closed(block_name)
end
def render_to_output_buffer(_context, output)
output
end
def blank?
@body.empty?
end
def nodelist
[@body]
end
private
def ensure_valid_markup(tag_name, markup, parse_context)
unless NO_UNEXPECTED_ARGS.match?(markup)
raise SyntaxError, parse_context.locale.t("errors.syntax.block_tag_unexpected_args", tag: tag_name)
end
end
def raise_nested_doc_error
raise SyntaxError, parse_context.locale.t("errors.syntax.doc_invalid_nested")
end
end
end
+11 -24
View File
@@ -1,26 +1,17 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs # Echo outputs an expression
# @liquid_type tag #
# @liquid_category syntax # {% echo monkey %}
# @liquid_name echo # {% echo user.name %}
# @liquid_summary #
# Outputs an expression. # This is identical to variable output syntax, like {{ foo }}, but works
# @liquid_description # inside {% liquid %} tags. The full syntax is supported, including filters:
# Using the `echo` tag is the same as wrapping an expression in curly brackets (`{{` and `}}`). However, unlike the curly #
# bracket method, you can use the `echo` tag inside [`liquid` tags](/docs/api/liquid/tags/liquid). # {% echo user | link %}
# #
# > Tip:
# > You can use [filters](/docs/api/liquid/filters) on expressions inside `echo` tags.
# @liquid_syntax
# {% liquid
# echo expression
# %}
# @liquid_syntax_keyword expression The expression to be output.
class Echo < Tag class Echo < Tag
attr_reader :variable
def initialize(tag_name, markup, parse_context) def initialize(tag_name, markup, parse_context)
super super
@variable = Variable.new(markup, parse_context) @variable = Variable.new(markup, parse_context)
@@ -29,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
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor Template.register_tag('echo', Echo)
def children
[@node.variable]
end
end
end
end end
+58 -50
View File
@@ -1,29 +1,50 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs # "For" iterates over an array or collection.
# @liquid_type tag # Several useful variables are available to you within the loop.
# @liquid_category iteration
# @liquid_name for
# @liquid_summary
# Renders an expression for every item in an array.
# @liquid_description
# You can do a maximum of 50 iterations with a `for` loop. If you need to iterate over more than 50 items, then use the
# [`paginate` tag](/docs/api/liquid/tags/paginate) to split the items over multiple pages.
# #
# > Tip: # == Basic usage:
# > Every `for` loop has an associated [`forloop` object](/docs/api/liquid/objects/forloop) with information about the loop. # {% for item in collection %}
# @liquid_syntax # {{ forloop.index }}: {{ item.name }}
# {% for variable in array %}
# expression
# {% endfor %} # {% endfor %}
# @liquid_syntax_keyword variable The current item in the array. #
# @liquid_syntax_keyword array The array to iterate over. # == Advanced usage:
# @liquid_syntax_keyword expression The expression to render for each iteration. # {% for item in collection %}
# @liquid_optional_param limit: [number] The number of iterations to perform. # <div {% if forloop.first %}class="first"{% endif %}>
# @liquid_optional_param offset: [number] The 1-based index to start iterating at. # Item {{ forloop.index }}: {{ item.name }}
# @liquid_optional_param range [untyped] A custom numeric range to iterate over. # </div>
# @liquid_optional_param reversed [untyped] Iterate in reverse order. # {% 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.
#
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
@@ -33,21 +54,14 @@ module Liquid
super super
@from = @limit = nil @from = @limit = nil
parse_with_selected_parser(markup) parse_with_selected_parser(markup)
@for_block = new_body @for_block = BlockBody.new
@else_block = nil @else_block = nil
end end
def parse(tokens) def parse(tokens)
if parse_body(@for_block, tokens) return unless parse_body(@for_block, tokens)
parse_body(@else_block, tokens) parse_body(@else_block, tokens)
end end
if blank?
@else_block&.remove_blank_strings
@for_block.remove_blank_strings
end
@else_block&.freeze
@for_block.freeze
end
def nodelist def nodelist
@else_block ? [@for_block, @else_block] : [@for_block] @else_block ? [@for_block, @else_block] : [@for_block]
@@ -55,7 +69,7 @@ module Liquid
def unknown_tag(tag, markup, tokens) def unknown_tag(tag, markup, tokens)
return super unless tag == 'else' return super unless tag == 'else'
@else_block = new_body @else_block = BlockBody.new
end end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
@@ -78,7 +92,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,33 +102,26 @@ 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, safe: true)
@name = "#{@variable_name}-#{collection_name}" @name = "#{@variable_name}-#{collection_name}"
@collection_name = Expression.parse(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, safe: true) set_attribute(attribute, p.expression)
end end
p.consume(:end_of_string) p.consume(:end_of_string)
end end
private private
def strict2_parse(markup)
strict_parse(markup)
end
def collection_segment(context) def collection_segment(context)
offsets = context.registers[:for] ||= {} offsets = context.registers[:for] ||= {}
@@ -130,6 +137,7 @@ module Liquid
end end
collection = context.evaluate(@collection_name) collection = context.evaluate(@collection_name)
collection = collection.to_a if collection.is_a?(Range)
limit_value = context.evaluate(@limit) limit_value = context.evaluate(@limit)
to = if limit_value.nil? to = if limit_value.nil?
@@ -138,9 +146,7 @@ module Liquid
Utils.to_integer(limit_value) + from Utils.to_integer(limit_value) + from
end end
segment = Utils.slice_collection_for_iteration( segment = Utils.slice_collection(collection, from, to)
collection, from, to, context.resource_limits, use_range_to_a: true
)
segment.reverse! if @reversed segment.reverse! if @reversed
offsets[@name] = from + segment.length offsets[@name] = from + segment.length
@@ -179,16 +185,16 @@ module Liquid
output output
end end
def set_attribute(key, expr, safe: false) def set_attribute(key, expr)
case key case key
when 'offset' when 'offset'
@from = if expr == 'continue' @from = if expr == 'continue'
:continue :continue
else else
parse_expression(expr, safe: safe) Expression.parse(expr)
end end
when 'limit' when 'limit'
@limit = parse_expression(expr, safe: safe) @limit = Expression.parse(expr)
end end
end end
@@ -206,4 +212,6 @@ module Liquid
end end
end end
end end
Template.register_tag('for', For)
end end
+20 -39
View File
@@ -1,18 +1,16 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs # If is the conditional block
# @liquid_type tag #
# @liquid_category conditional # {% if user.admin %}
# @liquid_name if # Admin user!
# @liquid_summary # {% else %}
# Renders an expression if a specific condition is `true`. # Not admin user
# @liquid_syntax
# {% if condition %}
# expression
# {% endif %} # {% endif %}
# @liquid_syntax_keyword condition The condition to evaluate. #
# @liquid_syntax_keyword expression The expression to render if the condition is met. # There are {% if count < 5 %} less {% else %} more {% endif %} items than you need.
#
class If < Block class If < Block
Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o
ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o
@@ -33,17 +31,10 @@ 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|
block.attachment.remove_blank_strings if blank?
block.attachment.freeze
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 +43,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
@@ -66,10 +53,6 @@ module Liquid
private private
def strict2_parse(markup)
strict_parse(markup)
end
def push_block(tag, markup) def push_block(tag, markup)
block = if tag == 'else' block = if tag == 'else'
ElseCondition.new ElseCondition.new
@@ -78,25 +61,21 @@ module Liquid
end end
@blocks.push(block) @blocks.push(block)
block.attach(new_body) block.attach(BlockBody.new)
end
def parse_expression(markup, safe: false)
Condition.parse_expression(parse_context, markup, safe: safe)
end 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
@@ -106,7 +85,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
@@ -115,7 +94,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
@@ -124,9 +103,9 @@ module Liquid
end end
def parse_comparison(p) def parse_comparison(p)
a = parse_expression(p.expression, safe: true) a = Expression.parse(p.expression)
if (op = p.consume?(:comparison)) if (op = p.consume?(:comparison))
b = parse_expression(p.expression, safe: true) b = Expression.parse(p.expression)
Condition.new(a, op, b) Condition.new(a, op, b)
else else
Condition.new(a) Condition.new(a)
@@ -139,4 +118,6 @@ module Liquid
end end
end end
end end
Template.register_tag('if', If)
end end
+2
View File
@@ -14,4 +14,6 @@ module Liquid
output output
end end
end end
Template.register_tag('ifchanged', Ifchanged)
end end
+37 -76
View File
@@ -1,34 +1,46 @@
# 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 SYNTAX = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
FOR = 'for'
SYNTAX = /(#{QuotedFragment}+)(\s+(with|#{FOR})\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
Syntax = SYNTAX Syntax = SYNTAX
attr_reader :template_name_expr, :variable_name_expr, :attributes attr_reader :template_name_expr, :variable_name_expr, :attributes
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
parse_with_selected_parser(markup)
if markup =~ SYNTAX
template_name = Regexp.last_match(1)
variable_name = Regexp.last_match(3)
@alias_name = Regexp.last_match(5)
@variable_name_expr = variable_name ? Expression.parse(variable_name) : nil
@template_name_expr = Expression.parse(template_name)
@attributes = {}
markup.scan(TagAttributes) do |key, value|
@attributes[key] = Expression.parse(value)
end
else
raise SyntaxError, options[:locale].t("errors.syntax.include")
end
end end
def parse(_tokens) def parse(_tokens)
@@ -36,12 +48,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
@@ -54,11 +66,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)
@@ -85,57 +95,6 @@ module Liquid
alias_method :parse_context, :options alias_method :parse_context, :options
private :parse_context private :parse_context
def for_loop?
@is_for_loop
end
def strict2_parse(markup)
p = @parse_context.new_parser(markup)
@template_name_expr = safe_parse_expression(p)
with_or_for = p.id?("for") || p.id?("with")
@variable_name_expr = safe_parse_expression(p) if with_or_for
@alias_name = p.consume(:id) if p.id?("as")
@is_for_loop = (with_or_for == FOR)
p.consume?(:comma)
@attributes = {}
while p.look(:id)
key = p.consume
p.consume(:colon)
@attributes[key] = safe_parse_expression(p)
p.consume?(:comma)
end
p.consume(:end_of_string)
end
def strict_parse(markup)
lax_parse(markup)
end
def lax_parse(markup)
if markup =~ SYNTAX
template_name = Regexp.last_match(1)
with_or_for = Regexp.last_match(3)
variable_name = Regexp.last_match(4)
@alias_name = Regexp.last_match(6)
@variable_name_expr = variable_name ? parse_expression(variable_name) : nil
@template_name_expr = parse_expression(template_name)
@is_for_loop = (with_or_for == FOR)
@attributes = {}
markup.scan(TagAttributes) do |key, value|
@attributes[key] = parse_expression(value)
end
else
raise SyntaxError, options[:locale].t("errors.syntax.include")
end
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children def children
[ [
@@ -145,4 +104,6 @@ module Liquid
end end
end end
end end
Template.register_tag('include', Include)
end end
+18 -41
View File
@@ -1,58 +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.
# #
# > Caution: # if the variable does not exist, it is created with value 0.
# > Predefined Liquid objects can be overridden by variables with the same name. #
# > To make sure that you can access all Liquid objects, make sure that your variable name doesn't match a predefined object's name. # Hello: {% increment variable %}
# @liquid_description #
# Variables that are declared with `increment` are unique to the [layout](/themes/architecture/layouts), [template](/themes/architecture/templates), # gives you:
# 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. # 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
include ParserSwitching
attr_reader :variable_name
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
parse_with_selected_parser(markup) @variable = markup.strip
end
def lax_parse(markup)
@variable_name = markup.strip
end
def strict_parse(markup)
lax_parse(markup)
end
def strict2_parse(markup)
p = @parse_context.new_parser(markup.strip)
@variable_name = p.consume(:id)
p.consume(:end_of_string)
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
-28
View File
@@ -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
View File
@@ -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
+23 -88
View File
@@ -1,45 +1,29 @@
# 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' 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
parse_with_selected_parser(markup)
end
def for_loop? raise SyntaxError, options[:locale].t("errors.syntax.render") unless markup =~ SYNTAX
@is_for_loop
template_name = Regexp.last_match(1)
variable_name = Regexp.last_match(3)
@alias_name = Regexp.last_match(5)
@variable_name_expr = variable_name ? Expression.parse(variable_name) : nil
@template_name_expr = Expression.parse(template_name)
@attributes = {}
markup.scan(TagAttributes) do |key, value|
@attributes[key] = Expression.parse(value)
end
end end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
@@ -47,24 +31,23 @@ module Liquid
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
@attributes.each do |key, value| @attributes.each do |key, value|
inner_context[key] = context.evaluate(value) inner_context[key] = context.evaluate(value)
end end
@@ -74,7 +57,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 variable.is_a?(Array)
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
@@ -84,62 +67,14 @@ module Liquid
output output
end end
# render (string) (with|for expression)? (as id)? (key: value)*
def strict2_parse(markup)
p = @parse_context.new_parser(markup)
@template_name_expr = parse_expression(strict2_template_name(p), safe: true)
with_or_for = p.id?("for") || p.id?("with")
@variable_name_expr = safe_parse_expression(p) if with_or_for
@alias_name = p.consume(:id) if p.id?("as")
@is_for_loop = (with_or_for == FOR)
p.consume?(:comma)
@attributes = {}
while p.look(:id)
key = p.consume
p.consume(:colon)
@attributes[key] = safe_parse_expression(p)
p.consume?(:comma)
end
p.consume(:end_of_string)
end
def strict2_template_name(p)
p.consume(:string)
end
def strict_parse(markup)
lax_parse(markup)
end
def lax_parse(markup)
raise SyntaxError, options[:locale].t("errors.syntax.render") unless markup =~ SYNTAX
template_name = Regexp.last_match(1)
with_or_for = Regexp.last_match(3)
variable_name = Regexp.last_match(4)
@alias_name = Regexp.last_match(6)
@variable_name_expr = variable_name ? parse_expression(variable_name) : nil
@template_name_expr = parse_expression(template_name)
@is_for_loop = (with_or_for == FOR)
@attributes = {}
markup.scan(TagAttributes) do |key, value|
@attributes[key] = parse_expression(value)
end
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor class ParseTreeVisitor < Liquid::ParseTreeVisitor
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
+9 -83
View File
@@ -1,78 +1,19 @@
# 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
ALLOWED_ATTRIBUTES = ['cols', 'limit', 'offset', 'range'].freeze
attr_reader :variable_name, :collection_name, :attributes attr_reader :variable_name, :collection_name, :attributes
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
parse_with_selected_parser(markup)
end
def strict2_parse(markup)
p = @parse_context.new_parser(markup)
@variable_name = p.consume(:id)
unless p.id?("in")
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in")
end
@collection_name = safe_parse_expression(p)
p.consume?(:comma)
@attributes = {}
while p.look(:id)
key = p.consume
unless ALLOWED_ATTRIBUTES.include?(key)
raise SyntaxError, options[:locale].t("errors.syntax.table_row_invalid_attribute", attribute: key)
end
p.consume(:colon)
@attributes[key] = safe_parse_expression(p)
p.consume?(:comma)
end
p.consume(:end_of_string)
end
def strict_parse(markup)
lax_parse(markup)
end
def lax_parse(markup)
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")
@@ -82,16 +23,16 @@ 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_for_iteration(collection, from, to, context.resource_limits, allow_endless: true)
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.resource_limits.increment_write_score(output)
context.stack do context.stack do
tablerowloop = Liquid::TablerowloopDrop.new(length, cols) tablerowloop = Liquid::TablerowloopDrop.new(length, cols)
context['tablerowloop'] = tablerowloop context['tablerowloop'] = tablerowloop
@@ -102,17 +43,9 @@ module Liquid
output << "<td class=\"col#{tablerowloop.col}\">" output << "<td class=\"col#{tablerowloop.col}\">"
super super
output << '</td>' output << '</td>'
context.resource_limits.increment_write_score(output)
# 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}\">"
context.resource_limits.increment_write_score(output)
end end
tablerowloop.send(:increment!) tablerowloop.send(:increment!)
@@ -120,7 +53,6 @@ module Liquid
end end
output << "</tr>\n" output << "</tr>\n"
context.resource_limits.increment_write_score(output)
output output
end end
@@ -129,13 +61,7 @@ module Liquid
super + @node.attributes.values + [@node.collection_name] super + @node.attributes.values + [@node.collection_name]
end end
end end
end
private Template.register_tag('tablerow', TableRow)
def to_integer(value)
value.to_i
rescue NoMethodError
raise Liquid::ArgumentError, "invalid integer"
end
end
end end
+8 -25
View File
@@ -3,40 +3,21 @@
require_relative 'if' require_relative 'if'
module Liquid module Liquid
# @liquid_public_docs # Unless is a conditional just like 'if' but works on the inverse logic.
# @liquid_type tag #
# @liquid_category conditional # {% unless x < 0 %} x is greater than zero {% endunless %}
# @liquid_name unless #
# @liquid_summary
# Renders an expression unless a specific condition is `true`.
# @liquid_description
# > Tip:
# > Similar to the [`if` tag](/docs/api/liquid/tags/if), you can use `elsif` to add more conditions to an `unless` tag.
# @liquid_syntax
# {% unless condition %}
# expression
# {% endunless %}
# @liquid_syntax_keyword condition The condition to evaluate.
# @liquid_syntax_keyword expression The expression to render unless the condition is met.
class Unless < If class Unless < If
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
# First condition is interpreted backwards ( if not ) # First condition is interpreted backwards ( if not )
first_block = @blocks.first first_block = @blocks.first
result = Liquid::Utils.to_liquid_value( unless first_block.evaluate(context)
first_block.evaluate(context),
)
unless result
return first_block.attachment.render_to_output_buffer(context, output) return first_block.attachment.render_to_output_buffer(context, output)
end end
# After the first condition unless works just like if # After the first condition unless works just like if
@blocks[1..-1].each do |block| @blocks[1..-1].each do |block|
result = Liquid::Utils.to_liquid_value( if block.evaluate(context)
block.evaluate(context),
)
if result
return block.attachment.render_to_output_buffer(context, output) return block.attachment.render_to_output_buffer(context, output)
end end
end end
@@ -44,4 +25,6 @@ module Liquid
output output
end end
end end
Template.register_tag('unless', Unless)
end end
+115 -86
View File
@@ -2,7 +2,7 @@
module Liquid module Liquid
# Templates are central to liquid. # Templates are central to liquid.
# Interpreting templates is a two step process. First you compile the # Interpretating templates is a two step process. First you compile the
# source code you got. During compile time some extensive error checking is performed. # source code you got. During compile time some extensive error checking is performed.
# your code should expect to get some SyntaxErrors. # your code should expect to get some SyntaxErrors.
# #
@@ -15,96 +15,132 @@ 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
@@file_system = BlankFileSystem.new
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
# Sets how strict the parser should be. # Sets how strict the parser should be.
# :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 enforces correct syntax for most tags # :strict will enforce correct syntax.
# :strict2 enforces correct syntax for all tags attr_writer :error_mode
def error_mode=(mode)
Deprecations.warn("Template.error_mode=", "Environment#error_mode=")
Environment.default.error_mode = mode
end
def error_mode # Sets how strict the taint checker should be.
Environment.default.error_mode # :lax is the default, and ignores the taint flag completely
end # :warn adds a warning, but does not interrupt the rendering
# :error raises an error when tainted output is used
attr_writer :taint_mode
def default_exception_renderer=(renderer) attr_accessor :default_exception_renderer
Deprecations.warn("Template.default_exception_renderer=", "Environment#exception_renderer=") Template.default_exception_renderer = lambda do |exception|
Environment.default.exception_renderer = renderer exception
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 end
def file_system def file_system
Environment.default.file_system @@file_system
end end
def tags def file_system=(obj)
Environment.default.tags @@file_system = obj
end 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
def tags
@tags ||= TagRegistry.new
end
def add_register(name, klass)
registers[name.to_sym] = klass
end
def registers
@registers ||= {}
end
def error_mode
@error_mode ||= :lax
end
def taint_mode
@taint_mode ||= :lax
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") Strainer.global_filter(mod)
Environment.default.register_filter(mod)
end
private def default_resource_limits=(limits)
Deprecations.warn("Template.default_resource_limits=", "Environment#default_resource_limits=")
Environment.default.default_resource_limits = limits
end end
def default_resource_limits def default_resource_limits
Environment.default.default_resource_limits @default_resource_limits ||= {}
end 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 template = Template.new
new(environment: environment).parse(source, options) template.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(self.class.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
@@ -146,33 +182,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
c = 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)
drop.context = c if drop.respond_to?(:context=)
c
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,29 +215,22 @@ module Liquid
context.add_filters(args.pop) context.add_filters(args.pop)
end end
Template.registers.each do |key, register|
context_register[key] = register
end
# 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
previous_error_mode = context.registers.static[:template_error_mode]
context.registers.static[:template_error_mode] = @error_mode
begin begin
# render the nodelist. # render the nodelist.
# 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 || +'') @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
if previous_error_mode
context.registers.static[:template_error_mode] = previous_error_mode
else
context.registers.static.delete(:template_error_mode)
end
@errors = context.errors @errors = context.errors
end end
end end
@@ -218,24 +246,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
@error_mode = parse_context.error_mode
parse_context
end end
def apply_options_to_context(context, options) def apply_options_to_context(context, options)
-9
View File
@@ -1,9 +0,0 @@
# frozen_string_literal: true
module Liquid
class TemplateFactory
def for(_template_name)
Liquid::Template.new
end
end
end
+10 -132
View File
@@ -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/
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) @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,111 +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)
rescue ::ArgumentError => e
if e.message == "invalid byte sequence in #{@ss.string.encoding}"
raise SyntaxError, "Invalid byte sequence in #{@ss.string.encoding}"
else
raise
end
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
+3 -181
View File
@@ -2,9 +2,6 @@
module Liquid module Liquid
module Utils module Utils
DECIMAL_REGEX = /\A-?\d+\.\d+\z/
UNIX_TIMESTAMP_REGEX = /\A\d+\z/
def self.slice_collection(collection, from, to) def self.slice_collection(collection, from, to)
if (from != 0 || !to.nil?) && collection.respond_to?(:load_slice) if (from != 0 || !to.nil?) && collection.respond_to?(:load_slice)
collection.load_slice(from, to) collection.load_slice(from, to)
@@ -13,26 +10,6 @@ module Liquid
end end
end end
# This is intentionally separate from slice_collection, whose Array-returning
# behavior is used outside of the iteration tags.
def self.slice_collection_for_iteration(
collection, from, to, resource_limits, allow_endless: false, use_range_to_a: false
)
if integer_range?(collection)
RangeSlice.new(collection, from, to, resource_limits)
elsif collection.is_a?(Range)
if use_range_to_a && range_method_overridden?(collection, :to_a)
# For historically honored custom Range#to_a. Charge the resulting
# selection before buffering it, just as for a custom #each.
slice_collection_for_iteration_using_each(collection.to_a, from, to, resource_limits)
else
slice_range_using_each(collection, from, to, resource_limits, allow_endless: allow_endless)
end
else
slice_collection(collection, from, to)
end
end
def self.slice_collection_using_each(collection, from, to) def self.slice_collection_using_each(collection, from, to)
segments = [] segments = []
index = 0 index = 0
@@ -58,55 +35,6 @@ module Liquid
segments segments
end end
# Arithmetic slicing must not bypass a Range subclass's custom #each.
def self.integer_range?(collection)
collection.instance_of?(Range) && collection.begin.is_a?(Integer) && collection.end.is_a?(Integer)
end
private_class_method :integer_range?
# Preserve support for Ruby-supplied string ranges and custom Range#each.
# Their selected length cannot be inferred from integer bounds, but the tags
# need it before rendering for loop metadata, continuation offsets, and columns.
# Buffer the selection so we do not have to replay a potentially custom iterator.
def self.slice_range_using_each(collection, from, to, resource_limits, allow_endless:)
# TableRow historically accepted an endless subclass when its custom #each
# was finite, while For historically raised through Range#to_a.
if collection.end.nil? && !(allow_endless && (!to.nil? || range_method_overridden?(collection, :each)))
raise RangeError, "cannot convert endless range to an array"
end
if collection.begin.nil? && !range_method_overridden?(collection, :each)
raise TypeError, "can't iterate from NilClass"
end
slice_collection_for_iteration_using_each(collection, from, to, resource_limits)
end
private_class_method :slice_range_using_each
# Custom Range#each can make a nominally beginless range finite; standard
# beginless ranges were rejected before reaching this budgeted traversal.
def self.slice_collection_for_iteration_using_each(collection, from, to, resource_limits)
return [] if to && to <= from
segments = []
index = 0
collection.each do |item|
break if to && to <= index
# Charge preparation, including skipped offsets, before buffering; checking
# only while rendering the buffered values would leave this work unbudgeted.
resource_limits.increment_render_score(1)
segments << item if from <= index
index += 1
end
segments
end
private_class_method :slice_collection_for_iteration_using_each
def self.range_method_overridden?(collection, method_name)
collection.method(method_name).owner != Range.instance_method(method_name).owner
end
private_class_method :range_method_overridden?
def self.to_integer(num) def self.to_integer(num)
return num if num.is_a?(Integer) return num if num.is_a?(Integer)
num = num.to_s num = num.to_s
@@ -124,7 +52,7 @@ module Liquid
when Numeric when Numeric
obj obj
when String when String
DECIMAL_REGEX.match?(obj.strip) ? BigDecimal(obj) : obj.to_i /\A-?\d+\.\d+\z/.match?(obj.strip) ? BigDecimal(obj) : obj.to_i
else else
if obj.respond_to?(:to_number) if obj.respond_to?(:to_number)
obj.to_number obj.to_number
@@ -138,14 +66,14 @@ module Liquid
return obj if obj.respond_to?(:strftime) return obj if obj.respond_to?(:strftime)
if obj.is_a?(String) if obj.is_a?(String)
return if obj.empty? return nil if obj.empty?
obj = obj.downcase obj = obj.downcase
end end
case obj case obj
when 'now', 'today' when 'now', 'today'
Time.now Time.now
when UNIX_TIMESTAMP_REGEX, Integer when /\A\d+\z/, Integer
Time.at(obj.to_i) Time.at(obj.to_i)
when String when String
Time.parse(obj) Time.parse(obj)
@@ -153,111 +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
def self.to_s(obj, seen = {})
case obj
when BigDecimal
obj.to_s("F")
when Hash
# If the custom hash implementation overrides `#to_s`, use their
# custom implementation. Otherwise we use Liquid's default
# implementation.
if obj.class.instance_method(:to_s) == HASH_TO_S_METHOD
hash_inspect(obj, seen)
else
obj.to_s
end
when Array
array_inspect(obj, seen)
else
obj.to_s
end
end
def self.inspect(obj, seen = {})
case obj
when Hash
# If the custom hash implementation overrides `#inspect`, use their
# custom implementation. Otherwise we use Liquid's default
# implementation.
if obj.class.instance_method(:inspect) == HASH_INSPECT_METHOD
hash_inspect(obj, seen)
else
obj.inspect
end
when Array
array_inspect(obj, seen)
else
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
HASH_TO_S_METHOD = Hash.instance_method(:to_s)
private_constant :HASH_TO_S_METHOD
HASH_INSPECT_METHOD = Hash.instance_method(:inspect)
private_constant :HASH_INSPECT_METHOD
end end
end end
+41 -85
View File
@@ -30,17 +30,13 @@ 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
@markup @markup
end end
def ==(other)
self.class == other.class && name == other.name && filters == other.filters
end
def markup_context(markup) def markup_context(markup)
"in \"{{#{markup}}}\"" "in \"{{#{markup}}}\""
end end
@@ -51,44 +47,31 @@ 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|
next unless f =~ /\w+/ next unless f =~ /\w+/
filtername = Regexp.last_match(0) filtername = Regexp.last_match(0)
filterargs = f.scan(FilterArgsRegex).flatten filterargs = f.scan(FilterArgsRegex).flatten
@filters << lax_parse_filter_expressions(filtername, filterargs) @filters << parse_filter_expressions(filtername, filterargs)
end end
end end
end end
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.safe_parse_expression(p)
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 << lax_parse_filter_expressions(filtername, filterargs) @filters << parse_filter_expressions(filtername, filterargs)
end end
p.consume(:end_of_string) p.consume(:end_of_string)
end end
def strict2_parse(markup)
@filters = []
p = @parse_context.new_parser(markup)
return if p.look(:end_of_string)
@name = parse_context.safe_parse_expression(p)
@filters << strict2_parse_filter_expressions(p) while p.consume?(:pipe)
p.consume(:end_of_string)
end
def parse_filterargs(p) def parse_filterargs(p)
# first argument # first argument
filterargs = [p.argument] filterargs = [p.argument]
@@ -98,33 +81,27 @@ 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) obj = context.apply_global_filter(obj)
taint_check(context, obj)
obj
end end
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 if obj.is_a?(Array)
output << obj.join
elsif obj.nil?
else
output << obj.to_s
end end
def render_obj_to_output(obj, output) output
case obj
when NilClass
# Do nothing
when Array
obj.each do |o|
render_obj_to_output(o, output)
end
else
output << Liquid::Utils.to_s(obj)
end
end end
def disabled?(_context) def disabled?(_context)
@@ -137,15 +114,15 @@ module Liquid
private private
def lax_parse_filter_expressions(filter_name, unparsed_args) def parse_filter_expressions(filter_name, unparsed_args)
filter_args = [] filter_args = []
keyword_args = nil keyword_args = nil
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]
@@ -153,46 +130,6 @@ module Liquid
result result
end end
# Surprisingly, positional and keyword arguments can be mixed.
#
# filter = filtername [":" filterargs?]
# filterargs = argument ("," argument)*
# argument = (positional_argument | keyword_argument)
# positional_argument = expression
# keyword_argument = id ":" expression
def strict2_parse_filter_expressions(p)
filtername = p.consume(:id)
filter_args = []
keyword_args = {}
if p.consume?(:colon)
# Parse first argument (no leading comma)
argument(p, filter_args, keyword_args) unless end_of_arguments?(p)
# Parse remaining arguments (with leading commas) and optional trailing comma
argument(p, filter_args, keyword_args) while p.consume?(:comma) && !end_of_arguments?(p)
end
result = [filtername, filter_args]
result << keyword_args unless keyword_args.empty?
result
end
def argument(p, positional_arguments, keyword_arguments)
if p.look(:id) && p.look(:colon, 1)
key = p.consume(:id)
p.consume(:colon)
value = parse_context.safe_parse_expression(p)
keyword_arguments[key] = value
else
positional_arguments << parse_context.safe_parse_expression(p)
end
end
def end_of_arguments?(p)
p.look(:pipe) || p.look(:end_of_string)
end
def evaluate_filter_expressions(context, filter_args, filter_kwargs) def evaluate_filter_expressions(context, filter_args, filter_kwargs)
parsed_args = filter_args.map { |expr| context.evaluate(expr) } parsed_args = filter_args.map { |expr| context.evaluate(expr) }
if filter_kwargs if filter_kwargs
@@ -205,6 +142,25 @@ module Liquid
parsed_args parsed_args
end end
def taint_check(context, obj)
return unless obj.tainted?
return if Template.taint_mode == :lax
@markup =~ QuotedFragment
name = Regexp.last_match(0)
error = TaintedError.new("variable '#{name}' is tainted and was not escaped")
error.line_number = line_number
error.template_name = context.template_name
case Template.taint_mode
when :warn
context.warnings << error
when :error
raise error
end
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children def children
[@node.name] + @node.filters.flatten [@node.name] + @node.filters.flatten
+9 -28
View File
@@ -2,24 +2,21 @@
module Liquid module Liquid
class VariableLookup class VariableLookup
SQUARE_BRACKETED = /\A\[(.*)\]\z/m
COMMAND_METHODS = ['size', 'first', 'last'].freeze 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,14 +53,9 @@ 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
# Handle string first/last like ActiveSupport does (returns first/last character)
# ActiveSupport returns "" for empty strings, not nil
elsif lookup_command?(i) && object.is_a?(String) && (key == "first" || key == "last")
object = key == "first" ? (object[0] || "") : (object[-1] || "")
# 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
# keywords either. The only thing we got left is to return nil or # keywords either. The only thing we got left is to return nil or
# raise an exception if `strict_variables` option is set to true # raise an exception if `strict_variables` option is set to true
+1 -1
View File
@@ -2,5 +2,5 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
VERSION = "5.14.0" VERSION = "4.0.3"
end end
+4 -8
View File
@@ -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_development_dependency('rake', '~> 11.3')
s.add_dependency("bigdecimal")
s.add_development_dependency('rake', '~> 13.0')
s.add_development_dependency('minitest') s.add_development_dependency('minitest')
end end
+6 -11
View File
@@ -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
+6 -9
View File
@@ -3,19 +3,11 @@
require 'yaml' require 'yaml'
module Database module Database
DATABASE_FILE_PATH = "#{__dir__}/vision.database.yml"
# Load the standard vision toolkit database and re-arrage it to be simply exportable # Load the standard vision toolkit database and re-arrage it to be simply exportable
# to liquid as assigns. All this is based on Shopify # to liquid as assigns. All this is based on Shopify
def self.tables def self.tables
@tables ||= begin @tables ||= begin
db = db = YAML.load_file("#{__dir__}/vision.database.yml")
if YAML.respond_to?(:unsafe_load_file) # Only Psych 4+ can use unsafe_load_file
# unsafe_load_file is needed for YAML references
YAML.unsafe_load_file(DATABASE_FILE_PATH)
else
YAML.load_file(DATABASE_FILE_PATH)
end
# From vision source # From vision source
db['products'].each do |product| db['products'].each do |product|
@@ -49,3 +41,8 @@ module Database
end end
end end
end end
if __FILE__ == $PROGRAM_NAME
p(Database.tables['collections']['frontpage'].keys)
# p Database.tables['blog']['articles']
end
+7 -8
View File
@@ -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)
+2 -2
View File
@@ -45,8 +45,8 @@ class Paginate < Liquid::Block
pagination['items'] = collection_size pagination['items'] = collection_size
pagination['pages'] = page_count - 1 pagination['pages'] = page_count - 1
pagination['previous'] = link('&laquo; Previous', current_page - 1) if 1 < current_page pagination['previous'] = link('&laquo; Previous', current_page - 1) unless 1 >= current_page
pagination['next'] = link('Next &raquo;', current_page + 1) if page_count > current_page + 1 pagination['next'] = link('Next &raquo;', current_page + 1) unless page_count <= current_page + 1
pagination['parts'] = [] pagination['parts'] = []
hellip_break = false hellip_break = false
+1 -1
View File
@@ -60,7 +60,7 @@ module ShopFilter
case style case style
when 'original' when 'original'
'/files/shops/random_number/' + url return '/files/shops/random_number/' + url
when 'grande', 'large', 'medium', 'compact', 'small', 'thumb', 'icon' when 'grande', 'large', 'medium', 'compact', 'small', 'thumb', 'icon'
"/files/shops/random_number/products/#{Regexp.last_match(1)}_#{style}.#{Regexp.last_match(2)}" "/files/shops/random_number/products/#{Regexp.last_match(1)}_#{style}.#{Regexp.last_match(2)}"
else else
+3 -20
View File
@@ -3,7 +3,7 @@
# This profiler run simulates Shopify. # This profiler run simulates Shopify.
# We are looking in the tests directory for liquid files and render them within the designated layout file. # We are looking in the tests directory for liquid files and render them within the designated layout file.
# We will also export a substantial database to liquid which the templates can render values of. # We will also export a substantial database to liquid which the templates can render values of.
# All this is to make the benchmark as non synthetic as possible. All templates and tests are lifted from # All this is to make the benchmark as non syntetic as possible. All templates and tests are lifted from
# direct real-world usage and the profiler measures code that looks very similar to the way it looks in # direct real-world usage and the profiler measures code that looks very similar to the way it looks in
# Shopify which is likely the biggest user of liquid in the world which something to the tune of several # Shopify which is likely the biggest user of liquid in the world which something to the tune of several
# million Template#render calls a day. # million Template#render calls a day.
@@ -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
-94
View File
@@ -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
-43
View File
@@ -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
-49
View File
@@ -1,49 +0,0 @@
# frozen_string_literal: true
# Liquid Spec Adapter for Shopify/liquid (Ruby reference implementation)
#
# Run with: bundle exec liquid-spec run spec/ruby_liquid.rb
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
require 'liquid'
require_relative 'support/liquid_spec_adapter_helper'
LiquidSpec.configure do |config|
config.missing_features = [
:activesupport,
:lax_parsing,
:shopify_filters,
:shopify_includes,
:shopify_blank,
:shopify_error_handling,
:shopify_error_format,
:shopify_string_access,
]
end
# Compile a template string into a Liquid::Template
LiquidSpec.compile do |ctx, source, options|
options[:error_mode] ||= :strict
ctx[:template] = Liquid::Template.parse(source, **options)
end
# Render a compiled template with the given context
# @param ctx [Hash] adapter context containing :template
# @param assigns [Hash] environment variables
# @param options [Hash] :registers, :strict_errors, :exception_renderer
LiquidSpec.render do |ctx, assigns, options|
registers = Liquid::Registers.new(options[:registers] || {})
context = Liquid::Context.build(
static_environments: assigns,
registers: registers,
rethrow_errors: options[:strict_errors],
resource_limits: LiquidSpecAdapterHelper.resource_limits(options),
)
context.exception_renderer = options[:exception_renderer] if options[:exception_renderer]
LiquidSpecAdapterHelper.with_frozen_time do
ctx[:template].render(context)
end
end
-46
View File
@@ -1,46 +0,0 @@
# frozen_string_literal: true
# Liquid Spec Adapter for Shopify/liquid with lax parsing mode
#
# Run with: bundle exec liquid-spec run spec/ruby_liquid_lax.rb
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
require 'liquid'
require_relative 'support/liquid_spec_adapter_helper'
LiquidSpec.configure do |config|
config.missing_features = [
:activesupport,
:shopify_filters,
:shopify_includes,
:shopify_blank,
:shopify_error_handling,
:shopify_error_format,
:shopify_string_access,
]
end
# Compile a template string into a Liquid::Template
LiquidSpec.compile do |ctx, source, options|
# Default to lax mode while still honoring specs that explicitly set error_mode.
options = { error_mode: :lax }.merge(options)
ctx[:template] = Liquid::Template.parse(source, **options)
end
# Render a compiled template with the given context
LiquidSpec.render do |ctx, assigns, options|
registers = Liquid::Registers.new(options[:registers] || {})
context = Liquid::Context.build(
static_environments: assigns,
registers: registers,
rethrow_errors: options[:strict_errors],
resource_limits: LiquidSpecAdapterHelper.resource_limits(options),
)
context.exception_renderer = options[:exception_renderer] if options[:exception_renderer]
LiquidSpecAdapterHelper.with_frozen_time do
ctx[:template].render(context)
end
end
-49
View File
@@ -1,49 +0,0 @@
# frozen_string_literal: true
# Liquid Spec Adapter for Shopify/liquid with ActiveSupport loaded
#
# Run with: bundle exec liquid-spec run spec/ruby_liquid_with_active_support.rb
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
require 'active_support/all'
require 'liquid'
require_relative 'support/liquid_spec_adapter_helper'
LiquidSpec.configure do |config|
config.missing_features = [
:lax_parsing,
:shopify_filters,
:shopify_includes,
:shopify_blank,
:shopify_error_handling,
:shopify_error_format,
:shopify_string_access,
]
end
# Compile a template string into a Liquid::Template
LiquidSpec.compile do |ctx, source, options|
options[:error_mode] ||= :strict
ctx[:template] = Liquid::Template.parse(source, **options)
end
# Render a compiled template with the given context
# @param ctx [Hash] adapter context containing :template
# @param assigns [Hash] environment variables
# @param options [Hash] :registers, :strict_errors, :exception_renderer
LiquidSpec.render do |ctx, assigns, options|
registers = Liquid::Registers.new(options[:registers] || {})
context = Liquid::Context.build(
static_environments: assigns,
registers: registers,
rethrow_errors: options[:strict_errors],
resource_limits: LiquidSpecAdapterHelper.resource_limits(options),
)
context.exception_renderer = options[:exception_renderer] if options[:exception_renderer]
LiquidSpecAdapterHelper.with_frozen_time do
ctx[:template].render(context)
end
end
-53
View File
@@ -1,53 +0,0 @@
# frozen_string_literal: true
# Liquid Spec Adapter for Shopify/liquid with YJIT + strict mode + ActiveSupport
#
# Run with: bundle exec liquid-spec run spec/ruby_liquid_yjit.rb
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
# Enable YJIT if available
if defined?(RubyVM::YJIT) && RubyVM::YJIT.respond_to?(:enable)
RubyVM::YJIT.enable
end
require 'active_support/all'
require 'liquid'
require_relative 'support/liquid_spec_adapter_helper'
LiquidSpec.configure do |config|
config.missing_features = [
:lax_parsing,
:shopify_filters,
:shopify_includes,
:shopify_blank,
:shopify_error_handling,
:shopify_error_format,
:shopify_string_access,
]
end
# Compile a template string into a Liquid::Template
LiquidSpec.compile do |ctx, source, options|
# Force strict mode
options = { error_mode: :strict }.merge(options)
ctx[:template] = Liquid::Template.parse(source, **options)
end
# Render a compiled template with the given context
LiquidSpec.render do |ctx, assigns, options|
registers = Liquid::Registers.new(options[:registers] || {})
context = Liquid::Context.build(
static_environments: assigns,
registers: registers,
rethrow_errors: options[:strict_errors],
resource_limits: LiquidSpecAdapterHelper.resource_limits(options),
)
context.exception_renderer = options[:exception_renderer] if options[:exception_renderer]
LiquidSpecAdapterHelper.with_frozen_time do
ctx[:template].render(context)
end
end
@@ -1,24 +0,0 @@
# frozen_string_literal: true
module LiquidSpecAdapterHelper
extend self
def resource_limits(render_options)
return unless render_options[:resource_limits]
Liquid::ResourceLimits.new({}).tap do |limits|
render_options[:resource_limits].each do |key, value|
limits.public_send(:"#{key}=", value)
end
end
end
def with_frozen_time(&block)
original_tz = ENV['TZ']
ENV['TZ'] = 'UTC'
Liquid::Spec::TimeFreezer.freeze(Liquid::Spec::AdapterRunner::TEST_TIME, &block)
ensure
ENV['TZ'] = original_tz
end
end
+21 -128
View File
@@ -6,152 +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,
)
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
def test_assign_with_valid_identifier_in_strict2
assert_template_result("hello", "{% assign my_var = 'hello' %}{{ my_var }}", error_mode: :strict2)
end
def test_assign_with_hyphen_in_strict2
assert_template_result("hello", "{% assign my-var = 'hello' %}{{ my-var }}", error_mode: :strict2)
end
def test_assign_rejects_parentheses_in_variable_name_in_strict2
assert_raises(Liquid::SyntaxError) do
Liquid::Template.parse("{% assign (a(b(c) = 1234 %}", error_mode: :strict2)
end end
end end
with_error_mode(:lax) do
def test_assign_rejects_brackets_in_variable_name_in_strict2 assert Template.parse("{% assign foo = ('X' | downcase) %}")
assert_raises(Liquid::SyntaxError) do
Liquid::Template.parse("{% assign [x.y] = 'hello' %}", error_mode: :strict2)
end
end
def test_assign_rejects_dot_in_variable_name_in_strict2
assert_raises(Liquid::SyntaxError) do
Liquid::Template.parse("{% assign a.b = 'hello' %}", error_mode: :strict2)
end
end
def test_assign_rejects_numeric_variable_name_in_strict2
assert_raises(Liquid::SyntaxError) do
Liquid::Template.parse("{% assign 1abc = 'hello' %}", error_mode: :strict2)
end
end
def test_assign_allows_invalid_names_in_lax
assert_template_result("1234", "{% assign (a(b(c) = 1234 %}{{ self['(a(b(c)'] }}", error_mode: :lax)
end
def test_assign_with_filter_in_strict2
assert_template_result("HELLO", "{% assign my_var = 'hello' | upcase %}{{ my_var }}", error_mode: :strict2)
end
private
class ObjectWrapperDrop < Liquid::Drop
def initialize(obj)
@obj = obj
end
def value
@obj
end
end
def assign_score_of(obj)
context = Liquid::Context.new('drop' => ObjectWrapperDrop.new(obj))
Liquid::Template.parse('{% assign obj = drop.value %}').render!(context)
context.resource_limits.assign_score
end end
end end
end # AssignTest
@@ -1,85 +0,0 @@
# frozen_string_literal: true
require 'test_helper'
class BlankBodyErrorHandlingTest < Minitest::Test
COMPARISON_ERROR = 'Liquid error (line 1): comparison of Integer with String failed'
INVALID_INTEGER_ERROR = 'Liquid error (line 1): invalid integer'
def render_inline(source, error_mode:, assigns: {})
Liquid::Template.parse(source, line_numbers: true, error_mode: error_mode).render(assigns, render_errors: true)
end
def assert_render_raises(source, error_mode:, assigns: {}, message: nil)
error = assert_raises(Liquid::ArgumentError) do
Liquid::Template.parse(source, line_numbers: true, error_mode: error_mode).render!(assigns)
end
assert_includes(error.message, message) if message
end
def test_blank_if_body_suppresses_inline_error_text_in_lax_and_strict
[:lax, :strict].each do |mode|
assert_equal('', render_inline('{% if 5 > "x" %}{% endif %}', error_mode: mode))
end
end
def test_blank_unless_body_suppresses_inline_error_text_in_lax_and_strict
[:lax, :strict].each do |mode|
assert_equal('', render_inline('{% unless 5 > "x" %} {% endunless %}', error_mode: mode))
end
end
def test_blank_for_body_suppresses_inline_error_text_in_lax_and_strict
[:lax, :strict].each do |mode|
assert_equal('', render_inline('{% for i in (1..3) offset: xs %}{% endfor %}', error_mode: mode, assigns: { 'xs' => 'bad' }))
end
end
def test_strict2_blank_if_body_shows_inline_error_text
assert_equal(COMPARISON_ERROR, render_inline('{% if 5 > "x" %}{% endif %}', error_mode: :strict2))
end
def test_strict2_whitespace_if_body_shows_inline_error_text
assert_equal(COMPARISON_ERROR, render_inline('{% if 5 > "x" %} {% endif %}', error_mode: :strict2))
end
def test_strict2_assign_if_body_shows_inline_error_text
assert_equal(COMPARISON_ERROR, render_inline('{% if 5 > "x" %}{% assign a = 1 %}{% endif %}', error_mode: :strict2))
end
def test_strict2_comment_if_body_shows_inline_error_text
assert_equal(COMPARISON_ERROR, render_inline('{% if 5 > "x" %}{% comment %}c{% endcomment %}{% endif %}', error_mode: :strict2))
end
def test_strict2_capture_if_body_shows_inline_error_text
assert_equal(COMPARISON_ERROR, render_inline('{% if 5 > "x" %}{% capture c %}text{% endcapture %}{% endif %}', error_mode: :strict2))
end
def test_strict2_blank_unless_body_shows_inline_error_text
assert_equal(COMPARISON_ERROR, render_inline('{% unless 5 > "x" %} {% endunless %}', error_mode: :strict2))
end
def test_strict2_blank_for_body_shows_inline_error_text
assert_equal(INVALID_INTEGER_ERROR, render_inline('{% for i in (1..3) offset: xs %}{% endfor %}', error_mode: :strict2, assigns: { 'xs' => 'bad' }))
end
def test_nonblank_bodies_show_inline_error_text_in_all_modes
[:lax, :strict, :strict2].each do |mode|
assert_equal(COMPARISON_ERROR, render_inline('{% if 5 > "x" %}{% echo 1 %}{% endif %}', error_mode: mode))
assert_equal(COMPARISON_ERROR, render_inline('{% if 5 > "x" %}{{ "" }}{% endif %}', error_mode: mode))
assert_equal(COMPARISON_ERROR, render_inline('{% if 5 > "x" %}{% else %}E{% endif %}', error_mode: mode))
end
end
def test_raised_errors_are_not_swallowed_by_blank_if_body
[:lax, :strict, :strict2].each do |mode|
assert_render_raises('{% if 5 > "x" %}{% endif %}', error_mode: mode, message: 'comparison of Integer with String failed')
end
end
def test_raised_errors_are_not_swallowed_by_blank_for_body
[:lax, :strict, :strict2].each do |mode|
assert_render_raises('{% for i in (1..3) offset: xs %}{% endfor %}', error_mode: mode, assigns: { 'xs' => 'bad' }, message: 'invalid integer')
end
end
end
+13 -20
View File
@@ -9,6 +9,12 @@ class FoobarTag < Liquid::Tag
end end
end end
class BlankTestFileSystem
def read_template_file(template_path)
template_path
end
end
class BlankTest < Minitest::Test class BlankTest < Minitest::Test
include Liquid include Liquid
N = 10 N = 10
@@ -27,7 +33,7 @@ class BlankTest < Minitest::Test
def test_new_tags_are_not_blank_by_default def test_new_tags_are_not_blank_by_default
with_custom_tag('foobar', FoobarTag) do with_custom_tag('foobar', FoobarTag) do
assert_equal(" " * N, Liquid::Template.parse(wrap_in_for("{% foobar %}")).render!) assert_template_result(" " * N, wrap_in_for("{% foobar %}"))
end end
end end
@@ -57,11 +63,9 @@ class BlankTest < Minitest::Test
def test_nested_blocks_are_blank_but_only_if_all_children_are def test_nested_blocks_are_blank_but_only_if_all_children_are
assert_template_result("", wrap(wrap(" "))) assert_template_result("", wrap(wrap(" ")))
assert_template_result( assert_template_result("\n but this is not " * (N + 1),
"\n but this is not " * (N + 1),
wrap('{% if true %} {% comment %} this is blank {% endcomment %} {% endif %} wrap('{% if true %} {% comment %} this is blank {% endcomment %} {% endif %}
{% if true %} but this is not {% endif %}'), {% if true %} but this is not {% endif %}'))
)
end end
def test_assigns_are_blank def test_assigns_are_blank
@@ -91,21 +95,10 @@ class BlankTest < Minitest::Test
end end
def test_include_is_blank def test_include_is_blank
assert_template_result( Liquid::Template.file_system = BlankTestFileSystem.new
"foobar" * (N + 1), assert_template_result("foobar" * (N + 1), wrap("{% include 'foobar' %}"))
wrap("{% include 'foobar' %}"), assert_template_result(" foobar " * (N + 1), wrap("{% include ' foobar ' %}"))
partials: { 'foobar' => 'foobar' }, assert_template_result(" " * (N + 1), wrap(" {% include ' ' %} "))
)
assert_template_result(
" foobar " * (N + 1),
wrap("{% include ' foobar ' %}"),
partials: { ' foobar ' => ' foobar ' },
)
assert_template_result(
" " * (N + 1),
wrap(" {% include ' ' %} "),
partials: { ' ' => ' ' },
)
end end
def test_case_is_blank def test_case_is_blank

Some files were not shown because too many files have changed in this diff Show More