mirror of
https://github.com/Shopify/liquid.git
synced 2025-08-14 00:01:40 -04:00
Compare commits
181 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
dbe709c3bf | ||
|
2b75bfaff4 | ||
|
87bc6e7cfa | ||
|
7f122aeed2 | ||
|
aa1640035f | ||
|
f5d6a36574 | ||
|
c5711c095f | ||
|
284f5fb647 | ||
|
21432928d0 | ||
|
1783c0c084 | ||
|
e38f730c00 | ||
|
2d0442798b | ||
|
6453a0ea48 | ||
|
a398b4cc74 | ||
|
cca9fe99cf | ||
|
17d327988d | ||
|
f643af4bac | ||
|
ae8a0a86ac | ||
|
b439d0da53 | ||
|
16592cfb8f | ||
|
da4afd4156 | ||
|
1bb3091208 | ||
|
040801b32c | ||
|
550135c0b9 | ||
|
aec966eed7 | ||
|
bfe29e11be | ||
|
f9454d8cf3 | ||
|
8dd9279265 | ||
|
bf1419b8ac | ||
|
5718c4cee2 | ||
|
b0dbc62696 | ||
|
03aafa974c | ||
|
6372289ba3 | ||
|
0ec52a40b5 | ||
|
74af735f0e | ||
|
4b65a28722 | ||
|
ecf25ea83d | ||
|
6909570f8e | ||
|
cd9971579f | ||
|
8e37c5e18b | ||
|
b3f9639e7d | ||
|
fe3da0e17d | ||
|
e200c4544b | ||
|
7124540563 | ||
|
0558bd12c4 | ||
|
0639a094a8 | ||
|
58777bcd93 | ||
|
323951b36f | ||
|
a5b91e83ae | ||
|
10114b333e | ||
|
a07ae90523 | ||
|
2e236b0a0e | ||
|
bd05dfbd1c | ||
|
2a5ecf068f | ||
|
0b8e30b819 | ||
|
97cada61f9 | ||
|
7c9b77e8cf | ||
|
d48708ae46 | ||
|
42d822bda9 | ||
|
2c7d686690 | ||
|
313d01706a | ||
|
e9f86724f6 | ||
|
0a17c15289 | ||
|
04bd9dbe90 | ||
|
253ec81b56 | ||
|
e05719fb82 | ||
|
ac374a208f | ||
|
91be3dd75e | ||
|
40191022e8 | ||
|
71506ad54e | ||
|
cef64e277e | ||
|
7c592c1c00 | ||
|
002e4caea7 | ||
|
3c16c27ee1 | ||
|
eff5c5de8e | ||
|
19adfbd863 | ||
|
a672f3836c | ||
|
92fa334192 | ||
|
7c8a269b4d | ||
|
0b1dc295ff | ||
|
1de6025362 | ||
|
8ecb703d4d | ||
|
b4667adadf | ||
|
94e02d765f | ||
|
60701f865d | ||
|
0c49dd592f | ||
|
c3ac0e0127 | ||
|
a0a4307e7d | ||
|
fdd8c714b2 | ||
|
63583ffe5b | ||
|
9a06cedbba | ||
|
42b6763546 | ||
|
e5d18c83bb | ||
|
c77ff68573 | ||
|
b0cba0bfd2 | ||
|
8d8661349a | ||
|
1f3ea7322b | ||
|
06f44226c0 | ||
|
4bd22a26dc | ||
|
3ed54bfdf9 | ||
|
29986d3704 | ||
|
8e40f8050a | ||
|
ffce6de8bb | ||
|
f00670cb01 | ||
|
f6a3e25e2e | ||
|
f6ffc37cf2 | ||
|
1375a9e4dc | ||
|
c626dfa1a1 | ||
|
8a9f33a060 | ||
|
1943441361 | ||
|
36251e640c | ||
|
d94293a464 | ||
|
6c13805a60 | ||
|
b4196489c2 | ||
|
6d58c41440 | ||
|
fb6ac72520 | ||
|
cb16219552 | ||
|
8d7ed706f4 | ||
|
b3553787c8 | ||
|
b233b3d081 | ||
|
ac91d31268 | ||
|
9067e5167a | ||
|
fb6634f454 | ||
|
a0411e0927 | ||
|
ed421202e2 | ||
|
d6ca569e8a | ||
|
d36937d17f | ||
|
77bc56a1c2 | ||
|
88d013c8da | ||
|
36c7fc8e07 | ||
|
2b4810006b | ||
|
fc4f19471e | ||
|
8596bb2e38 | ||
|
6bf18775e7 | ||
|
56a0b7c42b | ||
|
dba733084e | ||
|
4a4fe3c72a | ||
|
4f35b0bc66 | ||
|
a5e5fab82a | ||
|
02ecaab9d1 | ||
|
1b2b62964e | ||
|
9b38a15282 | ||
|
7b25b770af | ||
|
cf76c0bbec | ||
|
6a0fe3f7e3 | ||
|
730ad3684a | ||
|
3ac7e470e6 | ||
|
369a6c55e3 | ||
|
f5ed5404b5 | ||
|
a3c837687e | ||
|
96a036372d | ||
|
4924822c88 | ||
|
fc9c338682 | ||
|
3c5ad7db61 | ||
|
c658bf970a | ||
|
c618ac1c9f | ||
|
0f0d5d889f | ||
|
11a1f8e673 | ||
|
24d461a9e3 | ||
|
cbb422e5d3 | ||
|
bf0f79f36c | ||
|
cf2787791e | ||
|
6a5ebb0e85 | ||
|
6dafc19b6d | ||
|
41f65173b0 | ||
|
e180535784 | ||
|
a681e73aec | ||
|
2abf52d546 | ||
|
eada2b65a2 | ||
|
dbf0aa8cd5 | ||
|
407a8e5b0f | ||
|
e3dcc75ab5 | ||
|
ceb7a4237f | ||
|
7b60b7fef5 | ||
|
33e1a8ffbc | ||
|
cc47fa8f03 | ||
|
b1b9b9f691 | ||
|
75e7725f57 | ||
|
de6d15a73e | ||
|
2c5d2be193 | ||
|
347a2418c4 |
25
.github/workflows/liquid.yml
vendored
25
.github/workflows/liquid.yml
vendored
@ -11,9 +11,24 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
entry:
|
||||
- { ruby: 2.7, allowed-failure: false } # minimum supported
|
||||
- { ruby: 3.2, allowed-failure: false } # latest
|
||||
- { ruby: ruby-head, allowed-failure: true }
|
||||
- { ruby: 3.0, allowed-failure: false } # minimum supported
|
||||
- { ruby: 3.2, allowed-failure: false }
|
||||
- { ruby: 3.3, allowed-failure: false }
|
||||
- { ruby: 3.3, allowed-failure: false }
|
||||
- { ruby: 3.4, allowed-failure: false } # latest
|
||||
- {
|
||||
ruby: 3.4,
|
||||
allowed-failure: false,
|
||||
rubyopt: "--enable-frozen-string-literal",
|
||||
}
|
||||
- { ruby: 3.4, allowed-failure: false, rubyopt: "--yjit" }
|
||||
- { ruby: ruby-head, allowed-failure: false }
|
||||
- {
|
||||
ruby: ruby-head,
|
||||
allowed-failure: false,
|
||||
rubyopt: "--enable-frozen-string-literal",
|
||||
}
|
||||
- { ruby: ruby-head, allowed-failure: false, rubyopt: "--yjit" }
|
||||
name: Test Ruby ${{ matrix.entry.ruby }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@ -21,8 +36,11 @@ jobs:
|
||||
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 }}
|
||||
|
||||
memory_profile:
|
||||
runs-on: ubuntu-latest
|
||||
@ -30,6 +48,5 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 2.7
|
||||
bundler-cache: true
|
||||
- run: bundle exec rake memory_profile:run
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -4,7 +4,6 @@
|
||||
pkg
|
||||
*.rbc
|
||||
.rvmrc
|
||||
.ruby-version
|
||||
Gemfile.lock
|
||||
.bundle
|
||||
.byebug_history
|
||||
Gemfile.lock
|
||||
|
@ -10,7 +10,6 @@ Performance:
|
||||
Enabled: true
|
||||
|
||||
AllCops:
|
||||
TargetRubyVersion: 2.7
|
||||
NewCops: disable
|
||||
SuggestExtensions: false
|
||||
Exclude:
|
||||
|
1
.ruby-version
Normal file
1
.ruby-version
Normal file
@ -0,0 +1 @@
|
||||
3.4.1
|
@ -26,3 +26,11 @@
|
||||
* If it makes sense, add tests for your code and/or run a performance benchmark
|
||||
* Make sure all tests pass (`bundle exec rake`)
|
||||
* Create a pull request
|
||||
|
||||
## 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)
|
||||
|
||||
|
13
Gemfile
13
Gemfile
@ -7,22 +7,25 @@ end
|
||||
|
||||
gemspec
|
||||
|
||||
gem "base64"
|
||||
|
||||
group :benchmark, :test do
|
||||
gem 'benchmark-ips'
|
||||
gem 'memory_profiler'
|
||||
gem 'terminal-table'
|
||||
gem "lru_redux"
|
||||
|
||||
install_if -> { RUBY_PLATFORM !~ /mingw|mswin|java/ && RUBY_ENGINE != 'truffleruby' } do
|
||||
gem 'stackprof'
|
||||
end
|
||||
end
|
||||
|
||||
group :development do
|
||||
gem "webrick"
|
||||
end
|
||||
|
||||
group :test do
|
||||
gem 'rubocop', '~> 1.44.0'
|
||||
gem 'rubocop', '~> 1.61.0'
|
||||
gem 'rubocop-shopify', '~> 2.12.0', require: false
|
||||
gem 'rubocop-performance', require: false
|
||||
|
||||
platform :mri, :truffleruby do
|
||||
gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'master'
|
||||
end
|
||||
end
|
||||
|
75
History.md
75
History.md
@ -1,5 +1,80 @@
|
||||
# Liquid Change Log
|
||||
|
||||
## 5.8.1 (unreleased)
|
||||
|
||||
## 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
|
||||
|
50
README.md
50
README.md
@ -1,4 +1,4 @@
|
||||
[](http://travis-ci.org/Shopify/liquid)
|
||||
[](https://github.com/Shopify/liquid/actions/workflows/liquid.yml)
|
||||
[](http://inch-ci.org/github/Shopify/liquid)
|
||||
|
||||
# Liquid template engine
|
||||
@ -52,6 +52,47 @@ For standard use you can just pass it the content of a file and call render with
|
||||
@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
|
||||
|
||||
Setting the error mode of Liquid lets you specify how strictly you want your templates to be interpreted.
|
||||
@ -62,9 +103,10 @@ Liquid also comes with a stricter parser that can be used when editing templates
|
||||
when templates are invalid. You can enable this new parser like this:
|
||||
|
||||
```ruby
|
||||
Liquid::Template.error_mode = :strict # Raises a SyntaxError when invalid syntax is used
|
||||
Liquid::Template.error_mode = :warn # Adds strict errors to template.errors but continues as normal
|
||||
Liquid::Template.error_mode = :lax # The default mode, accepts almost anything.
|
||||
Liquid::Environment.default.error_mode = :strict
|
||||
Liquid::Environment.default.error_mode = :strict # Raises a SyntaxError when invalid syntax is used
|
||||
Liquid::Environment.default.error_mode = :warn # Adds strict errors to template.errors but continues as normal
|
||||
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`:
|
||||
|
31
Rakefile
31
Rakefile
@ -43,8 +43,6 @@ task :test do
|
||||
Rake::Task['base_test'].invoke
|
||||
|
||||
if RUBY_ENGINE == 'ruby' || RUBY_ENGINE == 'truffleruby'
|
||||
ENV['LIQUID_C'] = '1'
|
||||
|
||||
ENV['LIQUID_PARSER_MODE'] = 'lax'
|
||||
Rake::Task['integration_test'].reenable
|
||||
Rake::Task['integration_test'].invoke
|
||||
@ -73,7 +71,7 @@ end
|
||||
|
||||
namespace :benchmark do
|
||||
desc "Run the liquid benchmark with lax parsing"
|
||||
task :run do
|
||||
task :lax do
|
||||
ruby "./performance/benchmark.rb lax"
|
||||
end
|
||||
|
||||
@ -81,6 +79,33 @@ namespace :benchmark do
|
||||
task :strict do
|
||||
ruby "./performance/benchmark.rb strict"
|
||||
end
|
||||
|
||||
desc "Run the liquid benchmark with both lax and strict parsing"
|
||||
task run: [:lax, :strict]
|
||||
|
||||
desc "Run unit benchmarks"
|
||||
namespace :unit do
|
||||
task :all do
|
||||
Dir["./performance/unit/*_benchmark.rb"].each do |file|
|
||||
puts "🧪 Running #{file}"
|
||||
ruby file
|
||||
end
|
||||
end
|
||||
|
||||
task :lexer do
|
||||
Dir["./performance/unit/lexer_benchmark.rb"].each do |file|
|
||||
puts "🧪 Running #{file}"
|
||||
ruby file
|
||||
end
|
||||
end
|
||||
|
||||
task :expression do
|
||||
Dir["./performance/unit/expression_benchmark.rb"].each do |file|
|
||||
puts "🧪 Running #{file}"
|
||||
ruby file
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
namespace :profile do
|
||||
|
@ -21,6 +21,8 @@
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
require "strscan"
|
||||
|
||||
module Liquid
|
||||
FilterSeparator = /\|/
|
||||
ArgumentSeparator = ','
|
||||
@ -44,13 +46,21 @@ module Liquid
|
||||
VariableParser = /\[(?>[^\[\]]+|\g<0>)*\]|#{VariableSegment}+\??/o
|
||||
|
||||
RAISE_EXCEPTION_LAMBDA = ->(_e) { raise }
|
||||
|
||||
singleton_class.send(:attr_accessor, :cache_classes)
|
||||
self.cache_classes = true
|
||||
HAS_STRING_SCANNER_SCAN_BYTE = StringScanner.instance_methods.include?(:scan_byte)
|
||||
end
|
||||
|
||||
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/interrupts'
|
||||
require 'liquid/tags'
|
||||
require "liquid/environment"
|
||||
require 'liquid/lexer'
|
||||
require 'liquid/parser'
|
||||
require 'liquid/i18n'
|
||||
@ -61,23 +71,16 @@ require 'liquid/extensions'
|
||||
require 'liquid/errors'
|
||||
require 'liquid/interrupts'
|
||||
require 'liquid/strainer_template'
|
||||
require 'liquid/strainer_factory'
|
||||
require 'liquid/expression'
|
||||
require 'liquid/context'
|
||||
require 'liquid/parser_switching'
|
||||
require 'liquid/tag'
|
||||
require 'liquid/tag/disabler'
|
||||
require 'liquid/tag/disableable'
|
||||
require 'liquid/block'
|
||||
require 'liquid/block_body'
|
||||
require 'liquid/document'
|
||||
require 'liquid/variable'
|
||||
require 'liquid/variable_lookup'
|
||||
require 'liquid/range_lookup'
|
||||
require 'liquid/file_system'
|
||||
require 'liquid/resource_limits'
|
||||
require 'liquid/expression'
|
||||
require 'liquid/template'
|
||||
require 'liquid/standardfilters'
|
||||
require 'liquid/condition'
|
||||
require 'liquid/utils'
|
||||
require 'liquid/tokenizer'
|
||||
@ -86,7 +89,3 @@ require 'liquid/partial_cache'
|
||||
require 'liquid/usage'
|
||||
require 'liquid/registers'
|
||||
require 'liquid/template_factory'
|
||||
|
||||
# Load all the tags of the standard library
|
||||
#
|
||||
Dir["#{__dir__}/liquid/tags/*.rb"].each { |f| require f }
|
||||
|
@ -6,6 +6,7 @@ module Liquid
|
||||
class BlockBody
|
||||
LiquidTagToken = /\A\s*(#{TagName})\s*(.*?)\z/o
|
||||
FullToken = /\A#{TagStart}#{WhitespaceControl}?(\s*)(#{TagName})(\s*)(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
|
||||
FullTokenPossiblyInvalid = /\A(.*)#{TagStart}#{WhitespaceControl}?\s*(\w+)\s*(.*)?#{WhitespaceControl}?#{TagEnd}\z/om
|
||||
ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
|
||||
WhitespaceOrNothing = /\A\s*\z/
|
||||
TAGSTART = "{%"
|
||||
@ -51,7 +52,7 @@ module Liquid
|
||||
next parse_liquid_tag(markup, parse_context)
|
||||
end
|
||||
|
||||
unless (tag = registered_tags[tag_name])
|
||||
unless (tag = parse_context.environment.tag_for_name(tag_name))
|
||||
# end parsing if we reach an unknown tag and let the caller decide
|
||||
# determine how to proceed
|
||||
return yield tag_name, markup
|
||||
@ -146,7 +147,7 @@ module Liquid
|
||||
next
|
||||
end
|
||||
|
||||
unless (tag = registered_tags[tag_name])
|
||||
unless (tag = parse_context.environment.tag_for_name(tag_name))
|
||||
# end parsing if we reach an unknown tag and let the caller decide
|
||||
# determine how to proceed
|
||||
return yield tag_name, markup
|
||||
@ -245,10 +246,17 @@ module Liquid
|
||||
end
|
||||
|
||||
def create_variable(token, parse_context)
|
||||
if token =~ ContentOfVariable
|
||||
markup = Regexp.last_match(1)
|
||||
if token.end_with?("}}")
|
||||
i = 2
|
||||
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)
|
||||
end
|
||||
|
||||
BlockBody.raise_missing_variable_terminator(token, parse_context)
|
||||
end
|
||||
|
||||
@ -261,9 +269,5 @@ module Liquid
|
||||
def raise_missing_variable_terminator(token, parse_context)
|
||||
BlockBody.raise_missing_variable_terminator(token, parse_context)
|
||||
end
|
||||
|
||||
def registered_tags
|
||||
Template.tags
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -24,6 +24,9 @@ module Liquid
|
||||
else
|
||||
false
|
||||
end
|
||||
rescue Encoding::CompatibilityError
|
||||
# "✅".b.include?("✅") raises Encoding::CompatibilityError despite being materially equal
|
||||
left.b.include?(right.b)
|
||||
end,
|
||||
}
|
||||
|
||||
|
8
lib/liquid/const.rb
Normal file
8
lib/liquid/const.rb
Normal file
@ -0,0 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
module Const
|
||||
EMPTY_HASH = {}.freeze
|
||||
EMPTY_ARRAY = [].freeze
|
||||
end
|
||||
end
|
@ -15,35 +15,40 @@ module Liquid
|
||||
# context['bob'] #=> nil class Context
|
||||
class Context
|
||||
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
|
||||
attr_accessor :exception_renderer, :template_name, :partial, :global_filter, :strict_variables, :strict_filters, :environment
|
||||
|
||||
# rubocop:disable Metrics/ParameterLists
|
||||
def self.build(environments: {}, outer_scope: {}, registers: {}, rethrow_errors: false, resource_limits: nil, static_environments: {}, &block)
|
||||
new(environments, outer_scope, registers, rethrow_errors, resource_limits, static_environments, &block)
|
||||
def self.build(environment: Environment.default, environments: {}, outer_scope: {}, registers: {}, rethrow_errors: false, resource_limits: nil, static_environments: {}, &block)
|
||||
new(environments, outer_scope, registers, rethrow_errors, resource_limits, static_environments, environment, &block)
|
||||
end
|
||||
|
||||
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil, static_environments = {})
|
||||
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil, static_environments = {}, environment = Environment.default)
|
||||
@environment = environment
|
||||
@environments = [environments]
|
||||
@environments.flatten!
|
||||
|
||||
@static_environments = [static_environments].flatten(1).freeze
|
||||
@scopes = [(outer_scope || {})]
|
||||
@scopes = [outer_scope || {}]
|
||||
@registers = registers.is_a?(Registers) ? registers : Registers.new(registers)
|
||||
@errors = []
|
||||
@partial = false
|
||||
@strict_variables = false
|
||||
@resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
|
||||
@resource_limits = resource_limits || ResourceLimits.new(environment.default_resource_limits)
|
||||
@base_scope_depth = 0
|
||||
@interrupts = []
|
||||
@filters = []
|
||||
@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] ||= Liquid::Template.file_system
|
||||
@registers.static[:file_system] ||= environment.file_system
|
||||
@registers.static[:template_factory] ||= Liquid::TemplateFactory.new
|
||||
|
||||
self.exception_renderer = Template.default_exception_renderer
|
||||
self.exception_renderer = environment.exception_renderer
|
||||
if rethrow_errors
|
||||
self.exception_renderer = Liquid::RAISE_EXCEPTION_LAMBDA
|
||||
end
|
||||
@ -60,7 +65,7 @@ module Liquid
|
||||
end
|
||||
|
||||
def strainer
|
||||
@strainer ||= StrainerFactory.create(self, @filters)
|
||||
@strainer ||= @environment.create_strainer(self, @filters)
|
||||
end
|
||||
|
||||
# Adds filters to this context.
|
||||
@ -142,6 +147,7 @@ module Liquid
|
||||
check_overflow
|
||||
|
||||
self.class.build(
|
||||
environment: @environment,
|
||||
resource_limits: resource_limits,
|
||||
static_environments: static_environments,
|
||||
registers: Registers.new(registers),
|
||||
@ -174,7 +180,7 @@ module Liquid
|
||||
# Example:
|
||||
# products == empty #=> products.empty?
|
||||
def [](expression)
|
||||
evaluate(Expression.parse(expression))
|
||||
evaluate(Expression.parse(expression, @string_scanner))
|
||||
end
|
||||
|
||||
def key?(key)
|
||||
@ -197,10 +203,14 @@ module Liquid
|
||||
try_variable_find_in_environments(key, raise_on_not_found: raise_on_not_found)
|
||||
end
|
||||
|
||||
variable = variable.to_liquid
|
||||
# update variable's context before invoking #to_liquid
|
||||
variable.context = self if variable.respond_to?(:context=)
|
||||
|
||||
variable
|
||||
liquid_variable = variable.to_liquid
|
||||
|
||||
liquid_variable.context = self if variable != liquid_variable && liquid_variable.respond_to?(:context=)
|
||||
|
||||
liquid_variable
|
||||
end
|
||||
|
||||
def lookup_and_evaluate(obj, key, raise_on_not_found: true)
|
||||
|
22
lib/liquid/deprecations.rb
Normal file
22
lib/liquid/deprecations.rb
Normal file
@ -0,0 +1,22 @@
|
||||
# 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
|
159
lib/liquid/environment.rb
Normal file
159
lib/liquid/environment.rb
Normal file
@ -0,0 +1,159 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# The Environment is the container for all configuration options of Liquid, such as
|
||||
# the registered tags, filters, and the default error mode.
|
||||
class Environment
|
||||
# The default error mode for all templates. This can be overridden on a
|
||||
# per-template basis.
|
||||
attr_accessor :error_mode
|
||||
|
||||
# The tags that are available to use in the template.
|
||||
attr_accessor :tags
|
||||
|
||||
# The strainer template which is used to store filters that are available to
|
||||
# use in templates.
|
||||
attr_accessor :strainer_template
|
||||
|
||||
# The exception renderer that is used to render exceptions that are raised
|
||||
# when rendering a template
|
||||
attr_accessor :exception_renderer
|
||||
|
||||
# The default file system that is used to load templates from.
|
||||
attr_accessor :file_system
|
||||
|
||||
# The default resource limits that are used to limit the resources that a
|
||||
# template can consume.
|
||||
attr_accessor :default_resource_limits
|
||||
|
||||
class << self
|
||||
# Creates a new environment instance.
|
||||
#
|
||||
# @param tags [Hash] The tags that are available to use in
|
||||
# the template.
|
||||
# @param file_system The default file system that is used
|
||||
# to load templates from.
|
||||
# @param error_mode [Symbol] The default error mode for all templates
|
||||
# (either :strict, :warn, or :lax).
|
||||
# @param exception_renderer [Proc] The exception renderer that is used to
|
||||
# render exceptions.
|
||||
# @yieldparam environment [Environment] The environment instance that is being built.
|
||||
# @return [Environment] The new environment instance.
|
||||
def build(tags: nil, file_system: nil, error_mode: nil, exception_renderer: nil)
|
||||
ret = new
|
||||
ret.tags = tags if tags
|
||||
ret.file_system = file_system if file_system
|
||||
ret.error_mode = error_mode if error_mode
|
||||
ret.exception_renderer = exception_renderer if exception_renderer
|
||||
yield ret if block_given?
|
||||
ret.freeze
|
||||
end
|
||||
|
||||
# Returns the default environment instance.
|
||||
#
|
||||
# @return [Environment] The default environment instance.
|
||||
def default
|
||||
@default ||= new
|
||||
end
|
||||
|
||||
# Sets the default environment instance for the duration of the block
|
||||
#
|
||||
# @param environment [Environment] The environment instance to use as the default for the
|
||||
# duration of the block.
|
||||
# @yield
|
||||
# @return [Object] The return value of the block.
|
||||
def dangerously_override(environment)
|
||||
original_default = @default
|
||||
@default = environment
|
||||
yield
|
||||
ensure
|
||||
@default = original_default
|
||||
end
|
||||
end
|
||||
|
||||
# Initializes a new environment instance.
|
||||
# @api private
|
||||
def initialize
|
||||
@tags = Tags::STANDARD_TAGS.dup
|
||||
@error_mode = :lax
|
||||
@strainer_template = Class.new(StrainerTemplate).tap do |klass|
|
||||
klass.add_filter(StandardFilters)
|
||||
end
|
||||
@exception_renderer = ->(exception) { exception }
|
||||
@file_system = BlankFileSystem.new
|
||||
@default_resource_limits = Const::EMPTY_HASH
|
||||
@strainer_template_class_cache = {}
|
||||
end
|
||||
|
||||
# Registers a new tag with the environment.
|
||||
#
|
||||
# @param name [String] The name of the tag.
|
||||
# @param klass [Liquid::Tag] The class that implements the tag.
|
||||
# @return [void]
|
||||
def register_tag(name, klass)
|
||||
@tags[name] = klass
|
||||
end
|
||||
|
||||
# Registers a new filter with the environment.
|
||||
#
|
||||
# @param filter [Module] The module that contains the filter methods.
|
||||
# @return [void]
|
||||
def register_filter(filter)
|
||||
@strainer_template_class_cache.clear
|
||||
@strainer_template.add_filter(filter)
|
||||
end
|
||||
|
||||
# Registers multiple filters with this environment.
|
||||
#
|
||||
# @param filters [Array<Module>] The modules that contain the filter methods.
|
||||
# @return [self]
|
||||
def register_filters(filters)
|
||||
@strainer_template_class_cache.clear
|
||||
filters.each { |f| @strainer_template.add_filter(f) }
|
||||
self
|
||||
end
|
||||
|
||||
# Creates a new strainer instance with the given filters, caching the result
|
||||
# for faster lookup.
|
||||
#
|
||||
# @param context [Liquid::Context] The context that the strainer will be
|
||||
# used in.
|
||||
# @param filters [Array<Module>] The filters that the strainer will have
|
||||
# access to.
|
||||
# @return [Liquid::Strainer] The new strainer instance.
|
||||
def create_strainer(context, filters = Const::EMPTY_ARRAY)
|
||||
return @strainer_template.new(context) if filters.empty?
|
||||
|
||||
strainer_template = @strainer_template_class_cache[filters] ||= begin
|
||||
klass = Class.new(@strainer_template)
|
||||
filters.each { |f| klass.add_filter(f) }
|
||||
klass
|
||||
end
|
||||
|
||||
strainer_template.new(context)
|
||||
end
|
||||
|
||||
# Returns the names of all the filter methods that are available to use in
|
||||
# the strainer template.
|
||||
#
|
||||
# @return [Array<String>] The names of all the filter methods.
|
||||
def filter_method_names
|
||||
@strainer_template.filter_method_names
|
||||
end
|
||||
|
||||
# Returns the tag class for the given tag name.
|
||||
#
|
||||
# @param name [String] The name of the tag.
|
||||
# @return [Liquid::Tag] The tag class.
|
||||
def tag_for_name(name)
|
||||
@tags[name]
|
||||
end
|
||||
|
||||
def freeze
|
||||
@tags.freeze
|
||||
# TODO: freeze the tags, currently this is not possible because of liquid-c
|
||||
# @strainer_template.freeze
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
@ -40,19 +40,20 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
ArgumentError = Class.new(Error)
|
||||
ContextError = Class.new(Error)
|
||||
FileSystemError = Class.new(Error)
|
||||
StandardError = Class.new(Error)
|
||||
SyntaxError = Class.new(Error)
|
||||
StackLevelError = Class.new(Error)
|
||||
MemoryError = Class.new(Error)
|
||||
ZeroDivisionError = Class.new(Error)
|
||||
FloatDomainError = Class.new(Error)
|
||||
UndefinedVariable = Class.new(Error)
|
||||
UndefinedDropMethod = Class.new(Error)
|
||||
UndefinedFilter = Class.new(Error)
|
||||
MethodOverrideError = Class.new(Error)
|
||||
DisabledError = Class.new(Error)
|
||||
InternalError = Class.new(Error)
|
||||
ArgumentError = Class.new(Error)
|
||||
ContextError = Class.new(Error)
|
||||
FileSystemError = Class.new(Error)
|
||||
StandardError = Class.new(Error)
|
||||
SyntaxError = Class.new(Error)
|
||||
StackLevelError = Class.new(Error)
|
||||
MemoryError = Class.new(Error)
|
||||
ZeroDivisionError = Class.new(Error)
|
||||
FloatDomainError = Class.new(Error)
|
||||
UndefinedVariable = Class.new(Error)
|
||||
UndefinedDropMethod = Class.new(Error)
|
||||
UndefinedFilter = Class.new(Error)
|
||||
MethodOverrideError = Class.new(Error)
|
||||
DisabledError = Class.new(Error)
|
||||
InternalError = Class.new(Error)
|
||||
TemplateEncodingError = Class.new(Error)
|
||||
end
|
||||
|
@ -10,37 +10,113 @@ module Liquid
|
||||
'true' => true,
|
||||
'false' => false,
|
||||
'blank' => '',
|
||||
'empty' => ''
|
||||
'empty' => '',
|
||||
# in lax mode, minus sign can be a VariableLookup
|
||||
# For simplicity and performace, we treat it like a literal
|
||||
'-' => VariableLookup.parse("-", nil).freeze,
|
||||
}.freeze
|
||||
|
||||
INTEGERS_REGEX = /\A(-?\d+)\z/
|
||||
FLOATS_REGEX = /\A(-?\d[\d\.]+)\z/
|
||||
DOT = ".".ord
|
||||
ZERO = "0".ord
|
||||
NINE = "9".ord
|
||||
DASH = "-".ord
|
||||
|
||||
# Use an atomic group (?>...) to avoid pathological backtracing from
|
||||
# malicious input as described in https://github.com/Shopify/liquid/issues/1357
|
||||
RANGES_REGEX = /\A\(\s*(?>(\S+)\s*\.\.)\s*(\S+)\s*\)\z/
|
||||
RANGES_REGEX = /\A\(\s*(?>(\S+)\s*\.\.)\s*(\S+)\s*\)\z/
|
||||
INTEGER_REGEX = /\A(-?\d+)\z/
|
||||
FLOAT_REGEX = /\A(-?\d+)\.\d+\z/
|
||||
|
||||
def self.parse(markup)
|
||||
return nil unless markup
|
||||
class << self
|
||||
def parse(markup, ss = StringScanner.new(""), cache = nil)
|
||||
return unless markup
|
||||
|
||||
markup = markup.strip
|
||||
if (markup.start_with?('"') && markup.end_with?('"')) ||
|
||||
(markup.start_with?("'") && markup.end_with?("'"))
|
||||
return markup[1..-2]
|
||||
markup = markup.strip # markup can be a frozen string
|
||||
|
||||
if (markup.start_with?('"') && markup.end_with?('"')) ||
|
||||
(markup.start_with?("'") && markup.end_with?("'"))
|
||||
return markup[1..-2]
|
||||
elsif LITERALS.key?(markup)
|
||||
return LITERALS[markup]
|
||||
end
|
||||
|
||||
# Cache only exists during parsing
|
||||
if cache
|
||||
return cache[markup] if cache.key?(markup)
|
||||
|
||||
cache[markup] = inner_parse(markup, ss, cache).freeze
|
||||
else
|
||||
inner_parse(markup, ss, nil).freeze
|
||||
end
|
||||
end
|
||||
|
||||
case markup
|
||||
when INTEGERS_REGEX
|
||||
Regexp.last_match(1).to_i
|
||||
when RANGES_REGEX
|
||||
RangeLookup.parse(Regexp.last_match(1), Regexp.last_match(2))
|
||||
when FLOATS_REGEX
|
||||
Regexp.last_match(1).to_f
|
||||
else
|
||||
if LITERALS.key?(markup)
|
||||
LITERALS[markup]
|
||||
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)
|
||||
VariableLookup.parse(markup, ss, cache)
|
||||
end
|
||||
end
|
||||
|
||||
def parse_number(markup, ss)
|
||||
# check if the markup is simple integer or float
|
||||
case markup
|
||||
when INTEGER_REGEX
|
||||
return Integer(markup, 10)
|
||||
when FLOAT_REGEX
|
||||
return markup.to_f
|
||||
end
|
||||
|
||||
ss.string = markup
|
||||
# the first byte must be a digit or a dash
|
||||
byte = ss.scan_byte
|
||||
|
||||
return false if byte != DASH && (byte < ZERO || byte > NINE)
|
||||
|
||||
if byte == DASH
|
||||
peek_byte = ss.peek_byte
|
||||
|
||||
# if it starts with a dash, the next byte must be a digit
|
||||
return false if peek_byte.nil? || !(peek_byte >= ZERO && peek_byte <= NINE)
|
||||
end
|
||||
|
||||
# The markup could be a float with multiple dots
|
||||
first_dot_pos = nil
|
||||
num_end_pos = nil
|
||||
|
||||
while (byte = ss.scan_byte)
|
||||
return false if byte != DOT && (byte < ZERO || byte > NINE)
|
||||
|
||||
# we found our number and now we are just scanning the rest of the string
|
||||
next if num_end_pos
|
||||
|
||||
if byte == DOT
|
||||
if first_dot_pos.nil?
|
||||
first_dot_pos = ss.pos
|
||||
else
|
||||
# we found another dot, so we know that the number ends here
|
||||
num_end_pos = ss.pos - 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
num_end_pos = markup.length if ss.eos?
|
||||
|
||||
if num_end_pos
|
||||
# number ends with a number "123.123"
|
||||
markup.byteslice(0, num_end_pos).to_f
|
||||
else
|
||||
# number ends with a dot "123."
|
||||
markup.byteslice(0, first_dot_pos).to_f
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -1,61 +1,179 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "strscan"
|
||||
module Liquid
|
||||
class Lexer
|
||||
SPECIALS = {
|
||||
'|' => :pipe,
|
||||
'.' => :dot,
|
||||
':' => :colon,
|
||||
',' => :comma,
|
||||
'[' => :open_square,
|
||||
']' => :close_square,
|
||||
'(' => :open_round,
|
||||
')' => :close_round,
|
||||
'?' => :question,
|
||||
'-' => :dash,
|
||||
}.freeze
|
||||
IDENTIFIER = /[a-zA-Z_][\w-]*\??/
|
||||
SINGLE_STRING_LITERAL = /'[^\']*'/
|
||||
CLOSE_ROUND = [:close_round, ")"].freeze
|
||||
CLOSE_SQUARE = [:close_square, "]"].freeze
|
||||
COLON = [:colon, ":"].freeze
|
||||
COMMA = [:comma, ","].freeze
|
||||
COMPARISION_NOT_EQUAL = [:comparison, "!="].freeze
|
||||
COMPARISON_CONTAINS = [:comparison, "contains"].freeze
|
||||
COMPARISON_EQUAL = [:comparison, "=="].freeze
|
||||
COMPARISON_GREATER_THAN = [:comparison, ">"].freeze
|
||||
COMPARISON_GREATER_THAN_OR_EQUAL = [:comparison, ">="].freeze
|
||||
COMPARISON_LESS_THAN = [:comparison, "<"].freeze
|
||||
COMPARISON_LESS_THAN_OR_EQUAL = [:comparison, "<="].freeze
|
||||
COMPARISON_NOT_EQUAL_ALT = [:comparison, "<>"].freeze
|
||||
DASH = [:dash, "-"].freeze
|
||||
DOT = [:dot, "."].freeze
|
||||
DOTDOT = [:dotdot, ".."].freeze
|
||||
DOT_ORD = ".".ord
|
||||
DOUBLE_STRING_LITERAL = /"[^\"]*"/
|
||||
STRING_LITERAL = Regexp.union(SINGLE_STRING_LITERAL, DOUBLE_STRING_LITERAL)
|
||||
EOS = [:end_of_string].freeze
|
||||
IDENTIFIER = /[a-zA-Z_][\w-]*\??/
|
||||
NUMBER_LITERAL = /-?\d+(\.\d+)?/
|
||||
DOTDOT = /\.\./
|
||||
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/
|
||||
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 = /'[^\']*'/
|
||||
WHITESPACE_OR_NOTHING = /\s*/
|
||||
|
||||
def initialize(input)
|
||||
@ss = StringScanner.new(input)
|
||||
SINGLE_COMPARISON_TOKENS = [].tap do |table|
|
||||
table["<".ord] = COMPARISON_LESS_THAN
|
||||
table[">".ord] = COMPARISON_GREATER_THAN
|
||||
table.freeze
|
||||
end
|
||||
|
||||
def tokenize
|
||||
@output = []
|
||||
TWO_CHARS_COMPARISON_JUMP_TABLE = [].tap do |table|
|
||||
table["=".ord] = [].tap do |sub_table|
|
||||
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
|
||||
|
||||
until @ss.eos?
|
||||
@ss.skip(WHITESPACE_OR_NOTHING)
|
||||
break if @ss.eos?
|
||||
tok = if (t = @ss.scan(COMPARISON_OPERATOR))
|
||||
[:comparison, t]
|
||||
elsif (t = @ss.scan(STRING_LITERAL))
|
||||
[:string, t]
|
||||
elsif (t = @ss.scan(NUMBER_LITERAL))
|
||||
[:number, t]
|
||||
elsif (t = @ss.scan(IDENTIFIER))
|
||||
[:id, t]
|
||||
elsif (t = @ss.scan(DOTDOT))
|
||||
[:dotdot, t]
|
||||
else
|
||||
c = @ss.getch
|
||||
if (s = SPECIALS[c])
|
||||
[s, c]
|
||||
COMPARISON_JUMP_TABLE = [].tap do |table|
|
||||
table["<".ord] = [].tap do |sub_table|
|
||||
sub_table["=".ord] = COMPARISON_LESS_THAN_OR_EQUAL
|
||||
sub_table[">".ord] = COMPARISON_NOT_EQUAL_ALT
|
||||
sub_table.freeze
|
||||
end
|
||||
table[">".ord] = [].tap do |sub_table|
|
||||
sub_table["=".ord] = COMPARISON_GREATER_THAN_OR_EQUAL
|
||||
sub_table.freeze
|
||||
end
|
||||
table.freeze
|
||||
end
|
||||
|
||||
NEXT_MATCHER_JUMP_TABLE = [].tap do |table|
|
||||
"a".upto("z") do |c|
|
||||
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
|
||||
output << special
|
||||
end
|
||||
else
|
||||
output << special
|
||||
end
|
||||
elsif (sub_table = TWO_CHARS_COMPARISON_JUMP_TABLE[peeked])
|
||||
ss.scan_byte
|
||||
if (peeked_byte = ss.peek_byte) && (found = sub_table[peeked_byte])
|
||||
output << found
|
||||
ss.scan_byte
|
||||
else
|
||||
raise_syntax_error(start_pos, ss)
|
||||
end
|
||||
elsif (sub_table = COMPARISON_JUMP_TABLE[peeked])
|
||||
ss.scan_byte
|
||||
if (peeked_byte = ss.peek_byte) && (found = sub_table[peeked_byte])
|
||||
output << found
|
||||
ss.scan_byte
|
||||
else
|
||||
output << SINGLE_COMPARISON_TOKENS[peeked]
|
||||
end
|
||||
else
|
||||
raise SyntaxError, "Unexpected character #{c}"
|
||||
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
|
||||
@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
|
||||
|
||||
@output << [:end_of_string]
|
||||
def raise_syntax_error(start_pos, ss)
|
||||
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
|
||||
|
@ -2,12 +2,14 @@
|
||||
errors:
|
||||
syntax:
|
||||
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]"
|
||||
capture: "Syntax Error in 'capture' - Valid syntax: capture [var]"
|
||||
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_else: "Syntax Error in tag 'case' - Valid else condition: {% else %} (no parameters) "
|
||||
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_invalid_in: "For loops require an 'in' clause"
|
||||
for_invalid_attribute: "Invalid attribute in for loop. Valid attributes are limit and offset"
|
||||
@ -15,6 +17,7 @@
|
||||
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"
|
||||
invalid_delimiter: "'%{tag}' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}"
|
||||
invalid_template_encoding: "Invalid template encoding"
|
||||
render: "Syntax error in tag 'render' - Template name must be a quoted string"
|
||||
table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
|
||||
tag_never_closed: "'%{block_name}' tag was never closed"
|
||||
|
@ -3,14 +3,27 @@
|
||||
module Liquid
|
||||
class ParseContext
|
||||
attr_accessor :locale, :line_number, :trim_whitespace, :depth
|
||||
attr_reader :partial, :warnings, :error_mode
|
||||
attr_reader :partial, :warnings, :error_mode, :environment
|
||||
|
||||
def initialize(options = {})
|
||||
def initialize(options = Const::EMPTY_HASH)
|
||||
@environment = options.fetch(:environment, Environment.default)
|
||||
@template_options = options ? options.dup : {}
|
||||
|
||||
@locale = @template_options[:locale] ||= I18n.new
|
||||
@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.partial = false
|
||||
end
|
||||
@ -23,19 +36,29 @@ module Liquid
|
||||
Liquid::BlockBody.new
|
||||
end
|
||||
|
||||
def new_tokenizer(markup, start_line_number: nil, for_liquid_tag: false)
|
||||
Tokenizer.new(markup, line_number: start_line_number, for_liquid_tag: for_liquid_tag)
|
||||
def new_parser(input)
|
||||
@string_scanner.string = input
|
||||
Parser.new(@string_scanner)
|
||||
end
|
||||
|
||||
def new_tokenizer(source, start_line_number: nil, for_liquid_tag: false)
|
||||
Tokenizer.new(
|
||||
source: source,
|
||||
string_scanner: @string_scanner,
|
||||
line_number: start_line_number,
|
||||
for_liquid_tag: for_liquid_tag,
|
||||
)
|
||||
end
|
||||
|
||||
def parse_expression(markup)
|
||||
Expression.parse(markup)
|
||||
Expression.parse(markup, @string_scanner, @expression_cache)
|
||||
end
|
||||
|
||||
def partial=(value)
|
||||
@partial = value
|
||||
@options = value ? partial_options : @template_options
|
||||
|
||||
@error_mode = @options[:error_mode] || Template.error_mode
|
||||
@error_mode = @options[:error_mode] || @environment.error_mode
|
||||
end
|
||||
|
||||
def partial_options
|
||||
|
@ -36,7 +36,7 @@ module Liquid
|
||||
protected
|
||||
|
||||
def children
|
||||
@node.respond_to?(:nodelist) ? Array(@node.nodelist) : []
|
||||
@node.respond_to?(:nodelist) ? Array(@node.nodelist) : Const::EMPTY_ARRAY
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -3,8 +3,8 @@
|
||||
module Liquid
|
||||
class Parser
|
||||
def initialize(input)
|
||||
l = Lexer.new(input)
|
||||
@tokens = l.tokenize
|
||||
ss = input.is_a?(StringScanner) ? input : StringScanner.new(input)
|
||||
@tokens = Lexer.tokenize(ss)
|
||||
@p = 0 # pointer to current location
|
||||
end
|
||||
|
||||
@ -53,7 +53,7 @@ module Liquid
|
||||
str = consume
|
||||
str << variable_lookups
|
||||
when :open_square
|
||||
str = consume
|
||||
str = consume.dup
|
||||
str << expression
|
||||
str << consume(:close_square)
|
||||
str << variable_lookups
|
||||
|
@ -2,9 +2,9 @@
|
||||
|
||||
module Liquid
|
||||
class RangeLookup
|
||||
def self.parse(start_markup, end_markup)
|
||||
start_obj = Expression.parse(start_markup)
|
||||
end_obj = Expression.parse(end_markup)
|
||||
def self.parse(start_markup, end_markup, string_scanner, cache = nil)
|
||||
start_obj = Expression.parse(start_markup, string_scanner, cache)
|
||||
end_obj = Expression.parse(end_markup, string_scanner, cache)
|
||||
if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate)
|
||||
new(start_obj, end_obj)
|
||||
else
|
||||
|
@ -3,7 +3,6 @@
|
||||
require 'cgi'
|
||||
require 'base64'
|
||||
require 'bigdecimal'
|
||||
|
||||
module Liquid
|
||||
module StandardFilters
|
||||
MAX_I32 = (1 << 31) - 1
|
||||
@ -29,6 +28,19 @@ module Liquid
|
||||
)
|
||||
STRIP_HTML_TAGS = /<.*?>/m
|
||||
|
||||
class << self
|
||||
def try_coerce_encoding(input, encoding:)
|
||||
original_encoding = input.encoding
|
||||
if input.encoding != encoding
|
||||
input.force_encoding(encoding)
|
||||
unless input.valid_encoding?
|
||||
input.force_encoding(original_encoding)
|
||||
end
|
||||
end
|
||||
input
|
||||
end
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category array
|
||||
@ -51,7 +63,7 @@ module Liquid
|
||||
# @liquid_syntax string | downcase
|
||||
# @liquid_return [string]
|
||||
def downcase(input)
|
||||
input.to_s.downcase
|
||||
Utils.to_s(input).downcase
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
@ -62,7 +74,7 @@ module Liquid
|
||||
# @liquid_syntax string | upcase
|
||||
# @liquid_return [string]
|
||||
def upcase(input)
|
||||
input.to_s.upcase
|
||||
Utils.to_s(input).upcase
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
@ -73,7 +85,7 @@ module Liquid
|
||||
# @liquid_syntax string | capitalize
|
||||
# @liquid_return [string]
|
||||
def capitalize(input)
|
||||
input.to_s.capitalize
|
||||
Utils.to_s(input).capitalize
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
@ -84,7 +96,7 @@ module Liquid
|
||||
# @liquid_syntax string | escape
|
||||
# @liquid_return [string]
|
||||
def escape(input)
|
||||
CGI.escapeHTML(input.to_s) unless input.nil?
|
||||
CGI.escapeHTML(Utils.to_s(input)) unless input.nil?
|
||||
end
|
||||
alias_method :h, :escape
|
||||
|
||||
@ -96,7 +108,7 @@ module Liquid
|
||||
# @liquid_syntax string | escape_once
|
||||
# @liquid_return [string]
|
||||
def escape_once(input)
|
||||
input.to_s.gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE)
|
||||
Utils.to_s(input).gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE)
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
@ -111,7 +123,7 @@ module Liquid
|
||||
# @liquid_syntax string | url_encode
|
||||
# @liquid_return [string]
|
||||
def url_encode(input)
|
||||
CGI.escape(input.to_s) unless input.nil?
|
||||
CGI.escape(Utils.to_s(input)) unless input.nil?
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
@ -125,7 +137,7 @@ module Liquid
|
||||
def url_decode(input)
|
||||
return if input.nil?
|
||||
|
||||
result = CGI.unescape(input.to_s)
|
||||
result = CGI.unescape(Utils.to_s(input))
|
||||
raise Liquid::ArgumentError, "invalid byte sequence in #{result.encoding}" unless result.valid_encoding?
|
||||
|
||||
result
|
||||
@ -139,7 +151,7 @@ module Liquid
|
||||
# @liquid_syntax string | base64_encode
|
||||
# @liquid_return [string]
|
||||
def base64_encode(input)
|
||||
Base64.strict_encode64(input.to_s)
|
||||
Base64.strict_encode64(Utils.to_s(input))
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
@ -150,7 +162,8 @@ module Liquid
|
||||
# @liquid_syntax string | base64_decode
|
||||
# @liquid_return [string]
|
||||
def base64_decode(input)
|
||||
Base64.strict_decode64(input.to_s)
|
||||
input = Utils.to_s(input)
|
||||
StandardFilters.try_coerce_encoding(Base64.strict_decode64(input), encoding: input.encoding)
|
||||
rescue ::ArgumentError
|
||||
raise Liquid::ArgumentError, "invalid base64 provided to base64_decode"
|
||||
end
|
||||
@ -163,7 +176,7 @@ module Liquid
|
||||
# @liquid_syntax string | base64_url_safe_encode
|
||||
# @liquid_return [string]
|
||||
def base64_url_safe_encode(input)
|
||||
Base64.urlsafe_encode64(input.to_s)
|
||||
Base64.urlsafe_encode64(Utils.to_s(input))
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
@ -174,7 +187,8 @@ module Liquid
|
||||
# @liquid_syntax string | base64_url_safe_decode
|
||||
# @liquid_return [string]
|
||||
def base64_url_safe_decode(input)
|
||||
Base64.urlsafe_decode64(input.to_s)
|
||||
input = Utils.to_s(input)
|
||||
StandardFilters.try_coerce_encoding(Base64.urlsafe_decode64(input), encoding: input.encoding)
|
||||
rescue ::ArgumentError
|
||||
raise Liquid::ArgumentError, "invalid base64 provided to base64_url_safe_decode"
|
||||
end
|
||||
@ -197,7 +211,7 @@ module Liquid
|
||||
if input.is_a?(Array)
|
||||
input.slice(offset, length) || []
|
||||
else
|
||||
input.to_s.slice(offset, length) || ''
|
||||
Utils.to_s(input).slice(offset, length) || ''
|
||||
end
|
||||
rescue RangeError
|
||||
if I64_RANGE.cover?(length) && I64_RANGE.cover?(offset)
|
||||
@ -221,10 +235,10 @@ module Liquid
|
||||
# @liquid_return [string]
|
||||
def truncate(input, length = 50, truncate_string = "...")
|
||||
return if input.nil?
|
||||
input_str = input.to_s
|
||||
input_str = Utils.to_s(input)
|
||||
length = Utils.to_integer(length)
|
||||
|
||||
truncate_string_str = truncate_string.to_s
|
||||
truncate_string_str = Utils.to_s(truncate_string)
|
||||
|
||||
l = length - truncate_string_str.length
|
||||
l = 0 if l < 0
|
||||
@ -248,7 +262,7 @@ module Liquid
|
||||
# @liquid_return [string]
|
||||
def truncatewords(input, words = 15, truncate_string = "...")
|
||||
return if input.nil?
|
||||
input = input.to_s
|
||||
input = Utils.to_s(input)
|
||||
words = Utils.to_integer(words)
|
||||
words = 1 if words <= 0
|
||||
|
||||
@ -262,7 +276,8 @@ module Liquid
|
||||
return input if wordlist.length <= words
|
||||
|
||||
wordlist.pop
|
||||
wordlist.join(" ").concat(truncate_string.to_s)
|
||||
truncate_string = Utils.to_s(truncate_string)
|
||||
wordlist.join(" ").concat(truncate_string)
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
@ -273,7 +288,9 @@ module Liquid
|
||||
# @liquid_syntax string | split: string
|
||||
# @liquid_return [array[string]]
|
||||
def split(input, pattern)
|
||||
input.to_s.split(pattern.to_s)
|
||||
pattern = Utils.to_s(pattern)
|
||||
input = Utils.to_s(input)
|
||||
input.split(pattern)
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
@ -284,7 +301,8 @@ module Liquid
|
||||
# @liquid_syntax string | strip
|
||||
# @liquid_return [string]
|
||||
def strip(input)
|
||||
input.to_s.strip
|
||||
input = Utils.to_s(input)
|
||||
input.strip
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
@ -295,7 +313,8 @@ module Liquid
|
||||
# @liquid_syntax string | lstrip
|
||||
# @liquid_return [string]
|
||||
def lstrip(input)
|
||||
input.to_s.lstrip
|
||||
input = Utils.to_s(input)
|
||||
input.lstrip
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
@ -306,7 +325,8 @@ module Liquid
|
||||
# @liquid_syntax string | rstrip
|
||||
# @liquid_return [string]
|
||||
def rstrip(input)
|
||||
input.to_s.rstrip
|
||||
input = Utils.to_s(input)
|
||||
input.rstrip
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
@ -317,8 +337,9 @@ module Liquid
|
||||
# @liquid_syntax string | strip_html
|
||||
# @liquid_return [string]
|
||||
def strip_html(input)
|
||||
input = Utils.to_s(input)
|
||||
empty = ''
|
||||
result = input.to_s.gsub(STRIP_HTML_BLOCKS, empty)
|
||||
result = input.gsub(STRIP_HTML_BLOCKS, empty)
|
||||
result.gsub!(STRIP_HTML_TAGS, empty)
|
||||
result
|
||||
end
|
||||
@ -331,7 +352,8 @@ module Liquid
|
||||
# @liquid_syntax string | strip_newlines
|
||||
# @liquid_return [string]
|
||||
def strip_newlines(input)
|
||||
input.to_s.gsub(/\r?\n/, '')
|
||||
input = Utils.to_s(input)
|
||||
input.gsub(/\r?\n/, '')
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
@ -342,6 +364,7 @@ module Liquid
|
||||
# @liquid_syntax array | join
|
||||
# @liquid_return [string]
|
||||
def join(input, glue = ' ')
|
||||
glue = Utils.to_s(glue)
|
||||
InputIterator.new(input, context).join(glue)
|
||||
end
|
||||
|
||||
@ -363,6 +386,7 @@ module Liquid
|
||||
end
|
||||
elsif ary.all? { |el| el.respond_to?(:[]) }
|
||||
begin
|
||||
property = Utils.to_s(property)
|
||||
ary.sort { |a, b| nil_safe_compare(a[property], b[property]) }
|
||||
rescue TypeError
|
||||
raise_property_error(property)
|
||||
@ -392,6 +416,7 @@ module Liquid
|
||||
end
|
||||
elsif ary.all? { |el| el.respond_to?(:[]) }
|
||||
begin
|
||||
property = Utils.to_s(property)
|
||||
ary.sort { |a, b| nil_safe_casecmp(a[property], b[property]) }
|
||||
rescue TypeError
|
||||
raise_property_error(property)
|
||||
@ -409,29 +434,59 @@ module Liquid
|
||||
# @liquid_syntax array | where: string, string
|
||||
# @liquid_return [array[untyped]]
|
||||
def where(input, property, target_value = nil)
|
||||
ary = InputIterator.new(input, context)
|
||||
filter_array(input, property, target_value) { |ary, &block| ary.select(&block) }
|
||||
end
|
||||
|
||||
if ary.empty?
|
||||
[]
|
||||
elsif target_value.nil?
|
||||
ary.select do |item|
|
||||
item[property]
|
||||
rescue TypeError
|
||||
raise_property_error(property)
|
||||
rescue NoMethodError
|
||||
return nil unless item.respond_to?(:[])
|
||||
raise
|
||||
end
|
||||
else
|
||||
ary.select do |item|
|
||||
item[property] == target_value
|
||||
rescue TypeError
|
||||
raise_property_error(property)
|
||||
rescue NoMethodError
|
||||
return nil unless item.respond_to?(:[])
|
||||
raise
|
||||
end
|
||||
end
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category array
|
||||
# @liquid_summary
|
||||
# Filters an array to exclude items with a specific property value.
|
||||
# @liquid_description
|
||||
# This requires you to provide both the property name and the associated value.
|
||||
# @liquid_syntax array | reject: string, string
|
||||
# @liquid_return [array[untyped]]
|
||||
def reject(input, property, target_value = nil)
|
||||
filter_array(input, property, target_value) { |ary, &block| ary.reject(&block) }
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category array
|
||||
# @liquid_summary
|
||||
# Tests if any item in an array has a specific property value.
|
||||
# @liquid_description
|
||||
# This requires you to provide both the property name and the associated value.
|
||||
# @liquid_syntax array | has: string, string
|
||||
# @liquid_return [boolean]
|
||||
def has(input, property, target_value = nil)
|
||||
filter_array(input, property, target_value, false) { |ary, &block| ary.any?(&block) }
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category array
|
||||
# @liquid_summary
|
||||
# Returns the first item in an array with a specific property value.
|
||||
# @liquid_description
|
||||
# This requires you to provide both the property name and the associated value.
|
||||
# @liquid_syntax array | find: string, string
|
||||
# @liquid_return [untyped]
|
||||
def find(input, property, target_value = nil)
|
||||
filter_array(input, property, target_value, nil) { |ary, &block| ary.find(&block) }
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category array
|
||||
# @liquid_summary
|
||||
# Returns the index of the first item in an array with a specific property value.
|
||||
# @liquid_description
|
||||
# This requires you to provide both the property name and the associated value.
|
||||
# @liquid_syntax array | find_index: string, string
|
||||
# @liquid_return [number]
|
||||
def find_index(input, property, target_value = nil)
|
||||
filter_array(input, property, target_value, nil) { |ary, &block| ary.find_index(&block) }
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
@ -449,6 +504,7 @@ module Liquid
|
||||
elsif ary.empty? # The next two cases assume a non-empty array.
|
||||
[]
|
||||
else
|
||||
property = Utils.to_s(property)
|
||||
ary.uniq do |item|
|
||||
item[property]
|
||||
rescue TypeError
|
||||
@ -480,6 +536,11 @@ module Liquid
|
||||
# @liquid_syntax array | map: string
|
||||
# @liquid_return [array[untyped]]
|
||||
def map(input, property)
|
||||
property = Utils.to_s(property)
|
||||
|
||||
# Return the input array if property is empty (no-op)
|
||||
return InputIterator.new(input, context).to_a if property.empty?
|
||||
|
||||
InputIterator.new(input, context).map do |e|
|
||||
e = e.call if e.is_a?(Proc)
|
||||
|
||||
@ -509,6 +570,7 @@ module Liquid
|
||||
elsif ary.empty? # The next two cases assume a non-empty array.
|
||||
[]
|
||||
else
|
||||
property = Liquid::Utils.to_s(property)
|
||||
ary.reject do |item|
|
||||
item[property].nil?
|
||||
rescue TypeError
|
||||
@ -528,7 +590,10 @@ module Liquid
|
||||
# @liquid_syntax string | replace: string, string
|
||||
# @liquid_return [string]
|
||||
def replace(input, string, replacement = '')
|
||||
input.to_s.gsub(string.to_s, replacement.to_s)
|
||||
string = Utils.to_s(string)
|
||||
replacement = Utils.to_s(replacement)
|
||||
input = Utils.to_s(input)
|
||||
input.gsub(string, replacement)
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
@ -539,7 +604,10 @@ module Liquid
|
||||
# @liquid_syntax string | replace_first: string, string
|
||||
# @liquid_return [string]
|
||||
def replace_first(input, string, replacement = '')
|
||||
input.to_s.sub(string.to_s, replacement.to_s)
|
||||
string = Utils.to_s(string)
|
||||
replacement = Utils.to_s(replacement)
|
||||
input = Utils.to_s(input)
|
||||
input.sub(string, replacement)
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
@ -550,9 +618,9 @@ module Liquid
|
||||
# @liquid_syntax string | replace_last: string, string
|
||||
# @liquid_return [string]
|
||||
def replace_last(input, string, replacement)
|
||||
input = input.to_s
|
||||
string = string.to_s
|
||||
replacement = replacement.to_s
|
||||
input = Utils.to_s(input)
|
||||
string = Utils.to_s(string)
|
||||
replacement = Utils.to_s(replacement)
|
||||
|
||||
start_index = input.rindex(string)
|
||||
|
||||
@ -604,7 +672,9 @@ module Liquid
|
||||
# @liquid_syntax string | append: string
|
||||
# @liquid_return [string]
|
||||
def append(input, string)
|
||||
input.to_s + string.to_s
|
||||
input = Utils.to_s(input)
|
||||
string = Utils.to_s(string)
|
||||
input + string
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
@ -633,7 +703,9 @@ module Liquid
|
||||
# @liquid_syntax string | prepend: string
|
||||
# @liquid_return [string]
|
||||
def prepend(input, string)
|
||||
string.to_s + input.to_s
|
||||
input = Utils.to_s(input)
|
||||
string = Utils.to_s(string)
|
||||
string + input
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
@ -644,10 +716,20 @@ module Liquid
|
||||
# @liquid_syntax string | newline_to_br
|
||||
# @liquid_return [string]
|
||||
def newline_to_br(input)
|
||||
input.to_s.gsub(/\r?\n/, "<br />\n")
|
||||
input = Utils.to_s(input)
|
||||
input.gsub(/\r?\n/, "<br />\n")
|
||||
end
|
||||
|
||||
# Reformat a date using Ruby's core Time#strftime( string ) -> string
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category date
|
||||
# @liquid_summary
|
||||
# Formats a date according to a specified format string.
|
||||
# @liquid_description
|
||||
# This filter formats a date using various format specifiers. If the format string is empty,
|
||||
# the original input is returned. If the input cannot be converted to a date, the original input is returned.
|
||||
#
|
||||
# The following format specifiers can be used:
|
||||
#
|
||||
# %a - The abbreviated weekday name (``Sun'')
|
||||
# %A - The full weekday name (``Sunday'')
|
||||
@ -676,14 +758,15 @@ module Liquid
|
||||
# %Y - Year with century
|
||||
# %Z - Time zone name
|
||||
# %% - Literal ``%'' character
|
||||
#
|
||||
# See also: http://www.ruby-doc.org/core/Time.html#method-i-strftime
|
||||
# @liquid_syntax date | date: string
|
||||
# @liquid_return [string]
|
||||
def date(input, format)
|
||||
return input if format.to_s.empty?
|
||||
str_format = Utils.to_s(format)
|
||||
return input if str_format.empty?
|
||||
|
||||
return input unless (date = Utils.to_date(input))
|
||||
|
||||
date.strftime(format.to_s)
|
||||
date.strftime(str_format)
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
@ -862,7 +945,7 @@ module Liquid
|
||||
# - [`nil`](/docs/api/liquid/basics#nil)
|
||||
# @liquid_syntax variable | default: variable
|
||||
# @liquid_return [untyped]
|
||||
# @liquid_optional_param allow_false [boolean] Whether to use false values instead of the default.
|
||||
# @liquid_optional_param allow_false: [boolean] Whether to use false values instead of the default.
|
||||
def default(input, default_value = '', options = {})
|
||||
options = {} unless options.is_a?(Hash)
|
||||
false_check = options['allow_false'] ? input.nil? : !Liquid::Utils.to_liquid_value(input)
|
||||
@ -877,6 +960,8 @@ module Liquid
|
||||
# @liquid_syntax array | sum
|
||||
# @liquid_return [number]
|
||||
def sum(input, property = nil)
|
||||
property = property.nil? ? nil : Utils.to_s(property)
|
||||
|
||||
ary = InputIterator.new(input, context)
|
||||
return 0 if ary.empty?
|
||||
|
||||
@ -892,15 +977,37 @@ module Liquid
|
||||
raise_property_error(property)
|
||||
end
|
||||
|
||||
InputIterator.new(values_for_sum, context).sum do |item|
|
||||
result = InputIterator.new(values_for_sum, context).sum do |item|
|
||||
Utils.to_number(item)
|
||||
end
|
||||
|
||||
result.is_a?(BigDecimal) ? result.to_f : result
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :context
|
||||
|
||||
def filter_array(input, property, target_value, default_value = [], &block)
|
||||
ary = InputIterator.new(input, context)
|
||||
return default_value if ary.empty?
|
||||
|
||||
property = Utils.to_s(property)
|
||||
|
||||
block.call(ary) do |item|
|
||||
if target_value.nil?
|
||||
item[property]
|
||||
else
|
||||
item[property] == target_value
|
||||
end
|
||||
rescue TypeError
|
||||
raise_property_error(property)
|
||||
rescue NoMethodError
|
||||
return nil unless item.respond_to?(:[])
|
||||
raise
|
||||
end
|
||||
end
|
||||
|
||||
def raise_property_error(property)
|
||||
raise Liquid::ArgumentError, "cannot select the property '#{property}'"
|
||||
end
|
||||
@ -927,6 +1034,8 @@ module Liquid
|
||||
def nil_safe_casecmp(a, b)
|
||||
if !a.nil? && !b.nil?
|
||||
a.to_s.casecmp(b.to_s)
|
||||
elsif a.nil? && b.nil?
|
||||
0
|
||||
else
|
||||
a.nil? ? 1 : -1
|
||||
end
|
||||
@ -949,7 +1058,18 @@ module Liquid
|
||||
end
|
||||
|
||||
def join(glue)
|
||||
to_a.join(glue.to_s)
|
||||
first = true
|
||||
output = +""
|
||||
each do |item|
|
||||
if first
|
||||
first = false
|
||||
else
|
||||
output << glue
|
||||
end
|
||||
|
||||
output << Liquid::Utils.to_s(item)
|
||||
end
|
||||
output
|
||||
end
|
||||
|
||||
def concat(args)
|
||||
@ -961,7 +1081,10 @@ module Liquid
|
||||
end
|
||||
|
||||
def uniq(&block)
|
||||
to_a.uniq(&block)
|
||||
to_a.uniq do |item|
|
||||
item = Utils.to_liquid_value(item)
|
||||
block ? yield(item) : item
|
||||
end
|
||||
end
|
||||
|
||||
def compact
|
||||
@ -982,6 +1105,4 @@ module Liquid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_filter(StandardFilters)
|
||||
end
|
||||
|
@ -1,41 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# StrainerFactory is the factory for the filters system.
|
||||
module StrainerFactory
|
||||
extend self
|
||||
|
||||
def add_global_filter(filter)
|
||||
strainer_class_cache.clear
|
||||
GlobalCache.add_filter(filter)
|
||||
end
|
||||
|
||||
def create(context, filters = [])
|
||||
strainer_from_cache(filters).new(context)
|
||||
end
|
||||
|
||||
def global_filter_names
|
||||
GlobalCache.filter_method_names
|
||||
end
|
||||
|
||||
GlobalCache = Class.new(StrainerTemplate)
|
||||
|
||||
private
|
||||
|
||||
def strainer_from_cache(filters)
|
||||
if filters.empty?
|
||||
GlobalCache
|
||||
else
|
||||
strainer_class_cache[filters] ||= begin
|
||||
klass = Class.new(GlobalCache)
|
||||
filters.each { |f| klass.add_filter(f) }
|
||||
klass
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def strainer_class_cache
|
||||
@strainer_class_cache ||= {}
|
||||
end
|
||||
end
|
||||
end
|
@ -1,5 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'liquid/tag/disabler'
|
||||
require 'liquid/tag/disableable'
|
||||
|
||||
module Liquid
|
||||
class Tag
|
||||
attr_reader :nodelist, :tag_name, :line_number, :parse_context
|
||||
@ -54,7 +57,8 @@ module Liquid
|
||||
# of the `render_to_output_buffer` method will become the default and the `render`
|
||||
# method will be removed.
|
||||
def render_to_output_buffer(context, output)
|
||||
output << render(context)
|
||||
render_result = render(context)
|
||||
output << render_result if render_result
|
||||
output
|
||||
end
|
||||
|
||||
|
49
lib/liquid/tags.rb
Normal file
49
lib/liquid/tags.rb
Normal file
@ -0,0 +1,49 @@
|
||||
# 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
|
@ -72,6 +72,4 @@ module Liquid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('assign', Assign)
|
||||
end
|
||||
|
@ -26,6 +26,4 @@ module Liquid
|
||||
output
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('break', Break)
|
||||
end
|
||||
|
@ -39,6 +39,4 @@ module Liquid
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('capture', Capture)
|
||||
end
|
||||
|
@ -123,6 +123,4 @@ module Liquid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('case', Case)
|
||||
end
|
||||
|
@ -25,7 +25,64 @@ module Liquid
|
||||
def blank?
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('comment', Comment)
|
||||
private
|
||||
|
||||
def parse_body(body, tokenizer)
|
||||
if parse_context.depth >= MAX_DEPTH
|
||||
raise StackLevelError, "Nesting too deep"
|
||||
end
|
||||
|
||||
parse_context.depth += 1
|
||||
comment_tag_depth = 1
|
||||
|
||||
begin
|
||||
# Consume tokens without creating child nodes.
|
||||
# The children tag doesn't require to be a valid Liquid except the comment and raw tag.
|
||||
# The child comment and raw tag must be closed.
|
||||
while (token = tokenizer.send(:shift))
|
||||
tag_name = if tokenizer.for_liquid_tag
|
||||
next if token.empty? || token.match?(BlockBody::WhitespaceOrNothing)
|
||||
|
||||
tag_name_match = BlockBody::LiquidTagToken.match(token)
|
||||
|
||||
next if tag_name_match.nil?
|
||||
|
||||
tag_name_match[1]
|
||||
else
|
||||
token =~ BlockBody::FullToken
|
||||
Regexp.last_match(2)
|
||||
end
|
||||
|
||||
case tag_name
|
||||
when "raw"
|
||||
parse_raw_tag_body(tokenizer)
|
||||
when "comment"
|
||||
comment_tag_depth += 1
|
||||
when "endcomment"
|
||||
comment_tag_depth -= 1
|
||||
end
|
||||
|
||||
if comment_tag_depth.zero?
|
||||
parse_context.trim_whitespace = (token[-3] == WhitespaceControl) unless tokenizer.for_liquid_tag
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
raise_tag_never_closed(block_name)
|
||||
ensure
|
||||
parse_context.depth -= 1
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def parse_raw_tag_body(tokenizer)
|
||||
while (token = tokenizer.send(:shift))
|
||||
return if token =~ BlockBody::FullTokenPossiblyInvalid && "endraw" == Regexp.last_match(2)
|
||||
end
|
||||
|
||||
raise_tag_never_closed("raw")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -17,6 +17,4 @@ module Liquid
|
||||
output
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('continue', Continue)
|
||||
end
|
||||
|
@ -26,14 +26,20 @@ module Liquid
|
||||
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?(/\w+:0x\h{8}/)
|
||||
else
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.cycle")
|
||||
end
|
||||
end
|
||||
|
||||
def named?
|
||||
@is_named
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
context.registers[:cycle] ||= {}
|
||||
|
||||
@ -62,7 +68,13 @@ module Liquid
|
||||
def variables_from_string(markup)
|
||||
markup.split(',').collect do |var|
|
||||
var =~ /\s*(#{QuotedFragment})\s*/o
|
||||
Regexp.last_match(1) ? parse_expression(Regexp.last_match(1)) : nil
|
||||
next unless Regexp.last_match(1)
|
||||
|
||||
# Expression Parser returns cached objects, and we need to dup them to
|
||||
# start the cycle over for each new cycle call.
|
||||
# Liquid-C does not have a cache, so we don't need to dup the object.
|
||||
var = parse_expression(Regexp.last_match(1))
|
||||
var.is_a?(VariableLookup) ? var.dup : var
|
||||
end.compact
|
||||
end
|
||||
|
||||
@ -72,6 +84,4 @@ module Liquid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('cycle', Cycle)
|
||||
end
|
||||
|
@ -10,7 +10,7 @@ module Liquid
|
||||
# @liquid_description
|
||||
# Variables that are declared with `decrement` are unique to the [layout](/themes/architecture/layouts), [template](/themes/architecture/templates),
|
||||
# or [section](/themes/architecture/sections) file that they're created in. However, the variable is shared across
|
||||
# [snippets](/themes/architecture#snippets) included in the file.
|
||||
# [snippets](/themes/architecture/snippets) included in the file.
|
||||
#
|
||||
# Similarly, variables that are created with `decrement` are independent from those created with [`assign`](/docs/api/liquid/tags/assign)
|
||||
# and [`capture`](/docs/api/liquid/tags/capture). However, `decrement` and [`increment`](/docs/api/liquid/tags/increment) share
|
||||
@ -35,6 +35,4 @@ module Liquid
|
||||
output
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('decrement', Decrement)
|
||||
end
|
||||
|
78
lib/liquid/tags/doc.rb
Normal file
78
lib/liquid/tags/doc.rb
Normal file
@ -0,0 +1,78 @@
|
||||
# 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 %}
|
||||
# {{ foo }}, {{ bar }}!
|
||||
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)
|
||||
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)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
raise_tag_never_closed(block_name)
|
||||
end
|
||||
|
||||
def render_to_output_buffer(_context, output)
|
||||
output
|
||||
end
|
||||
|
||||
def blank?
|
||||
true
|
||||
end
|
||||
|
||||
def nodelist
|
||||
[]
|
||||
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
|
@ -36,6 +36,4 @@ module Liquid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('echo', Echo)
|
||||
end
|
||||
|
@ -88,7 +88,7 @@ module Liquid
|
||||
end
|
||||
|
||||
def strict_parse(markup)
|
||||
p = Parser.new(markup)
|
||||
p = @parse_context.new_parser(markup)
|
||||
@variable_name = p.consume(:id)
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in") unless p.id?('in')
|
||||
|
||||
@ -201,6 +201,4 @@ module Liquid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('for', For)
|
||||
end
|
||||
|
@ -102,7 +102,7 @@ module Liquid
|
||||
end
|
||||
|
||||
def strict_parse(markup)
|
||||
p = Parser.new(markup)
|
||||
p = @parse_context.new_parser(markup)
|
||||
condition = parse_binary_comparisons(p)
|
||||
p.consume(:end_of_string)
|
||||
condition
|
||||
@ -111,7 +111,7 @@ module Liquid
|
||||
def parse_binary_comparisons(p)
|
||||
condition = parse_comparison(p)
|
||||
first_condition = condition
|
||||
while (op = (p.id?('and') || p.id?('or')))
|
||||
while (op = p.id?('and') || p.id?('or'))
|
||||
child_condition = parse_comparison(p)
|
||||
condition.send(op, child_condition)
|
||||
condition = child_condition
|
||||
@ -135,6 +135,4 @@ module Liquid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('if', If)
|
||||
end
|
||||
|
@ -14,6 +14,4 @@ module Liquid
|
||||
output
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('ifchanged', Ifchanged)
|
||||
end
|
||||
|
@ -6,7 +6,7 @@ module Liquid
|
||||
# @liquid_category theme
|
||||
# @liquid_name include
|
||||
# @liquid_summary
|
||||
# Renders a [snippet](/themes/architecture#snippets).
|
||||
# Renders a [snippet](/themes/architecture/snippets).
|
||||
# @liquid_description
|
||||
# Inside the snippet, you can access and alter variables that are [created](/docs/api/liquid/tags/variable-tags) outside of the
|
||||
# snippet.
|
||||
@ -110,6 +110,4 @@ module Liquid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('include', Include)
|
||||
end
|
||||
|
@ -10,7 +10,7 @@ module Liquid
|
||||
# @liquid_description
|
||||
# Variables that are declared with `increment` are unique to the [layout](/themes/architecture/layouts), [template](/themes/architecture/templates),
|
||||
# or [section](/themes/architecture/sections) file that they're created in. However, the variable is shared across
|
||||
# [snippets](/themes/architecture#snippets) included in the file.
|
||||
# [snippets](/themes/architecture/snippets) included in the file.
|
||||
#
|
||||
# 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
|
||||
@ -35,6 +35,4 @@ module Liquid
|
||||
output
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('increment', Increment)
|
||||
end
|
||||
|
@ -25,6 +25,4 @@ module Liquid
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('#', InlineComment)
|
||||
end
|
||||
|
@ -14,7 +14,6 @@ module Liquid
|
||||
# @liquid_syntax_keyword expression The expression to be output without being rendered.
|
||||
class Raw < Block
|
||||
Syntax = /\A\s*\z/
|
||||
FullTokenPossiblyInvalid = /\A(.*)#{TagStart}#{WhitespaceControl}?\s*(\w+)\s*(.*)?#{WhitespaceControl}?#{TagEnd}\z/om
|
||||
|
||||
def initialize(tag_name, markup, parse_context)
|
||||
super
|
||||
@ -25,7 +24,7 @@ module Liquid
|
||||
def parse(tokens)
|
||||
@body = +''
|
||||
while (token = tokens.shift)
|
||||
if token =~ FullTokenPossiblyInvalid && block_delimiter == Regexp.last_match(2)
|
||||
if token =~ BlockBody::FullTokenPossiblyInvalid && block_delimiter == Regexp.last_match(2)
|
||||
parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
|
||||
@body << Regexp.last_match(1) if Regexp.last_match(1) != ""
|
||||
return
|
||||
@ -57,6 +56,4 @@ module Liquid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('raw', Raw)
|
||||
end
|
||||
|
@ -6,7 +6,7 @@ module Liquid
|
||||
# @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).
|
||||
# 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)
|
||||
@ -108,6 +108,4 @@ module Liquid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('render', Render)
|
||||
end
|
||||
|
@ -65,6 +65,12 @@ module Liquid
|
||||
super
|
||||
output << '</td>'
|
||||
|
||||
# Handle any interrupts if they exist.
|
||||
if context.interrupt?
|
||||
interrupt = context.pop_interrupt
|
||||
break if interrupt.is_a?(BreakInterrupt)
|
||||
end
|
||||
|
||||
if tablerowloop.col_last && !tablerowloop.last
|
||||
output << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">"
|
||||
end
|
||||
@ -91,6 +97,4 @@ module Liquid
|
||||
raise Liquid::ArgumentError, "invalid integer"
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('tablerow', TableRow)
|
||||
end
|
||||
|
@ -44,6 +44,4 @@ module Liquid
|
||||
output
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('unless', Unless)
|
||||
end
|
||||
|
@ -18,42 +18,6 @@ module Liquid
|
||||
attr_accessor :root, :name
|
||||
attr_reader :resource_limits, :warnings
|
||||
|
||||
class TagRegistry
|
||||
include Enumerable
|
||||
|
||||
def initialize
|
||||
@tags = {}
|
||||
@cache = {}
|
||||
end
|
||||
|
||||
def [](tag_name)
|
||||
return nil unless @tags.key?(tag_name)
|
||||
return @cache[tag_name] if Liquid.cache_classes
|
||||
|
||||
lookup_class(@tags[tag_name]).tap { |o| @cache[tag_name] = o }
|
||||
end
|
||||
|
||||
def []=(tag_name, klass)
|
||||
@tags[tag_name] = klass.name
|
||||
@cache[tag_name] = klass
|
||||
end
|
||||
|
||||
def delete(tag_name)
|
||||
@tags.delete(tag_name)
|
||||
@cache.delete(tag_name)
|
||||
end
|
||||
|
||||
def each(&block)
|
||||
@tags.each(&block)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def lookup_class(name)
|
||||
Object.const_get(name)
|
||||
end
|
||||
end
|
||||
|
||||
attr_reader :profiler
|
||||
|
||||
class << self
|
||||
@ -61,52 +25,83 @@ module Liquid
|
||||
# :lax acts like liquid 2.5 and silently ignores malformed tags in most cases.
|
||||
# :warn is the default and will give deprecation warnings when invalid syntax is used.
|
||||
# :strict will enforce correct syntax.
|
||||
attr_accessor :error_mode
|
||||
Template.error_mode = :lax
|
||||
|
||||
attr_accessor :default_exception_renderer
|
||||
Template.default_exception_renderer = lambda do |exception|
|
||||
exception
|
||||
def error_mode=(mode)
|
||||
Deprecations.warn("Template.error_mode=", "Environment#error_mode=")
|
||||
Environment.default.error_mode = mode
|
||||
end
|
||||
|
||||
attr_accessor :file_system
|
||||
Template.file_system = BlankFileSystem.new
|
||||
def error_mode
|
||||
Environment.default.error_mode
|
||||
end
|
||||
|
||||
attr_accessor :tags
|
||||
Template.tags = TagRegistry.new
|
||||
private :tags=
|
||||
def default_exception_renderer=(renderer)
|
||||
Deprecations.warn("Template.default_exception_renderer=", "Environment#exception_renderer=")
|
||||
Environment.default.exception_renderer = renderer
|
||||
end
|
||||
|
||||
def default_exception_renderer
|
||||
Environment.default.exception_renderer
|
||||
end
|
||||
|
||||
def file_system=(file_system)
|
||||
Deprecations.warn("Template.file_system=", "Environment#file_system=")
|
||||
Environment.default.file_system = file_system
|
||||
end
|
||||
|
||||
def file_system
|
||||
Environment.default.file_system
|
||||
end
|
||||
|
||||
def tags
|
||||
Environment.default.tags
|
||||
end
|
||||
|
||||
def register_tag(name, klass)
|
||||
tags[name.to_s] = klass
|
||||
Deprecations.warn("Template.register_tag", "Environment#register_tag")
|
||||
Environment.default.register_tag(name, klass)
|
||||
end
|
||||
|
||||
# Pass a module with filter methods which should be available
|
||||
# to all liquid views. Good for registering the standard library
|
||||
def register_filter(mod)
|
||||
StrainerFactory.add_global_filter(mod)
|
||||
Deprecations.warn("Template.register_filter", "Environment#register_filter")
|
||||
Environment.default.register_filter(mod)
|
||||
end
|
||||
|
||||
attr_accessor :default_resource_limits
|
||||
Template.default_resource_limits = {}
|
||||
private :default_resource_limits=
|
||||
private def default_resource_limits=(limits)
|
||||
Deprecations.warn("Template.default_resource_limits=", "Environment#default_resource_limits=")
|
||||
Environment.default.default_resource_limits = limits
|
||||
end
|
||||
|
||||
def default_resource_limits
|
||||
Environment.default.default_resource_limits
|
||||
end
|
||||
|
||||
# creates a new <tt>Template</tt> object from liquid source code
|
||||
# To enable profiling, pass in <tt>profile: true</tt> as an option.
|
||||
# See Liquid::Profiler for more information
|
||||
def parse(source, options = {})
|
||||
new.parse(source, options)
|
||||
environment = options[:environment] || Environment.default
|
||||
new(environment: environment).parse(source, options)
|
||||
end
|
||||
end
|
||||
|
||||
def initialize
|
||||
def initialize(environment: Environment.default)
|
||||
@environment = environment
|
||||
@rethrow_errors = false
|
||||
@resource_limits = ResourceLimits.new(Template.default_resource_limits)
|
||||
@resource_limits = ResourceLimits.new(environment.default_resource_limits)
|
||||
end
|
||||
|
||||
# Parse source code.
|
||||
# Returns self for easy chaining
|
||||
def parse(source, options = {})
|
||||
parse_context = configure_options(options)
|
||||
source = source.to_s.to_str
|
||||
|
||||
unless source.valid_encoding?
|
||||
raise TemplateEncodingError, parse_context.locale.t("errors.syntax.invalid_template_encoding")
|
||||
end
|
||||
|
||||
tokenizer = parse_context.new_tokenizer(source, start_line_number: @line_numbers && 1)
|
||||
@root = Document.parse(tokenizer, parse_context)
|
||||
self
|
||||
@ -156,11 +151,11 @@ module Liquid
|
||||
c
|
||||
when Liquid::Drop
|
||||
drop = args.shift
|
||||
drop.context = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits)
|
||||
drop.context = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits, {}, @environment)
|
||||
when Hash
|
||||
Context.new([args.shift, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits)
|
||||
Context.new([args.shift, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits, {}, @environment)
|
||||
when nil
|
||||
Context.new(assigns, instance_assigns, registers, @rethrow_errors, @resource_limits)
|
||||
Context.new(assigns, instance_assigns, registers, @rethrow_errors, @resource_limits, {}, @environment)
|
||||
else
|
||||
raise ArgumentError, "Expected Hash or Liquid::Context as parameter"
|
||||
end
|
||||
@ -220,8 +215,14 @@ module Liquid
|
||||
@options = options
|
||||
@profiling = profiling
|
||||
@line_numbers = options[:line_numbers] || @profiling
|
||||
parse_context = options.is_a?(ParseContext) ? options : ParseContext.new(options)
|
||||
@warnings = parse_context.warnings
|
||||
parse_context = if options.is_a?(ParseContext)
|
||||
options
|
||||
else
|
||||
opts = options.key?(:environment) ? options : options.merge(environment: @environment)
|
||||
ParseContext.new(opts)
|
||||
end
|
||||
|
||||
@warnings = parse_context.warnings
|
||||
parse_context
|
||||
end
|
||||
|
||||
|
@ -1,20 +1,43 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "strscan"
|
||||
|
||||
module Liquid
|
||||
class Tokenizer
|
||||
attr_reader :line_number, :for_liquid_tag
|
||||
|
||||
def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false)
|
||||
@source = source.to_s.to_str
|
||||
@line_number = line_number || (line_numbers ? 1 : nil)
|
||||
TAG_END = /%\}/
|
||||
TAG_OR_VARIABLE_START = /\{[\{\%]/
|
||||
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)
|
||||
@for_liquid_tag = for_liquid_tag
|
||||
@offset = 0
|
||||
@tokens = tokenize
|
||||
@source = source.to_s.to_str
|
||||
@offset = 0
|
||||
@tokens = []
|
||||
|
||||
if @source
|
||||
@ss = string_scanner
|
||||
@ss.string = @source
|
||||
tokenize
|
||||
end
|
||||
end
|
||||
|
||||
def shift
|
||||
token = @tokens[@offset]
|
||||
return nil unless token
|
||||
|
||||
return unless token
|
||||
|
||||
@offset += 1
|
||||
|
||||
@ -28,18 +51,111 @@ module Liquid
|
||||
private
|
||||
|
||||
def tokenize
|
||||
return [] if @source.empty?
|
||||
|
||||
return @source.split("\n") if @for_liquid_tag
|
||||
|
||||
tokens = @source.split(TemplateParser)
|
||||
|
||||
# removes the rogue empty element at the beginning of the array
|
||||
if tokens[0]&.empty?
|
||||
@offset += 1
|
||||
if @for_liquid_tag
|
||||
@tokens = @source.split("\n")
|
||||
else
|
||||
@tokens << shift_normal until @ss.eos?
|
||||
end
|
||||
|
||||
tokens
|
||||
@source = nil
|
||||
@ss = nil
|
||||
end
|
||||
|
||||
def shift_normal
|
||||
token = next_token
|
||||
|
||||
return unless token
|
||||
|
||||
token
|
||||
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
|
||||
|
@ -89,5 +89,101 @@ module Liquid
|
||||
# Otherwise return the object itself
|
||||
obj
|
||||
end
|
||||
|
||||
def self.to_s(obj, seen = {})
|
||||
case obj
|
||||
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
|
||||
|
@ -61,14 +61,14 @@ module Liquid
|
||||
|
||||
def strict_parse(markup)
|
||||
@filters = []
|
||||
p = Parser.new(markup)
|
||||
p = @parse_context.new_parser(markup)
|
||||
|
||||
return if p.look(:end_of_string)
|
||||
|
||||
@name = parse_context.parse_expression(p.expression)
|
||||
while p.consume?(:pipe)
|
||||
filtername = p.consume(:id)
|
||||
filterargs = p.consume?(:colon) ? parse_filterargs(p) : []
|
||||
filterargs = p.consume?(:colon) ? parse_filterargs(p) : Const::EMPTY_ARRAY
|
||||
@filters << parse_filter_expressions(filtername, filterargs)
|
||||
end
|
||||
p.consume(:end_of_string)
|
||||
@ -95,17 +95,23 @@ module Liquid
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
obj = render(context)
|
||||
|
||||
if obj.is_a?(Array)
|
||||
output << obj.join
|
||||
elsif obj.nil?
|
||||
else
|
||||
output << obj.to_s
|
||||
end
|
||||
|
||||
render_obj_to_output(obj, output)
|
||||
output
|
||||
end
|
||||
|
||||
def render_obj_to_output(obj, 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
|
||||
|
||||
def disabled?(_context)
|
||||
false
|
||||
end
|
||||
|
@ -6,16 +6,20 @@ module Liquid
|
||||
|
||||
attr_reader :name, :lookups
|
||||
|
||||
def self.parse(markup)
|
||||
new(markup)
|
||||
def self.parse(markup, string_scanner = StringScanner.new(""), cache = nil)
|
||||
new(markup, string_scanner, cache)
|
||||
end
|
||||
|
||||
def initialize(markup)
|
||||
def initialize(markup, string_scanner = StringScanner.new(""), cache = nil)
|
||||
lookups = markup.scan(VariableParser)
|
||||
|
||||
name = lookups.shift
|
||||
if name&.start_with?('[') && name&.end_with?(']')
|
||||
name = Expression.parse(name[1..-2])
|
||||
name = Expression.parse(
|
||||
name[1..-2],
|
||||
string_scanner,
|
||||
cache,
|
||||
)
|
||||
end
|
||||
@name = name
|
||||
|
||||
@ -25,7 +29,11 @@ module Liquid
|
||||
@lookups.each_index do |i|
|
||||
lookup = lookups[i]
|
||||
if lookup&.start_with?('[') && lookup&.end_with?(']')
|
||||
lookups[i] = Expression.parse(lookup[1..-2])
|
||||
lookups[i] = Expression.parse(
|
||||
lookup[1..-2],
|
||||
string_scanner,
|
||||
cache,
|
||||
)
|
||||
elsif COMMAND_METHODS.include?(lookup)
|
||||
@command_flags |= 1 << i
|
||||
end
|
||||
|
@ -2,5 +2,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
VERSION = "5.4.0"
|
||||
VERSION = "5.8.4"
|
||||
end
|
||||
|
@ -13,11 +13,11 @@ Gem::Specification.new do |s|
|
||||
s.summary = "A secure, non-evaling end user template engine with aesthetic markup."
|
||||
s.authors = ["Tobias Lütke"]
|
||||
s.email = ["tobi@leetsoft.com"]
|
||||
s.homepage = "http://www.liquidmarkup.org"
|
||||
s.homepage = "https://shopify.github.io/liquid/"
|
||||
s.license = "MIT"
|
||||
# s.description = "A secure, non-evaling end user template engine with aesthetic markup."
|
||||
|
||||
s.required_ruby_version = ">= 2.7.0"
|
||||
s.required_ruby_version = ">= 3.0.0"
|
||||
s.required_rubygems_version = ">= 1.3.7"
|
||||
|
||||
s.metadata['allowed_push_host'] = 'https://rubygems.org'
|
||||
@ -28,6 +28,9 @@ Gem::Specification.new do |s|
|
||||
|
||||
s.require_path = "lib"
|
||||
|
||||
s.add_dependency("strscan", ">= 3.1.1")
|
||||
s.add_dependency("bigdecimal")
|
||||
|
||||
s.add_development_dependency('rake', '~> 13.0')
|
||||
s.add_development_dependency('minitest')
|
||||
end
|
||||
|
@ -3,18 +3,23 @@
|
||||
require 'benchmark/ips'
|
||||
require_relative 'theme_runner'
|
||||
|
||||
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
|
||||
RubyVM::YJIT.enable if defined?(RubyVM::YJIT)
|
||||
Liquid::Environment.default.error_mode = ARGV.first.to_sym if ARGV.first
|
||||
|
||||
profiler = ThemeRunner.new
|
||||
|
||||
Benchmark.ips do |x|
|
||||
x.time = 10
|
||||
x.warmup = 5
|
||||
x.time = 20
|
||||
x.warmup = 10
|
||||
|
||||
puts
|
||||
puts "Running benchmark for #{x.time} seconds (with #{x.warmup} seconds warmup)."
|
||||
puts
|
||||
|
||||
x.report("parse:") { profiler.compile }
|
||||
x.report("render:") { profiler.render }
|
||||
x.report("parse & render:") { profiler.run }
|
||||
phase = ENV["PHASE"] || "all"
|
||||
|
||||
x.report("tokenize:") { profiler.tokenize } if phase == "all" || phase == "tokenize"
|
||||
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
|
||||
|
@ -11,11 +11,12 @@ require_relative 'shop_filter'
|
||||
require_relative 'tag_filter'
|
||||
require_relative 'weight_filter'
|
||||
|
||||
Liquid::Template.register_tag('paginate', Paginate)
|
||||
Liquid::Template.register_tag('form', CommentForm)
|
||||
default_environment = Liquid::Environment.default
|
||||
default_environment.register_tag('paginate', Paginate)
|
||||
default_environment.register_tag('form', CommentForm)
|
||||
|
||||
Liquid::Template.register_filter(JsonFilter)
|
||||
Liquid::Template.register_filter(MoneyFilter)
|
||||
Liquid::Template.register_filter(WeightFilter)
|
||||
Liquid::Template.register_filter(ShopFilter)
|
||||
Liquid::Template.register_filter(TagFilter)
|
||||
default_environment.register_filter(JsonFilter)
|
||||
default_environment.register_filter(MoneyFilter)
|
||||
default_environment.register_filter(WeightFilter)
|
||||
default_environment.register_filter(ShopFilter)
|
||||
default_environment.register_filter(TagFilter)
|
||||
|
@ -48,6 +48,19 @@ class ThemeRunner
|
||||
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
|
||||
def run
|
||||
each_test do |liquid, layout, assigns, page_template, template_name|
|
||||
|
94
performance/unit/expression_benchmark.rb
Normal file
94
performance/unit/expression_benchmark.rb
Normal file
@ -0,0 +1,94 @@
|
||||
# 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
performance/unit/lexer_benchmark.rb
Normal file
43
performance/unit/lexer_benchmark.rb
Normal file
@ -0,0 +1,43 @@
|
||||
# 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
|
@ -36,6 +36,24 @@ class Category
|
||||
end
|
||||
end
|
||||
|
||||
class ProductsDrop < Liquid::Drop
|
||||
def initialize(products)
|
||||
@products = products
|
||||
end
|
||||
|
||||
def size
|
||||
@products.size
|
||||
end
|
||||
|
||||
def to_liquid
|
||||
if @context["forloop"]
|
||||
@products.first(@context["forloop"].length)
|
||||
else
|
||||
@products
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class CategoryDrop < Liquid::Drop
|
||||
attr_accessor :category, :context
|
||||
|
||||
@ -635,6 +653,40 @@ class ContextTest < Minitest::Test
|
||||
assert_equal(:my_value, c.registers[:my_register])
|
||||
end
|
||||
|
||||
def test_variable_to_liquid_returns_contextual_drop
|
||||
context = {
|
||||
"products" => ProductsDrop.new(["A", "B", "C", "D", "E"]),
|
||||
}
|
||||
|
||||
template = Liquid::Template.parse(<<~LIQUID)
|
||||
{%- for i in (1..3) -%}
|
||||
for_loop_products_count: {{ products | size }}
|
||||
{% endfor %}
|
||||
|
||||
unscoped_products_count: {{ products | size }}
|
||||
LIQUID
|
||||
|
||||
result = template.render(context)
|
||||
|
||||
assert_includes(result, "for_loop_products_count: 3")
|
||||
assert_includes(result, "unscoped_products_count: 5")
|
||||
end
|
||||
|
||||
def test_new_isolated_context_inherits_parent_environment
|
||||
global_environment = Liquid::Environment.build(tags: {})
|
||||
context = Context.build(environment: global_environment)
|
||||
|
||||
subcontext = context.new_isolated_subcontext
|
||||
assert_equal(global_environment, subcontext.environment)
|
||||
end
|
||||
|
||||
def test_newly_built_context_inherits_parent_environment
|
||||
global_environment = Liquid::Environment.build(tags: {})
|
||||
context = Context.build(environment: global_environment)
|
||||
assert_equal(global_environment, context.environment)
|
||||
assert(context.environment.tags.each.to_a.empty?)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def assert_no_object_allocations
|
||||
|
@ -203,20 +203,34 @@ class ErrorHandlingTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_setting_default_exception_renderer
|
||||
old_exception_renderer = Liquid::Template.default_exception_renderer
|
||||
exceptions = []
|
||||
Liquid::Template.default_exception_renderer = ->(e) {
|
||||
default_exception_renderer = ->(e) {
|
||||
exceptions << e
|
||||
''
|
||||
}
|
||||
template = Liquid::Template.parse('This is a runtime error: {{ errors.argument_error }}')
|
||||
|
||||
env = Liquid::Environment.build(exception_renderer: default_exception_renderer)
|
||||
template = Liquid::Template.parse('This is a runtime error: {{ errors.argument_error }}', environment: env)
|
||||
|
||||
output = template.render('errors' => ErrorDrop.new)
|
||||
|
||||
assert_equal('This is a runtime error: ', output)
|
||||
assert_equal([Liquid::ArgumentError], template.errors.map(&:class))
|
||||
ensure
|
||||
Liquid::Template.default_exception_renderer = old_exception_renderer if old_exception_renderer
|
||||
end
|
||||
|
||||
def test_setting_exception_renderer_on_environment
|
||||
exceptions = []
|
||||
exception_renderer = ->(e) do
|
||||
exceptions << e
|
||||
''
|
||||
end
|
||||
|
||||
environment = Liquid::Environment.build(exception_renderer: exception_renderer)
|
||||
template = Liquid::Template.parse('This is a runtime error: {{ errors.argument_error }}', environment: environment)
|
||||
output = template.render('errors' => ErrorDrop.new)
|
||||
|
||||
assert_equal('This is a runtime error: ', output)
|
||||
assert_equal([Liquid::ArgumentError], template.errors.map(&:class))
|
||||
end
|
||||
|
||||
def test_exception_renderer_exposing_non_liquid_error
|
||||
@ -242,16 +256,10 @@ class ErrorHandlingTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_included_template_name_with_line_numbers
|
||||
old_file_system = Liquid::Template.file_system
|
||||
environment = Liquid::Environment.build(file_system: TestFileSystem.new)
|
||||
template = Liquid::Template.parse("Argument error:\n{% include 'product' %}", line_numbers: true, environment: environment)
|
||||
page = template.render('errors' => ErrorDrop.new)
|
||||
|
||||
begin
|
||||
Liquid::Template.file_system = TestFileSystem.new
|
||||
|
||||
template = Liquid::Template.parse("Argument error:\n{% include 'product' %}", line_numbers: true)
|
||||
page = template.render('errors' => ErrorDrop.new)
|
||||
ensure
|
||||
Liquid::Template.file_system = old_file_system
|
||||
end
|
||||
assert_equal("Argument error:\nLiquid error (product line 1): argument error", page)
|
||||
assert_equal("product", template.errors.first.template_name)
|
||||
end
|
||||
|
@ -1,6 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
require 'lru_redux'
|
||||
|
||||
class ExpressionTest < Minitest::Test
|
||||
def test_keyword_literals
|
||||
@ -13,6 +14,7 @@ class ExpressionTest < Minitest::Test
|
||||
assert_template_result("double quoted", '{{"double quoted"}}')
|
||||
assert_template_result("spaced", "{{ 'spaced' }}")
|
||||
assert_template_result("spaced2", "{{ 'spaced2' }}")
|
||||
assert_template_result("emoji🔥", "{{ 'emoji🔥' }}")
|
||||
end
|
||||
|
||||
def test_int
|
||||
@ -22,8 +24,18 @@ class ExpressionTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_float
|
||||
assert_template_result("-17.42", "{{ -17.42 }}")
|
||||
assert_template_result("2.5", "{{ 2.5 }}")
|
||||
assert_expression_result(0.0, "0.....5")
|
||||
assert_expression_result(0.0, "-0..1")
|
||||
assert_expression_result(1.5, "1.5")
|
||||
|
||||
# this is a unfortunate quirky behavior of Liquid
|
||||
result = Expression.parse(".5")
|
||||
assert_kind_of(Liquid::VariableLookup, result)
|
||||
|
||||
result = Expression.parse("-.5")
|
||||
assert_kind_of(Liquid::VariableLookup, result)
|
||||
end
|
||||
|
||||
def test_range
|
||||
@ -40,6 +52,101 @@ class ExpressionTest < Minitest::Test
|
||||
)
|
||||
end
|
||||
|
||||
def test_quirky_negative_sign_expression_markup
|
||||
result = Expression.parse("-", nil)
|
||||
assert(result.is_a?(VariableLookup))
|
||||
assert_equal("-", result.name)
|
||||
|
||||
# for this template, the expression markup is "-"
|
||||
assert_template_result(
|
||||
"",
|
||||
"{{ - 'theme.css' - }}",
|
||||
)
|
||||
end
|
||||
|
||||
def test_expression_cache
|
||||
skip("Liquid-C does not support Expression caching") if defined?(Liquid::C) && Liquid::C.enabled
|
||||
|
||||
cache = {}
|
||||
template = <<~LIQUID
|
||||
{% assign x = 1 %}
|
||||
{{ x }}
|
||||
{% assign x = 2 %}
|
||||
{{ x }}
|
||||
{% assign y = 1 %}
|
||||
{{ y }}
|
||||
LIQUID
|
||||
|
||||
Liquid::Template.parse(template, expression_cache: cache).render
|
||||
|
||||
assert_equal(
|
||||
["1", "2", "x", "y"],
|
||||
cache.to_a.map { _1[0] }.sort,
|
||||
)
|
||||
end
|
||||
|
||||
def test_expression_cache_with_true_boolean
|
||||
skip("Liquid-C does not support Expression caching") if defined?(Liquid::C) && Liquid::C.enabled
|
||||
|
||||
template = <<~LIQUID
|
||||
{% assign x = 1 %}
|
||||
{{ x }}
|
||||
{% assign x = 2 %}
|
||||
{{ x }}
|
||||
{% assign y = 1 %}
|
||||
{{ y }}
|
||||
LIQUID
|
||||
|
||||
parse_context = ParseContext.new(expression_cache: true)
|
||||
|
||||
Liquid::Template.parse(template, parse_context).render
|
||||
|
||||
cache = parse_context.instance_variable_get(:@expression_cache)
|
||||
|
||||
assert_equal(
|
||||
["1", "2", "x", "y"],
|
||||
cache.to_a.map { _1[0] }.sort,
|
||||
)
|
||||
end
|
||||
|
||||
def test_expression_cache_with_lru_redux
|
||||
skip("Liquid-C does not support Expression caching") if defined?(Liquid::C) && Liquid::C.enabled
|
||||
|
||||
cache = LruRedux::Cache.new(10)
|
||||
template = <<~LIQUID
|
||||
{% assign x = 1 %}
|
||||
{{ x }}
|
||||
{% assign x = 2 %}
|
||||
{{ x }}
|
||||
{% assign y = 1 %}
|
||||
{{ y }}
|
||||
LIQUID
|
||||
|
||||
Liquid::Template.parse(template, expression_cache: cache).render
|
||||
|
||||
assert_equal(
|
||||
["1", "2", "x", "y"],
|
||||
cache.to_a.map { _1[0] }.sort,
|
||||
)
|
||||
end
|
||||
|
||||
def test_disable_expression_cache
|
||||
skip("Liquid-C does not support Expression caching") if defined?(Liquid::C) && Liquid::C.enabled
|
||||
|
||||
template = <<~LIQUID
|
||||
{% assign x = 1 %}
|
||||
{{ x }}
|
||||
{% assign x = 2 %}
|
||||
{{ x }}
|
||||
{% assign y = 1 %}
|
||||
{{ y }}
|
||||
LIQUID
|
||||
|
||||
parse_context = Liquid::ParseContext.new(expression_cache: false)
|
||||
Liquid::Template.parse(template, parse_context).render
|
||||
assert(parse_context.instance_variable_get(:@expression_cache).nil?)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def assert_expression_result(expect, markup, **assigns)
|
||||
|
106
test/integration/hash_rendering_test.rb
Normal file
106
test/integration/hash_rendering_test.rb
Normal file
@ -0,0 +1,106 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class HashRenderingTest < Minitest::Test
|
||||
def test_render_empty_hash
|
||||
assert_template_result("{}", "{{ my_hash }}", { "my_hash" => {} })
|
||||
end
|
||||
|
||||
def test_render_hash_with_string_keys_and_values
|
||||
assert_template_result("{\"key1\"=>\"value1\", \"key2\"=>\"value2\"}", "{{ my_hash }}", { "my_hash" => { "key1" => "value1", "key2" => "value2" } })
|
||||
end
|
||||
|
||||
def test_render_hash_with_symbol_keys_and_integer_values
|
||||
assert_template_result("{:key1=>1, :key2=>2}", "{{ my_hash }}", { "my_hash" => { key1: 1, key2: 2 } })
|
||||
end
|
||||
|
||||
def test_render_nested_hash
|
||||
assert_template_result("{\"outer\"=>{\"inner\"=>\"value\"}}", "{{ my_hash }}", { "my_hash" => { "outer" => { "inner" => "value" } } })
|
||||
end
|
||||
|
||||
def test_render_hash_with_array_values
|
||||
assert_template_result("{\"numbers\"=>[1, 2, 3]}", "{{ my_hash }}", { "my_hash" => { "numbers" => [1, 2, 3] } })
|
||||
end
|
||||
|
||||
def test_render_recursive_hash
|
||||
recursive_hash = { "self" => {} }
|
||||
recursive_hash["self"]["self"] = recursive_hash
|
||||
assert_template_result("{\"self\"=>{\"self\"=>{...}}}", "{{ my_hash }}", { "my_hash" => recursive_hash })
|
||||
end
|
||||
|
||||
def test_hash_with_downcase_filter
|
||||
assert_template_result("{\"key\"=>\"value\", \"anotherkey\"=>\"anothervalue\"}", "{{ my_hash | downcase }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
|
||||
end
|
||||
|
||||
def test_hash_with_upcase_filter
|
||||
assert_template_result("{\"KEY\"=>\"VALUE\", \"ANOTHERKEY\"=>\"ANOTHERVALUE\"}", "{{ my_hash | upcase }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
|
||||
end
|
||||
|
||||
def test_hash_with_strip_filter
|
||||
assert_template_result("{\"Key\"=>\"Value\", \"AnotherKey\"=>\"AnotherValue\"}", "{{ my_hash | strip }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
|
||||
end
|
||||
|
||||
def test_hash_with_escape_filter
|
||||
assert_template_result("{"Key"=>"Value", "AnotherKey"=>"AnotherValue"}", "{{ my_hash | escape }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
|
||||
end
|
||||
|
||||
def test_hash_with_url_encode_filter
|
||||
assert_template_result("%7B%22Key%22%3D%3E%22Value%22%2C+%22AnotherKey%22%3D%3E%22AnotherValue%22%7D", "{{ my_hash | url_encode }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
|
||||
end
|
||||
|
||||
def test_hash_with_strip_html_filter
|
||||
assert_template_result("{\"Key\"=>\"Value\", \"AnotherKey\"=>\"AnotherValue\"}", "{{ my_hash | strip_html }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
|
||||
end
|
||||
|
||||
def test_hash_with_truncate__20_filter
|
||||
assert_template_result("{\"Key\"=>\"Value\", ...", "{{ my_hash | truncate: 20 }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
|
||||
end
|
||||
|
||||
def test_hash_with_replace___key____replaced_key__filter
|
||||
assert_template_result("{\"Key\"=>\"Value\", \"AnotherKey\"=>\"AnotherValue\"}", "{{ my_hash | replace: 'key', 'replaced_key' }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
|
||||
end
|
||||
|
||||
def test_hash_with_append____appended_text__filter
|
||||
assert_template_result("{\"Key\"=>\"Value\", \"AnotherKey\"=>\"AnotherValue\"} appended text", "{{ my_hash | append: ' appended text' }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
|
||||
end
|
||||
|
||||
def test_hash_with_prepend___prepended_text___filter
|
||||
assert_template_result("prepended text {\"Key\"=>\"Value\", \"AnotherKey\"=>\"AnotherValue\"}", "{{ my_hash | prepend: 'prepended text ' }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
|
||||
end
|
||||
|
||||
def test_render_hash_with_array_values_empty
|
||||
assert_template_result("{\"numbers\"=>[]}", "{{ my_hash }}", { "my_hash" => { "numbers" => [] } })
|
||||
end
|
||||
|
||||
def test_render_hash_with_array_values_hash
|
||||
assert_template_result("{\"numbers\"=>[{:foo=>42}]}", "{{ my_hash }}", { "my_hash" => { "numbers" => [{ foo: 42 }] } })
|
||||
end
|
||||
|
||||
def test_join_filter_with_hash
|
||||
array = [{ "key1" => "value1" }, { "key2" => "value2" }]
|
||||
glue = { "lol" => "wut" }
|
||||
assert_template_result("{\"key1\"=>\"value1\"}{\"lol\"=>\"wut\"}{\"key2\"=>\"value2\"}", "{{ my_array | join: glue }}", { "my_array" => array, "glue" => glue })
|
||||
end
|
||||
|
||||
def test_render_hash_with_hash_key
|
||||
assert_template_result("{{\"foo\"=>\"bar\"}=>42}", "{{ my_hash }}", { "my_hash" => { Hash["foo" => "bar"] => 42 } })
|
||||
end
|
||||
|
||||
def test_rendering_hash_with_custom_to_s_method_uses_custom_to_s
|
||||
my_hash = Class.new(Hash) do
|
||||
def to_s
|
||||
"kewl"
|
||||
end
|
||||
end.new
|
||||
|
||||
assert_template_result("kewl", "{{ my_hash }}", { "my_hash" => my_hash })
|
||||
end
|
||||
|
||||
def test_rendering_hash_without_custom_to_s_uses_default_inspect
|
||||
my_hash = Class.new(Hash).new
|
||||
my_hash[:foo] = :bar
|
||||
|
||||
assert_template_result("{:foo=>:bar}", "{{ my_hash }}", { "my_hash" => my_hash })
|
||||
end
|
||||
end
|
@ -131,4 +131,24 @@ class ParsingQuirksTest < Minitest::Test
|
||||
def test_contains_in_id
|
||||
assert_template_result(' YES ', '{% if containsallshipments == true %} YES {% endif %}', { 'containsallshipments' => true })
|
||||
end
|
||||
|
||||
def test_incomplete_expression
|
||||
with_error_mode(:lax) do
|
||||
assert_template_result("false", "{{ false - }}")
|
||||
assert_template_result("false", "{{ false > }}")
|
||||
assert_template_result("false", "{{ false < }}")
|
||||
assert_template_result("false", "{{ false = }}")
|
||||
assert_template_result("false", "{{ false ! }}")
|
||||
assert_template_result("false", "{{ false 1 }}")
|
||||
assert_template_result("false", "{{ false a }}")
|
||||
|
||||
assert_template_result("false", "{% liquid assign foo = false -\n%}{{ foo }}")
|
||||
assert_template_result("false", "{% liquid assign foo = false >\n%}{{ foo }}")
|
||||
assert_template_result("false", "{% liquid assign foo = false <\n%}{{ foo }}")
|
||||
assert_template_result("false", "{% liquid assign foo = false =\n%}{{ foo }}")
|
||||
assert_template_result("false", "{% liquid assign foo = false !\n%}{{ foo }}")
|
||||
assert_template_result("false", "{% liquid assign foo = false 1\n%}{{ foo }}")
|
||||
assert_template_result("false", "{% liquid assign foo = false a\n%}{{ foo }}")
|
||||
end
|
||||
end
|
||||
end # ParsingQuirksTest
|
||||
|
@ -33,7 +33,7 @@ class ProfilerTest < Minitest::Test
|
||||
end
|
||||
|
||||
def setup
|
||||
Liquid::Template.file_system = ProfilingFileSystem.new
|
||||
Liquid::Environment.default.file_system = ProfilingFileSystem.new
|
||||
end
|
||||
|
||||
def test_template_allows_flagging_profiling
|
||||
|
@ -32,7 +32,7 @@ class TestDrop < Liquid::Drop
|
||||
attr_reader :value
|
||||
|
||||
def registers
|
||||
{ @value => @context.registers[@value] }
|
||||
"{#{@value.inspect}=>#{@context.registers[@value].inspect}}"
|
||||
end
|
||||
end
|
||||
|
||||
@ -133,6 +133,18 @@ class StandardFiltersTest < Minitest::Test
|
||||
assert_equal([], @filters.slice(input, -(1 << 63), 6))
|
||||
end
|
||||
|
||||
def test_find_on_empty_array
|
||||
assert_nil(@filters.find([], 'foo', 'bar'))
|
||||
end
|
||||
|
||||
def test_find_index_on_empty_array
|
||||
assert_nil(@filters.find_index([], 'foo', 'bar'))
|
||||
end
|
||||
|
||||
def test_has_on_empty_array
|
||||
refute(@filters.has([], 'foo', 'bar'))
|
||||
end
|
||||
|
||||
def test_truncate
|
||||
assert_equal('1234...', @filters.truncate('1234567890', 7))
|
||||
assert_equal('1234567890', @filters.truncate('1234567890', 20))
|
||||
@ -176,7 +188,17 @@ class StandardFiltersTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_base64_decode
|
||||
assert_equal('one two three', @filters.base64_decode('b25lIHR3byB0aHJlZQ=='))
|
||||
decoded = @filters.base64_decode('b25lIHR3byB0aHJlZQ==')
|
||||
assert_equal('one two three', decoded)
|
||||
assert_equal(Encoding::UTF_8, decoded.encoding)
|
||||
|
||||
decoded = @filters.base64_decode('4pyF')
|
||||
assert_equal('✅', decoded)
|
||||
assert_equal(Encoding::UTF_8, decoded.encoding)
|
||||
|
||||
decoded = @filters.base64_decode("/w==")
|
||||
assert_equal(Encoding::ASCII_8BIT, decoded.encoding)
|
||||
assert_equal((+"\xFF").force_encoding(Encoding::ASCII_8BIT), decoded)
|
||||
|
||||
exception = assert_raises(Liquid::ArgumentError) do
|
||||
@filters.base64_decode("invalidbase64")
|
||||
@ -194,10 +216,21 @@ class StandardFiltersTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_base64_url_safe_decode
|
||||
decoded = @filters.base64_url_safe_decode('YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXogQUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVogMTIzNDU2Nzg5MCAhQCMkJV4mKigpLT1fKy8_Ljo7W117fVx8')
|
||||
assert_equal(
|
||||
'abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890 !@#$%^&*()-=_+/?.:;[]{}\|',
|
||||
@filters.base64_url_safe_decode('YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXogQUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVogMTIzNDU2Nzg5MCAhQCMkJV4mKigpLT1fKy8_Ljo7W117fVx8'),
|
||||
decoded,
|
||||
)
|
||||
assert_equal(Encoding::UTF_8, decoded.encoding)
|
||||
|
||||
decoded = @filters.base64_url_safe_decode('4pyF')
|
||||
assert_equal('✅', decoded)
|
||||
assert_equal(Encoding::UTF_8, decoded.encoding)
|
||||
|
||||
decoded = @filters.base64_url_safe_decode("_w==")
|
||||
assert_equal(Encoding::ASCII_8BIT, decoded.encoding)
|
||||
assert_equal((+"\xFF").force_encoding(Encoding::ASCII_8BIT), decoded)
|
||||
|
||||
exception = assert_raises(Liquid::ArgumentError) do
|
||||
@filters.base64_url_safe_decode("invalidbase64")
|
||||
end
|
||||
@ -260,6 +293,16 @@ class StandardFiltersTest < Minitest::Test
|
||||
assert_equal('1121314', @filters.join([1, 2, 3, 4], 1))
|
||||
end
|
||||
|
||||
def test_join_calls_to_liquid_on_each_element
|
||||
drop = Class.new(Liquid::Drop) do
|
||||
def to_liquid
|
||||
'i did it'
|
||||
end
|
||||
end
|
||||
|
||||
assert_equal('i did it, i did it', @filters.join([drop.new, drop.new], ", "))
|
||||
end
|
||||
|
||||
def test_sort
|
||||
assert_equal([1, 2, 3, 4], @filters.sort([4, 3, 2, 1]))
|
||||
assert_equal([{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }], @filters.sort([{ "a" => 4 }, { "a" => 3 }, { "a" => 1 }, { "a" => 2 }], "a"))
|
||||
@ -310,8 +353,8 @@ class StandardFiltersTest < Minitest::Test
|
||||
{ "price" => "1", "handle" => "gamma" },
|
||||
{ "price" => 2, "handle" => "epsilon" },
|
||||
{ "price" => "4", "handle" => "alpha" },
|
||||
{ "handle" => "delta" },
|
||||
{ "handle" => "beta" },
|
||||
{ "handle" => "delta" },
|
||||
]
|
||||
assert_equal(expectation, @filters.sort_natural(input, "price"))
|
||||
end
|
||||
@ -517,15 +560,47 @@ class StandardFiltersTest < Minitest::Test
|
||||
end
|
||||
end
|
||||
|
||||
def test_map_returns_empty_with_no_property
|
||||
foo = [
|
||||
def test_map_with_nil_property
|
||||
array = [
|
||||
{ "handle" => "alpha", "value" => "A" },
|
||||
{ "handle" => "beta", "value" => "B" },
|
||||
{ "handle" => "gamma", "value" => "C" }
|
||||
]
|
||||
|
||||
assert_template_result("alpha beta gamma", "{{ array | map: nil | map: 'handle' | join: ' ' }}", { "array" => array })
|
||||
end
|
||||
|
||||
def test_map_with_empty_string_property
|
||||
array = [
|
||||
{ "handle" => "alpha", "value" => "A" },
|
||||
{ "handle" => "beta", "value" => "B" },
|
||||
{ "handle" => "gamma", "value" => "C" }
|
||||
]
|
||||
|
||||
assert_template_result("alpha beta gamma", "{{ array | map: '' | map: 'handle' | join: ' ' }}", { "array" => array })
|
||||
end
|
||||
|
||||
def test_map_with_value_property
|
||||
array = [
|
||||
{ "handle" => "alpha", "value" => "A" },
|
||||
{ "handle" => "beta", "value" => "B" },
|
||||
{ "handle" => "gamma", "value" => "C" }
|
||||
]
|
||||
|
||||
assert_template_result("A B C", "{{ array | map: 'value' | join: ' ' }}", { "array" => array })
|
||||
end
|
||||
|
||||
def test_map_returns_input_with_no_property
|
||||
input = [
|
||||
[1],
|
||||
[2],
|
||||
[3],
|
||||
]
|
||||
assert_raises(Liquid::ArgumentError) do
|
||||
@filters.map(foo, nil)
|
||||
end
|
||||
result = @filters.map(input, nil)
|
||||
assert_equal(input.flatten, result)
|
||||
|
||||
result = @filters.map(input, '')
|
||||
assert_equal(input.flatten, result)
|
||||
end
|
||||
|
||||
def test_sort_works_on_enumerables
|
||||
@ -806,21 +881,245 @@ class StandardFiltersTest < Minitest::Test
|
||||
assert_template_result('abc', "{{ 'abc' | date: '%D' }}")
|
||||
end
|
||||
|
||||
def test_where
|
||||
input = [
|
||||
def test_reject
|
||||
array = [
|
||||
{ "handle" => "alpha", "ok" => true },
|
||||
{ "handle" => "beta", "ok" => false },
|
||||
{ "handle" => "gamma", "ok" => false },
|
||||
{ "handle" => "delta", "ok" => true },
|
||||
]
|
||||
|
||||
expectation = [
|
||||
template = "{{ array | reject: 'ok' | map: 'handle' | join: ' ' }}"
|
||||
expected_output = "beta gamma"
|
||||
|
||||
assert_template_result(expected_output, template, { "array" => array })
|
||||
end
|
||||
|
||||
def test_reject_with_value
|
||||
array = [
|
||||
{ "handle" => "alpha", "ok" => true },
|
||||
{ "handle" => "beta", "ok" => false },
|
||||
{ "handle" => "gamma", "ok" => false },
|
||||
{ "handle" => "delta", "ok" => true },
|
||||
]
|
||||
|
||||
assert_equal(expectation, @filters.where(input, "ok", true))
|
||||
assert_equal(expectation, @filters.where(input, "ok"))
|
||||
template = "{{ array | reject: 'ok', true | map: 'handle' | join: ' ' }}"
|
||||
expected_output = "beta gamma"
|
||||
|
||||
assert_template_result(expected_output, template, { "array" => array })
|
||||
end
|
||||
|
||||
def test_reject_with_false_value
|
||||
array = [
|
||||
{ "handle" => "alpha", "ok" => true },
|
||||
{ "handle" => "beta", "ok" => false },
|
||||
{ "handle" => "gamma", "ok" => false },
|
||||
{ "handle" => "delta", "ok" => true },
|
||||
]
|
||||
|
||||
template = "{{ array | reject: 'ok', false | map: 'handle' | join: ' ' }}"
|
||||
expected_output = "alpha delta"
|
||||
|
||||
assert_template_result(expected_output, template, { "array" => array })
|
||||
end
|
||||
|
||||
def test_has
|
||||
array = [
|
||||
{ "handle" => "alpha", "ok" => true },
|
||||
{ "handle" => "beta", "ok" => false },
|
||||
{ "handle" => "gamma", "ok" => false },
|
||||
{ "handle" => "delta", "ok" => false },
|
||||
]
|
||||
|
||||
expected_output = "true"
|
||||
|
||||
assert_template_result(expected_output, "{{ array | has: 'ok' }}", { "array" => array })
|
||||
assert_template_result(expected_output, "{{ array | has: 'ok', true }}", { "array" => array })
|
||||
end
|
||||
|
||||
def test_has_when_does_not_have_it
|
||||
array = [
|
||||
{ "handle" => "alpha", "ok" => false },
|
||||
{ "handle" => "beta", "ok" => false },
|
||||
{ "handle" => "gamma", "ok" => false },
|
||||
{ "handle" => "delta", "ok" => false },
|
||||
]
|
||||
|
||||
expected_output = "false"
|
||||
|
||||
assert_template_result(expected_output, "{{ array | has: 'ok' }}", { "array" => array })
|
||||
assert_template_result(expected_output, "{{ array | has: 'ok', true }}", { "array" => array })
|
||||
end
|
||||
|
||||
def test_has_with_empty_arrays
|
||||
template = <<~LIQUID
|
||||
{%- assign has_product = products | has: 'title.content', 'Not found' -%}
|
||||
{%- unless has_product -%}
|
||||
Product not found.
|
||||
{%- endunless -%}
|
||||
LIQUID
|
||||
expected_output = "Product not found."
|
||||
|
||||
assert_template_result(expected_output, template, { "products" => [] })
|
||||
end
|
||||
|
||||
def test_has_with_false_value
|
||||
array = [
|
||||
{ "handle" => "alpha", "ok" => true },
|
||||
{ "handle" => "beta", "ok" => false },
|
||||
{ "handle" => "gamma", "ok" => false },
|
||||
{ "handle" => "delta", "ok" => true },
|
||||
]
|
||||
|
||||
template = "{{ array | has: 'ok', false }}"
|
||||
expected_output = "true"
|
||||
|
||||
assert_template_result(expected_output, template, { "array" => array })
|
||||
end
|
||||
|
||||
def test_has_with_false_value_when_does_not_have_it
|
||||
array = [
|
||||
{ "handle" => "alpha", "ok" => true },
|
||||
{ "handle" => "beta", "ok" => true },
|
||||
{ "handle" => "gamma", "ok" => true },
|
||||
{ "handle" => "delta", "ok" => true },
|
||||
]
|
||||
|
||||
template = "{{ array | has: 'ok', false }}"
|
||||
expected_output = "false"
|
||||
|
||||
assert_template_result(expected_output, template, { "array" => array })
|
||||
end
|
||||
|
||||
def test_find_with_value
|
||||
products = [
|
||||
{ "title" => "Pro goggles", "price" => 1299 },
|
||||
{ "title" => "Thermal gloves", "price" => 1499 },
|
||||
{ "title" => "Alpine jacket", "price" => 3999 },
|
||||
{ "title" => "Mountain boots", "price" => 3899 },
|
||||
{ "title" => "Safety helmet", "price" => 1999 }
|
||||
]
|
||||
|
||||
template = <<~LIQUID
|
||||
{%- assign product = products | find: 'price', 3999 -%}
|
||||
{{- product.title -}}
|
||||
LIQUID
|
||||
expected_output = "Alpine jacket"
|
||||
|
||||
assert_template_result(expected_output, template, { "products" => products })
|
||||
end
|
||||
|
||||
def test_find_with_empty_arrays
|
||||
template = <<~LIQUID
|
||||
{%- assign product = products | find: 'title.content', 'Not found' -%}
|
||||
{%- unless product -%}
|
||||
Product not found.
|
||||
{%- endunless -%}
|
||||
LIQUID
|
||||
expected_output = "Product not found."
|
||||
|
||||
assert_template_result(expected_output, template, { "products" => [] })
|
||||
end
|
||||
|
||||
def test_find_index_with_value
|
||||
products = [
|
||||
{ "title" => "Pro goggles", "price" => 1299 },
|
||||
{ "title" => "Thermal gloves", "price" => 1499 },
|
||||
{ "title" => "Alpine jacket", "price" => 3999 },
|
||||
{ "title" => "Mountain boots", "price" => 3899 },
|
||||
{ "title" => "Safety helmet", "price" => 1999 }
|
||||
]
|
||||
|
||||
template = <<~LIQUID
|
||||
{%- assign index = products | find_index: 'price', 3999 -%}
|
||||
{{- index -}}
|
||||
LIQUID
|
||||
expected_output = "2"
|
||||
|
||||
assert_template_result(expected_output, template, { "products" => products })
|
||||
end
|
||||
|
||||
def test_find_index_with_empty_arrays
|
||||
template = <<~LIQUID
|
||||
{%- assign index = products | find_index: 'title.content', 'Not found' -%}
|
||||
{%- unless index -%}
|
||||
Index not found.
|
||||
{%- endunless -%}
|
||||
LIQUID
|
||||
expected_output = "Index not found."
|
||||
|
||||
assert_template_result(expected_output, template, { "products" => [] })
|
||||
end
|
||||
|
||||
def test_where
|
||||
array = [
|
||||
{ "handle" => "alpha", "ok" => true },
|
||||
{ "handle" => "beta", "ok" => false },
|
||||
{ "handle" => "gamma", "ok" => false },
|
||||
{ "handle" => "delta", "ok" => true },
|
||||
]
|
||||
|
||||
template = "{{ array | where: 'ok' | map: 'handle' | join: ' ' }}"
|
||||
expected_output = "alpha delta"
|
||||
|
||||
assert_template_result(expected_output, template, { "array" => array })
|
||||
end
|
||||
|
||||
def test_where_with_empty_string_is_a_no_op
|
||||
environment = { "array" => ["alpha", "beta", "gamma"] }
|
||||
expected_output = "alpha beta gamma"
|
||||
template = "{{ array | where: '' | join: ' ' }}"
|
||||
|
||||
assert_template_result(expected_output, template, environment)
|
||||
end
|
||||
|
||||
def test_where_with_nil_is_a_no_op
|
||||
environment = { "array" => ["alpha", "beta", "gamma"] }
|
||||
expected_output = "alpha beta gamma"
|
||||
template = "{{ array | where: nil | join: ' ' }}"
|
||||
|
||||
assert_template_result(expected_output, template, environment)
|
||||
end
|
||||
|
||||
def test_where_with_value
|
||||
array = [
|
||||
{ "handle" => "alpha", "ok" => true },
|
||||
{ "handle" => "beta", "ok" => false },
|
||||
{ "handle" => "gamma", "ok" => false },
|
||||
{ "handle" => "delta", "ok" => true },
|
||||
]
|
||||
|
||||
template = "{{ array | where: 'ok', true | map: 'handle' | join: ' ' }}"
|
||||
expected_output = "alpha delta"
|
||||
|
||||
assert_template_result(expected_output, template, { "array" => array })
|
||||
end
|
||||
|
||||
def test_where_with_false_value
|
||||
array = [
|
||||
{ "handle" => "alpha", "ok" => true },
|
||||
{ "handle" => "beta", "ok" => false },
|
||||
{ "handle" => "gamma", "ok" => false },
|
||||
{ "handle" => "delta", "ok" => true },
|
||||
]
|
||||
|
||||
template = "{{ array | where: 'ok', false | map: 'handle' | join: ' ' }}"
|
||||
expected_output = "beta gamma"
|
||||
|
||||
assert_template_result(expected_output, template, { "array" => array })
|
||||
end
|
||||
|
||||
def test_where_with_non_string_property
|
||||
array = [
|
||||
{ "handle" => "alpha", "{}" => true },
|
||||
{ "handle" => "beta", "{}" => false },
|
||||
{ "handle" => "gamma", "{}" => false },
|
||||
{ "handle" => "delta", "{}" => true },
|
||||
]
|
||||
template = "{{ array | where: some_property, true | map: 'handle' | join: ' ' }}"
|
||||
expected_output = "alpha delta"
|
||||
|
||||
assert_template_result(expected_output, template, { "array" => array, "some_property" => {} })
|
||||
end
|
||||
|
||||
def test_where_string_keys
|
||||
@ -994,6 +1293,69 @@ class StandardFiltersTest < Minitest::Test
|
||||
assert(t.foo > 0)
|
||||
end
|
||||
|
||||
def test_sum_of_floats
|
||||
input = [0.1, 0.2, 0.3]
|
||||
assert_equal(0.6, @filters.sum(input))
|
||||
assert_template_result("0.6", "{{ input | sum }}", { "input" => input })
|
||||
end
|
||||
|
||||
def test_sum_of_negative_floats
|
||||
input = [0.1, 0.2, -0.3]
|
||||
assert_equal(0.0, @filters.sum(input))
|
||||
assert_template_result("0.0", "{{ input | sum }}", { "input" => input })
|
||||
end
|
||||
|
||||
def test_sum_with_float_strings
|
||||
input = [0.1, "0.2", "0.3"]
|
||||
assert_equal(0.6, @filters.sum(input))
|
||||
assert_template_result("0.6", "{{ input | sum }}", { "input" => input })
|
||||
end
|
||||
|
||||
def test_sum_resulting_in_negative_float
|
||||
input = [0.1, -0.2, -0.3]
|
||||
assert_equal(-0.4, @filters.sum(input))
|
||||
assert_template_result("-0.4", "{{ input | sum }}", { "input" => input })
|
||||
end
|
||||
|
||||
def test_sum_with_floats_and_indexable_map_values
|
||||
input = [{ "quantity" => 1 }, { "quantity" => 0.2, "weight" => -0.3 }, { "weight" => 0.4 }]
|
||||
assert_equal(0.0, @filters.sum(input))
|
||||
assert_equal(1.2, @filters.sum(input, "quantity"))
|
||||
assert_equal(0.1, @filters.sum(input, "weight"))
|
||||
assert_equal(0.0, @filters.sum(input, "subtotal"))
|
||||
assert_template_result("0", "{{ input | sum }}", { "input" => input })
|
||||
assert_template_result("1.2", "{{ input | sum: 'quantity' }}", { "input" => input })
|
||||
assert_template_result("0.1", "{{ input | sum: 'weight' }}", { "input" => input })
|
||||
assert_template_result("0", "{{ input | sum: 'subtotal' }}", { "input" => input })
|
||||
end
|
||||
|
||||
def test_sum_with_non_string_property
|
||||
input = [{ "true" => 1 }, { "1.0" => 0.2, "1" => -0.3 }, { "1..5" => 0.4 }]
|
||||
|
||||
assert_equal(1, @filters.sum(input, true))
|
||||
assert_equal(0.2, @filters.sum(input, 1.0))
|
||||
assert_equal(-0.3, @filters.sum(input, 1))
|
||||
assert_equal(0.4, @filters.sum(input, (1..5)))
|
||||
assert_equal(0, @filters.sum(input, nil))
|
||||
assert_equal(0, @filters.sum(input, ""))
|
||||
end
|
||||
|
||||
def test_uniq_with_to_liquid_value
|
||||
input = [StringDrop.new("foo"), StringDrop.new("bar"), "foo"]
|
||||
expected = [StringDrop.new("foo"), StringDrop.new("bar")]
|
||||
result = @filters.uniq(input)
|
||||
|
||||
assert_equal(expected, result)
|
||||
end
|
||||
|
||||
def test_uniq_with_to_liquid_value_pick_correct_classes
|
||||
input = ["foo", StringDrop.new("foo"), StringDrop.new("bar")]
|
||||
expected = [String, StringDrop]
|
||||
result = @filters.uniq(input).map(&:class)
|
||||
|
||||
assert_equal(expected, result)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def with_timezone(tz)
|
||||
|
48
test/integration/tags/cycle_tag_test.rb
Normal file
48
test/integration/tags/cycle_tag_test.rb
Normal file
@ -0,0 +1,48 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class CycleTagTest < Minitest::Test
|
||||
def test_simple_cycle
|
||||
template = <<~LIQUID
|
||||
{%- cycle '1', '2', '3' -%}
|
||||
{%- cycle '1', '2', '3' -%}
|
||||
{%- cycle '1', '2', '3' -%}
|
||||
LIQUID
|
||||
|
||||
assert_template_result("123", template)
|
||||
end
|
||||
|
||||
def test_simple_cycle_inside_for_loop
|
||||
template = <<~LIQUID
|
||||
{%- for i in (1..3) -%}
|
||||
{% cycle '1', '2', '3' %}
|
||||
{%- endfor -%}
|
||||
LIQUID
|
||||
|
||||
assert_template_result("123", template)
|
||||
end
|
||||
|
||||
def test_cycle_with_variables_inside_for_loop
|
||||
template = <<~LIQUID
|
||||
{%- assign a = 1 -%}
|
||||
{%- assign b = 2 -%}
|
||||
{%- assign c = 3 -%}
|
||||
{%- for i in (1..3) -%}
|
||||
{% cycle a, b, c %}
|
||||
{%- endfor -%}
|
||||
LIQUID
|
||||
|
||||
assert_template_result("123", template)
|
||||
end
|
||||
|
||||
def test_cycle_tag_always_resets_cycle
|
||||
template = <<~LIQUID
|
||||
{%- assign a = "1" -%}
|
||||
{%- cycle a, "2" -%}
|
||||
{%- cycle a, "2" -%}
|
||||
LIQUID
|
||||
|
||||
assert_template_result("11", template)
|
||||
end
|
||||
end
|
@ -49,14 +49,6 @@ end
|
||||
class IncludeTagTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def setup
|
||||
@default_file_system = Liquid::Template.file_system
|
||||
end
|
||||
|
||||
def teardown
|
||||
Liquid::Template.file_system = @default_file_system
|
||||
end
|
||||
|
||||
def test_include_tag_looks_for_file_system_in_registers_first
|
||||
assert_equal(
|
||||
'from OtherFileSystem',
|
||||
@ -182,10 +174,10 @@ class IncludeTagTest < Minitest::Test
|
||||
end
|
||||
end
|
||||
|
||||
Liquid::Template.file_system = infinite_file_system.new
|
||||
env = Liquid::Environment.build(file_system: infinite_file_system.new)
|
||||
|
||||
assert_raises(Liquid::StackLevelError) do
|
||||
Template.parse("{% include 'loop' %}").render!
|
||||
Template.parse("{% include 'loop' %}", environment: env).render!
|
||||
end
|
||||
end
|
||||
|
||||
@ -214,9 +206,10 @@ class IncludeTagTest < Minitest::Test
|
||||
|
||||
def test_include_tag_caches_second_read_of_same_partial
|
||||
file_system = CountingFileSystem.new
|
||||
environment = Liquid::Environment.build(file_system: file_system)
|
||||
assert_equal(
|
||||
'from CountingFileSystemfrom CountingFileSystem',
|
||||
Template.parse("{% include 'pick_a_source' %}{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system }),
|
||||
Template.parse("{% include 'pick_a_source' %}{% include 'pick_a_source' %}", environment: environment).render!({}, registers: { file_system: file_system }),
|
||||
)
|
||||
assert_equal(1, file_system.count)
|
||||
end
|
||||
@ -271,26 +264,27 @@ class IncludeTagTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_does_not_add_error_in_strict_mode_for_missing_variable
|
||||
Liquid::Template.file_system = TestFileSystem.new
|
||||
env = Liquid::Environment.build(file_system: TestFileSystem.new)
|
||||
|
||||
a = Liquid::Template.parse(' {% include "nested_template" %}')
|
||||
a = Liquid::Template.parse(' {% include "nested_template" %}', environment: env)
|
||||
a.render!
|
||||
assert_empty(a.errors)
|
||||
end
|
||||
|
||||
def test_passing_options_to_included_templates
|
||||
Liquid::Template.file_system = TestFileSystem.new
|
||||
env = Liquid::Environment.build(file_system: TestFileSystem.new)
|
||||
|
||||
assert_raises(Liquid::SyntaxError) do
|
||||
Template.parse("{% include template %}", error_mode: :strict).render!("template" => '{{ "X" || downcase }}')
|
||||
Template.parse("{% include template %}", error_mode: :strict, environment: env).render!("template" => '{{ "X" || downcase }}')
|
||||
end
|
||||
with_error_mode(:lax) do
|
||||
assert_equal('x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: true).render!("template" => '{{ "X" || downcase }}'))
|
||||
assert_equal('x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: true, environment: env).render!("template" => '{{ "X" || downcase }}'))
|
||||
end
|
||||
assert_raises(Liquid::SyntaxError) do
|
||||
Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:locale]).render!("template" => '{{ "X" || downcase }}')
|
||||
Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:locale], environment: env).render!("template" => '{{ "X" || downcase }}')
|
||||
end
|
||||
with_error_mode(:lax) do
|
||||
assert_equal('x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:error_mode]).render!("template" => '{{ "X" || downcase }}'))
|
||||
assert_equal('x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:error_mode], environment: env).render!("template" => '{{ "X" || downcase }}'))
|
||||
end
|
||||
end
|
||||
|
||||
@ -341,8 +335,11 @@ class IncludeTagTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_including_with_strict_variables
|
||||
Liquid::Template.file_system = StubFileSystem.new({ "simple" => "simple" })
|
||||
template = Liquid::Template.parse("{% include 'simple' %}", error_mode: :warn)
|
||||
env = Liquid::Environment.build(
|
||||
file_system: StubFileSystem.new('simple' => 'simple'),
|
||||
)
|
||||
|
||||
template = Liquid::Template.parse("{% include 'simple' %}", error_mode: :warn, environment: env)
|
||||
template.render(nil, strict_variables: true)
|
||||
|
||||
assert_equal([], template.errors)
|
||||
|
@ -16,6 +16,7 @@ class RawTagTest < Minitest::Test
|
||||
assert_template_result('>{{ test }}<', '> {%- raw -%}{{ test }}{%- endraw -%} <')
|
||||
assert_template_result("> inner <", "> {%- raw -%} inner {%- endraw %} <")
|
||||
assert_template_result("> inner <", "> {%- raw -%} inner {%- endraw -%} <")
|
||||
assert_template_result("{Hello}", "{% raw %}{{% endraw %}Hello{% raw %}}{% endraw %}")
|
||||
end
|
||||
|
||||
def test_open_tag_in_raw
|
||||
|
@ -82,19 +82,22 @@ class RenderTagTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_recursively_rendered_template_does_not_produce_endless_loop
|
||||
Liquid::Template.file_system = StubFileSystem.new('loop' => '{% render "loop" %}')
|
||||
env = Liquid::Environment.build(
|
||||
file_system: StubFileSystem.new('loop' => '{% render "loop" %}'),
|
||||
)
|
||||
|
||||
assert_raises(Liquid::StackLevelError) do
|
||||
Template.parse('{% render "loop" %}').render!
|
||||
Template.parse('{% render "loop" %}', environment: env).render!
|
||||
end
|
||||
end
|
||||
|
||||
def test_sub_contexts_count_towards_the_same_recursion_limit
|
||||
Liquid::Template.file_system = StubFileSystem.new(
|
||||
'loop_render' => '{% render "loop_render" %}',
|
||||
env = Liquid::Environment.build(
|
||||
file_system: StubFileSystem.new('loop_render' => '{% render "loop_render" %}'),
|
||||
)
|
||||
|
||||
assert_raises(Liquid::StackLevelError) do
|
||||
Template.parse('{% render "loop_render" %}').render!
|
||||
Template.parse('{% render "loop_render" %}', environment: env).render!
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -207,4 +207,52 @@ class TableRowTest < Minitest::Test
|
||||
render_errors: true,
|
||||
)
|
||||
end
|
||||
|
||||
def test_table_row_handles_interrupts
|
||||
assert_template_result(
|
||||
"<tr class=\"row1\">\n<td class=\"col1\"> 1 </td></tr>\n",
|
||||
'{% tablerow n in (1...3) cols:2 %} {{n}} {% break %} {{n}} {% endtablerow %}',
|
||||
)
|
||||
|
||||
assert_template_result(
|
||||
"<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 3 </td></tr>\n",
|
||||
'{% tablerow n in (1...3) cols:2 %} {{n}} {% continue %} {{n}} {% endtablerow %}',
|
||||
)
|
||||
end
|
||||
|
||||
def test_table_row_does_not_leak_interrupts
|
||||
template = <<~LIQUID
|
||||
{% for i in (1..2) -%}
|
||||
{% for j in (1..2) -%}
|
||||
{% tablerow k in (1..3) %}{% break %}{% endtablerow -%}
|
||||
loop j={{ j }}
|
||||
{% endfor -%}
|
||||
loop i={{ i }}
|
||||
{% endfor -%}
|
||||
after loop
|
||||
LIQUID
|
||||
|
||||
expected = <<~STR
|
||||
<tr class="row1">
|
||||
<td class="col1"></td></tr>
|
||||
loop j=1
|
||||
<tr class="row1">
|
||||
<td class="col1"></td></tr>
|
||||
loop j=2
|
||||
loop i=1
|
||||
<tr class="row1">
|
||||
<td class="col1"></td></tr>
|
||||
loop j=1
|
||||
<tr class="row1">
|
||||
<td class="col1"></td></tr>
|
||||
loop j=2
|
||||
loop i=2
|
||||
after loop
|
||||
STR
|
||||
|
||||
assert_template_result(
|
||||
expected,
|
||||
template,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
@ -337,4 +337,22 @@ class TemplateTest < Minitest::Test
|
||||
assert_equal("x=2", output)
|
||||
assert_instance_of(String, output)
|
||||
end
|
||||
|
||||
def test_raises_error_with_invalid_utf8
|
||||
e = assert_raises(TemplateEncodingError) do
|
||||
Template.parse(<<~LIQUID)
|
||||
{% comment %}
|
||||
\xC0
|
||||
{% endcomment %}
|
||||
LIQUID
|
||||
end
|
||||
|
||||
assert_equal('Liquid error: Invalid template encoding', e.message)
|
||||
end
|
||||
|
||||
def test_allows_non_string_values_as_source
|
||||
assert_equal('', Template.parse(nil).render)
|
||||
assert_equal('1', Template.parse(1).render)
|
||||
assert_equal('true', Template.parse(true).render)
|
||||
end
|
||||
end
|
||||
|
@ -130,6 +130,10 @@ class VariableTest < Minitest::Test
|
||||
assert_template_result('bar', '{{ foo }}', { 'foo' => :bar })
|
||||
end
|
||||
|
||||
def test_nested_array
|
||||
assert_template_result('', '{{ foo }}', { 'foo' => [[nil]] })
|
||||
end
|
||||
|
||||
def test_dynamic_find_var
|
||||
assert_template_result('bar', '{{ [key] }}', { 'key' => 'foo', 'foo' => 'bar' })
|
||||
end
|
||||
|
@ -13,12 +13,7 @@ if (env_mode = ENV['LIQUID_PARSER_MODE'])
|
||||
puts "-- #{env_mode.upcase} ERROR MODE"
|
||||
mode = env_mode.to_sym
|
||||
end
|
||||
Liquid::Template.error_mode = mode
|
||||
|
||||
if ENV['LIQUID_C'] == '1'
|
||||
puts "-- LIQUID C"
|
||||
require 'liquid/c'
|
||||
end
|
||||
Liquid::Environment.default.error_mode = mode
|
||||
|
||||
if Minitest.const_defined?('Test')
|
||||
# We're on Minitest 5+. Nothing to do here.
|
||||
@ -42,10 +37,11 @@ module Minitest
|
||||
message: nil, partials: nil, error_mode: nil, render_errors: false,
|
||||
template_factory: nil
|
||||
)
|
||||
template = Liquid::Template.parse(template, line_numbers: true, error_mode: error_mode&.to_sym)
|
||||
file_system = StubFileSystem.new(partials || {})
|
||||
environment = Liquid::Environment.build(file_system: file_system)
|
||||
template = Liquid::Template.parse(template, line_numbers: true, error_mode: error_mode&.to_sym, environment: environment)
|
||||
registers = Liquid::Registers.new(file_system: file_system, template_factory: template_factory)
|
||||
context = Liquid::Context.build(static_environments: assigns, rethrow_errors: !render_errors, registers: registers)
|
||||
context = Liquid::Context.build(static_environments: assigns, rethrow_errors: !render_errors, registers: registers, environment: environment)
|
||||
output = template.render(context)
|
||||
assert_equal(expected, output, message)
|
||||
end
|
||||
@ -78,44 +74,27 @@ module Minitest
|
||||
assert_equal(times, calls, "Number of calls to Usage.increment with #{name.inspect}")
|
||||
end
|
||||
|
||||
def with_global_filter(*globals)
|
||||
original_global_cache = Liquid::StrainerFactory::GlobalCache
|
||||
Liquid::StrainerFactory.send(:remove_const, :GlobalCache)
|
||||
Liquid::StrainerFactory.const_set(:GlobalCache, Class.new(Liquid::StrainerTemplate))
|
||||
def with_global_filter(*globals, &blk)
|
||||
environment = Liquid::Environment.build do |w|
|
||||
w.register_filters(globals)
|
||||
end
|
||||
|
||||
globals.each do |global|
|
||||
Liquid::Template.register_filter(global)
|
||||
end
|
||||
Liquid::StrainerFactory.send(:strainer_class_cache).clear
|
||||
begin
|
||||
yield
|
||||
ensure
|
||||
Liquid::StrainerFactory.send(:remove_const, :GlobalCache)
|
||||
Liquid::StrainerFactory.const_set(:GlobalCache, original_global_cache)
|
||||
Liquid::StrainerFactory.send(:strainer_class_cache).clear
|
||||
end
|
||||
Environment.dangerously_override(environment, &blk)
|
||||
end
|
||||
|
||||
def with_error_mode(mode)
|
||||
old_mode = Liquid::Template.error_mode
|
||||
Liquid::Template.error_mode = mode
|
||||
old_mode = Liquid::Environment.default.error_mode
|
||||
Liquid::Environment.default.error_mode = mode
|
||||
yield
|
||||
ensure
|
||||
Liquid::Template.error_mode = old_mode
|
||||
Liquid::Environment.default.error_mode = old_mode
|
||||
end
|
||||
|
||||
def with_custom_tag(tag_name, tag_class)
|
||||
old_tag = Liquid::Template.tags[tag_name]
|
||||
begin
|
||||
Liquid::Template.register_tag(tag_name, tag_class)
|
||||
yield
|
||||
ensure
|
||||
if old_tag
|
||||
Liquid::Template.tags[tag_name] = old_tag
|
||||
else
|
||||
Liquid::Template.tags.delete(tag_name)
|
||||
end
|
||||
end
|
||||
def with_custom_tag(tag_name, tag_class, &block)
|
||||
environment = Liquid::Environment.default.dup
|
||||
environment.register_tag(tag_name, tag_class)
|
||||
|
||||
Environment.dangerously_override(environment, &block)
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -167,6 +146,35 @@ class BooleanDrop < Liquid::Drop
|
||||
end
|
||||
end
|
||||
|
||||
class StringDrop < Liquid::Drop
|
||||
include Comparable
|
||||
|
||||
def initialize(value)
|
||||
super()
|
||||
@value = value
|
||||
end
|
||||
|
||||
def to_liquid_value
|
||||
@value
|
||||
end
|
||||
|
||||
def to_s
|
||||
@value
|
||||
end
|
||||
|
||||
def to_str
|
||||
@value
|
||||
end
|
||||
|
||||
def inspect
|
||||
"#<StringDrop @value=#{@value.inspect}>"
|
||||
end
|
||||
|
||||
def <=>(other)
|
||||
to_liquid_value <=> Liquid::Utils.to_liquid_value(other)
|
||||
end
|
||||
end
|
||||
|
||||
class ErrorDrop < Liquid::Drop
|
||||
def standard_error
|
||||
raise Liquid::StandardError, 'standard error'
|
||||
|
@ -32,6 +32,12 @@ class BlockUnitTest < Minitest::Test
|
||||
assert_equal(String, template.root.nodelist[2].class)
|
||||
end
|
||||
|
||||
def test_variable_with_multibyte_character
|
||||
template = Liquid::Template.parse("{{ '❤️' }}")
|
||||
assert_equal(1, template.root.nodelist.size)
|
||||
assert_equal(Variable, template.root.nodelist[0].class)
|
||||
end
|
||||
|
||||
def test_variable_many_embedded_fragments
|
||||
template = Liquid::Template.parse(" {{funk}} {{so}} {{brother}} ")
|
||||
assert_equal(7, template.root.nodelist.size)
|
||||
@ -41,12 +47,18 @@ class BlockUnitTest < Minitest::Test
|
||||
)
|
||||
end
|
||||
|
||||
def test_with_block
|
||||
def test_comment_tag_with_block
|
||||
template = Liquid::Template.parse(" {% comment %} {% endcomment %} ")
|
||||
assert_equal([String, Comment, String], block_types(template.root.nodelist))
|
||||
assert_equal(3, template.root.nodelist.size)
|
||||
end
|
||||
|
||||
def test_doc_tag_with_block
|
||||
template = Liquid::Template.parse(" {% doc %} {% enddoc %} ")
|
||||
assert_equal([String, Doc, String], block_types(template.root.nodelist))
|
||||
assert_equal(3, template.root.nodelist.size)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def block_types(nodelist)
|
||||
|
@ -55,6 +55,11 @@ class ConditionUnitTest < Minitest::Test
|
||||
assert_evaluates_false('bob', 'contains', '---')
|
||||
end
|
||||
|
||||
def test_contains_binary_encoding_compatibility_with_utf8
|
||||
assert_evaluates_true('🙈'.b, 'contains', '🙈')
|
||||
assert_evaluates_true('🙈', 'contains', '🙈'.b)
|
||||
end
|
||||
|
||||
def test_invalid_comparation_operator
|
||||
assert_evaluates_argument_error(1, '~~', 0)
|
||||
end
|
||||
@ -166,14 +171,14 @@ class ConditionUnitTest < Minitest::Test
|
||||
def assert_evaluates_true(left, op, right)
|
||||
assert(
|
||||
Condition.new(left, op, right).evaluate(@context),
|
||||
"Evaluated false: #{left} #{op} #{right}",
|
||||
"Evaluated false: #{left.inspect} #{op} #{right.inspect}",
|
||||
)
|
||||
end
|
||||
|
||||
def assert_evaluates_false(left, op, right)
|
||||
assert(
|
||||
!Condition.new(left, op, right).evaluate(@context),
|
||||
"Evaluated true: #{left} #{op} #{right}",
|
||||
"Evaluated true: #{left.inspect} #{op} #{right.inspect}",
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class StrainerFactoryUnitTest < Minitest::Test
|
||||
class EnvironmentFilterTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
module AccessScopeFilters
|
||||
@ -16,8 +16,6 @@ class StrainerFactoryUnitTest < Minitest::Test
|
||||
private :private_filter
|
||||
end
|
||||
|
||||
StrainerFactory.add_global_filter(AccessScopeFilters)
|
||||
|
||||
module LateAddedFilter
|
||||
def late_added_filter(_input)
|
||||
"filtered"
|
||||
@ -25,24 +23,28 @@ class StrainerFactoryUnitTest < Minitest::Test
|
||||
end
|
||||
|
||||
def setup
|
||||
@context = Context.build
|
||||
@environment = Liquid::Environment.build do |env|
|
||||
env.register_filter(AccessScopeFilters)
|
||||
end
|
||||
|
||||
@context = Context.build(environment: @environment)
|
||||
end
|
||||
|
||||
def test_strainer
|
||||
strainer = StrainerFactory.create(@context)
|
||||
strainer = @environment.create_strainer(@context)
|
||||
assert_equal(5, strainer.invoke('size', 'input'))
|
||||
assert_equal("public", strainer.invoke("public_filter"))
|
||||
end
|
||||
|
||||
def test_stainer_raises_argument_error
|
||||
strainer = StrainerFactory.create(@context)
|
||||
def test_strainer_raises_argument_error
|
||||
strainer = @environment.create_strainer(@context)
|
||||
assert_raises(Liquid::ArgumentError) do
|
||||
strainer.invoke("public_filter", 1)
|
||||
end
|
||||
end
|
||||
|
||||
def test_stainer_argument_error_contains_backtrace
|
||||
strainer = StrainerFactory.create(@context)
|
||||
def test_strainer_argument_error_contains_backtrace
|
||||
strainer = @environment.create_strainer(@context)
|
||||
|
||||
exception = assert_raises(Liquid::ArgumentError) do
|
||||
strainer.invoke("public_filter", 1)
|
||||
@ -52,12 +54,13 @@ class StrainerFactoryUnitTest < Minitest::Test
|
||||
/\ALiquid error: wrong number of arguments \((1 for 0|given 1, expected 0)\)\z/,
|
||||
exception.message,
|
||||
)
|
||||
|
||||
source = AccessScopeFilters.instance_method(:public_filter).source_location
|
||||
assert_equal(source.map(&:to_s), exception.backtrace[0].split(':')[0..1])
|
||||
assert_equal(source[0..1].map(&:to_s), exception.backtrace[0].split(':')[0..1])
|
||||
end
|
||||
|
||||
def test_strainer_only_invokes_public_filter_methods
|
||||
strainer = StrainerFactory.create(@context)
|
||||
strainer = @environment.create_strainer(@context)
|
||||
assert_equal(false, strainer.class.invokable?('__test__'))
|
||||
assert_equal(false, strainer.class.invokable?('test'))
|
||||
assert_equal(false, strainer.class.invokable?('instance_eval'))
|
||||
@ -66,18 +69,18 @@ class StrainerFactoryUnitTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_strainer_returns_nil_if_no_filter_method_found
|
||||
strainer = StrainerFactory.create(@context)
|
||||
strainer = @environment.create_strainer(@context)
|
||||
assert_nil(strainer.invoke("private_filter"))
|
||||
assert_nil(strainer.invoke("undef_the_filter"))
|
||||
end
|
||||
|
||||
def test_strainer_returns_first_argument_if_no_method_and_arguments_given
|
||||
strainer = StrainerFactory.create(@context)
|
||||
strainer = @environment.create_strainer(@context)
|
||||
assert_equal("password", strainer.invoke("undef_the_method", "password"))
|
||||
end
|
||||
|
||||
def test_strainer_only_allows_methods_defined_in_filters
|
||||
strainer = StrainerFactory.create(@context)
|
||||
strainer = @environment.create_strainer(@context)
|
||||
assert_equal("1 + 1", strainer.invoke("instance_eval", "1 + 1"))
|
||||
assert_equal("puts", strainer.invoke("__send__", "puts", "Hi Mom"))
|
||||
assert_equal("has_method?", strainer.invoke("invoke", "has_method?", "invoke"))
|
||||
@ -86,7 +89,9 @@ class StrainerFactoryUnitTest < Minitest::Test
|
||||
def test_strainer_uses_a_class_cache_to_avoid_method_cache_invalidation
|
||||
a = Module.new
|
||||
b = Module.new
|
||||
strainer = StrainerFactory.create(@context, [a, b])
|
||||
|
||||
strainer = @environment.create_strainer(@context, [a, b])
|
||||
|
||||
assert_kind_of(StrainerTemplate, strainer)
|
||||
assert_kind_of(a, strainer)
|
||||
assert_kind_of(b, strainer)
|
||||
@ -94,8 +99,10 @@ class StrainerFactoryUnitTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_add_global_filter_clears_cache
|
||||
assert_equal('input', StrainerFactory.create(@context).invoke('late_added_filter', 'input'))
|
||||
StrainerFactory.add_global_filter(LateAddedFilter)
|
||||
assert_equal('filtered', StrainerFactory.create(nil).invoke('late_added_filter', 'input'))
|
||||
assert_equal('input', @environment.create_strainer(@context).invoke('late_added_filter', 'input'))
|
||||
|
||||
@environment.register_filter(LateAddedFilter)
|
||||
|
||||
assert_equal('filtered', @environment.create_strainer(nil).invoke('late_added_filter', 'input'))
|
||||
end
|
||||
end
|
@ -6,48 +6,144 @@ class LexerUnitTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def test_strings
|
||||
tokens = Lexer.new(%( 'this is a test""' "wat 'lol'")).tokenize
|
||||
assert_equal([[:string, %('this is a test""')], [:string, %("wat 'lol'")], [:end_of_string]], tokens)
|
||||
assert_equal(
|
||||
[[:string, %('this is a test""')], [:string, %("wat 'lol'")], [:end_of_string]],
|
||||
tokenize(%( 'this is a test""' "wat 'lol'")),
|
||||
)
|
||||
end
|
||||
|
||||
def test_integer
|
||||
tokens = Lexer.new('hi 50').tokenize
|
||||
assert_equal([[:id, 'hi'], [:number, '50'], [:end_of_string]], tokens)
|
||||
assert_equal(
|
||||
[[:id, 'hi'], [:number, '50'], [:end_of_string]],
|
||||
tokenize('hi 50'),
|
||||
)
|
||||
end
|
||||
|
||||
def test_float
|
||||
tokens = Lexer.new('hi 5.0').tokenize
|
||||
assert_equal([[:id, 'hi'], [:number, '5.0'], [:end_of_string]], tokens)
|
||||
assert_equal(
|
||||
[[:id, 'hi'], [:number, '5.0'], [:end_of_string]],
|
||||
tokenize('hi 5.0'),
|
||||
)
|
||||
end
|
||||
|
||||
def test_comparison
|
||||
tokens = Lexer.new('== <> contains ').tokenize
|
||||
assert_equal([[:comparison, '=='], [:comparison, '<>'], [:comparison, 'contains'], [:end_of_string]], tokens)
|
||||
assert_equal(
|
||||
[[:comparison, '=='], [:comparison, '<>'], [:comparison, 'contains'], [:end_of_string]],
|
||||
tokenize('== <> contains '),
|
||||
)
|
||||
end
|
||||
|
||||
def test_comparison_without_whitespace
|
||||
assert_equal(
|
||||
[[:number, '1'], [:comparison, '>'], [:number, '0'], [:end_of_string]],
|
||||
tokenize('1>0'),
|
||||
)
|
||||
end
|
||||
|
||||
def test_comparison_with_negative_number
|
||||
assert_equal(
|
||||
[[:number, '1'], [:comparison, '>'], [:number, '-1'], [:end_of_string]],
|
||||
tokenize('1>-1'),
|
||||
)
|
||||
end
|
||||
|
||||
def test_raise_for_invalid_comparison
|
||||
assert_raises(SyntaxError) do
|
||||
tokenize('1>!1')
|
||||
end
|
||||
|
||||
assert_raises(SyntaxError) do
|
||||
tokenize('1=<1')
|
||||
end
|
||||
|
||||
assert_raises(SyntaxError) do
|
||||
tokenize('1!!1')
|
||||
end
|
||||
end
|
||||
|
||||
def test_specials
|
||||
tokens = Lexer.new('| .:').tokenize
|
||||
assert_equal([[:pipe, '|'], [:dot, '.'], [:colon, ':'], [:end_of_string]], tokens)
|
||||
tokens = Lexer.new('[,]').tokenize
|
||||
assert_equal([[:open_square, '['], [:comma, ','], [:close_square, ']'], [:end_of_string]], tokens)
|
||||
assert_equal(
|
||||
[[:pipe, '|'], [:dot, '.'], [:colon, ':'], [:end_of_string]],
|
||||
tokenize('| .:'),
|
||||
)
|
||||
|
||||
assert_equal(
|
||||
[[:open_square, '['], [:comma, ','], [:close_square, ']'], [:end_of_string]],
|
||||
tokenize('[,]'),
|
||||
)
|
||||
end
|
||||
|
||||
def test_fancy_identifiers
|
||||
tokens = Lexer.new('hi five?').tokenize
|
||||
assert_equal([[:id, 'hi'], [:id, 'five?'], [:end_of_string]], tokens)
|
||||
assert_equal([[:id, 'hi'], [:id, 'five?'], [:end_of_string]], tokenize('hi five?'))
|
||||
|
||||
tokens = Lexer.new('2foo').tokenize
|
||||
assert_equal([[:number, '2'], [:id, 'foo'], [:end_of_string]], tokens)
|
||||
assert_equal([[:number, '2'], [:id, 'foo'], [:end_of_string]], tokenize('2foo'))
|
||||
end
|
||||
|
||||
def test_whitespace
|
||||
tokens = Lexer.new("five|\n\t ==").tokenize
|
||||
assert_equal([[:id, 'five'], [:pipe, '|'], [:comparison, '=='], [:end_of_string]], tokens)
|
||||
assert_equal(
|
||||
[[:id, 'five'], [:pipe, '|'], [:comparison, '=='], [:end_of_string]],
|
||||
tokenize("five|\n\t =="),
|
||||
)
|
||||
end
|
||||
|
||||
def test_unexpected_character
|
||||
assert_raises(SyntaxError) do
|
||||
Lexer.new("%").tokenize
|
||||
tokenize("%")
|
||||
end
|
||||
end
|
||||
|
||||
def test_negative_numbers
|
||||
assert_equal(
|
||||
[[:id, 'foo'], [:pipe, '|'], [:id, 'default'], [:colon, ":"], [:number, '-1'], [:end_of_string]],
|
||||
tokenize("foo | default: -1"),
|
||||
)
|
||||
end
|
||||
|
||||
def test_greater_than_two_digits
|
||||
assert_equal(
|
||||
[[:id, 'foo'], [:comparison, '>'], [:number, '12'], [:end_of_string]],
|
||||
tokenize("foo > 12"),
|
||||
)
|
||||
end
|
||||
|
||||
def test_error_with_utf8_character
|
||||
error = assert_raises(SyntaxError) do
|
||||
tokenize("1 < 1Ø")
|
||||
end
|
||||
|
||||
assert_equal(
|
||||
'Liquid syntax error: Unexpected character Ø',
|
||||
error.message,
|
||||
)
|
||||
end
|
||||
|
||||
def test_contains_as_attribute_name
|
||||
assert_equal(
|
||||
[[:id, "a"], [:dot, "."], [:id, "contains"], [:dot, "."], [:id, "b"], [:end_of_string]],
|
||||
tokenize("a.contains.b"),
|
||||
)
|
||||
end
|
||||
|
||||
def test_tokenize_incomplete_expression
|
||||
assert_equal([[:id, "false"], [:dash, "-"], [:end_of_string]], tokenize("false -"))
|
||||
assert_equal([[:id, "false"], [:comparison, "<"], [:end_of_string]], tokenize("false <"))
|
||||
assert_equal([[:id, "false"], [:comparison, ">"], [:end_of_string]], tokenize("false >"))
|
||||
assert_equal([[:id, "false"], [:number, "1"], [:end_of_string]], tokenize("false 1"))
|
||||
end
|
||||
|
||||
def test_error_with_invalid_utf8
|
||||
error = assert_raises(SyntaxError) do
|
||||
tokenize("\x00\xff")
|
||||
end
|
||||
assert_equal(
|
||||
'Liquid syntax error: Invalid byte sequence in UTF-8',
|
||||
error.message,
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def tokenize(input)
|
||||
Lexer.tokenize(StringScanner.new(input))
|
||||
end
|
||||
end
|
||||
|
@ -6,20 +6,20 @@ class ParserUnitTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def test_consume
|
||||
p = Parser.new("wat: 7")
|
||||
p = new_parser("wat: 7")
|
||||
assert_equal('wat', p.consume(:id))
|
||||
assert_equal(':', p.consume(:colon))
|
||||
assert_equal('7', p.consume(:number))
|
||||
end
|
||||
|
||||
def test_jump
|
||||
p = Parser.new("wat: 7")
|
||||
p = new_parser("wat: 7")
|
||||
p.jump(2)
|
||||
assert_equal('7', p.consume(:number))
|
||||
end
|
||||
|
||||
def test_consume?
|
||||
p = Parser.new("wat: 7")
|
||||
p = new_parser("wat: 7")
|
||||
assert_equal('wat', p.consume?(:id))
|
||||
assert_equal(false, p.consume?(:dot))
|
||||
assert_equal(':', p.consume(:colon))
|
||||
@ -27,7 +27,7 @@ class ParserUnitTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_id?
|
||||
p = Parser.new("wat 6 Peter Hegemon")
|
||||
p = new_parser("wat 6 Peter Hegemon")
|
||||
assert_equal('wat', p.id?('wat'))
|
||||
assert_equal(false, p.id?('endgame'))
|
||||
assert_equal('6', p.consume(:number))
|
||||
@ -36,7 +36,7 @@ class ParserUnitTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_look
|
||||
p = Parser.new("wat 6 Peter Hegemon")
|
||||
p = new_parser("wat 6 Peter Hegemon")
|
||||
assert_equal(true, p.look(:id))
|
||||
assert_equal('wat', p.consume(:id))
|
||||
assert_equal(false, p.look(:comparison))
|
||||
@ -46,12 +46,12 @@ class ParserUnitTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_expressions
|
||||
p = Parser.new("hi.there hi?[5].there? hi.there.bob")
|
||||
p = new_parser("hi.there hi?[5].there? hi.there.bob")
|
||||
assert_equal('hi.there', p.expression)
|
||||
assert_equal('hi?[5].there?', p.expression)
|
||||
assert_equal('hi.there.bob', p.expression)
|
||||
|
||||
p = Parser.new("567 6.0 'lol' \"wut\"")
|
||||
p = new_parser("567 6.0 'lol' \"wut\"")
|
||||
assert_equal('567', p.expression)
|
||||
assert_equal('6.0', p.expression)
|
||||
assert_equal("'lol'", p.expression)
|
||||
@ -59,7 +59,7 @@ class ParserUnitTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_ranges
|
||||
p = Parser.new("(5..7) (1.5..9.6) (young..old) (hi[5].wat..old)")
|
||||
p = new_parser("(5..7) (1.5..9.6) (young..old) (hi[5].wat..old)")
|
||||
assert_equal('(5..7)', p.expression)
|
||||
assert_equal('(1.5..9.6)', p.expression)
|
||||
assert_equal('(young..old)', p.expression)
|
||||
@ -67,7 +67,7 @@ class ParserUnitTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_arguments
|
||||
p = Parser.new("filter: hi.there[5], keyarg: 7")
|
||||
p = new_parser("filter: hi.there[5], keyarg: 7")
|
||||
assert_equal('filter', p.consume(:id))
|
||||
assert_equal(':', p.consume(:colon))
|
||||
assert_equal('hi.there[5]', p.argument)
|
||||
@ -77,8 +77,14 @@ class ParserUnitTest < Minitest::Test
|
||||
|
||||
def test_invalid_expression
|
||||
assert_raises(SyntaxError) do
|
||||
p = Parser.new("==")
|
||||
p = new_parser("==")
|
||||
p.expression
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def new_parser(str)
|
||||
Parser.new(StringScanner.new(str))
|
||||
end
|
||||
end
|
||||
|
@ -25,11 +25,13 @@ class StrainerTemplateUnitTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_add_filter_raises_when_module_privately_overrides_registered_public_methods
|
||||
strainer = Context.new.strainer
|
||||
|
||||
error = assert_raises(Liquid::MethodOverrideError) do
|
||||
strainer.class.add_filter(PrivateMethodOverrideFilter)
|
||||
Liquid::Environment.build do |env|
|
||||
env.register_filter(PublicMethodOverrideFilter)
|
||||
env.register_filter(PrivateMethodOverrideFilter)
|
||||
end
|
||||
end
|
||||
|
||||
assert_equal('Liquid error: Filter overrides registered public methods as non public: public_filter', error.message)
|
||||
end
|
||||
|
||||
@ -42,11 +44,13 @@ class StrainerTemplateUnitTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_add_filter_raises_when_module_overrides_registered_public_method_as_protected
|
||||
strainer = Context.new.strainer
|
||||
|
||||
error = assert_raises(Liquid::MethodOverrideError) do
|
||||
strainer.class.add_filter(ProtectedMethodOverrideFilter)
|
||||
Liquid::Environment.build do |env|
|
||||
env.register_filter(PublicMethodOverrideFilter)
|
||||
env.register_filter(ProtectedMethodOverrideFilter)
|
||||
end
|
||||
end
|
||||
|
||||
assert_equal('Liquid error: Filter overrides registered public methods as non public: public_filter', error.message)
|
||||
end
|
||||
|
||||
@ -58,8 +62,9 @@ class StrainerTemplateUnitTest < Minitest::Test
|
||||
|
||||
def test_add_filter_does_not_raise_when_module_overrides_previously_registered_method
|
||||
with_global_filter do
|
||||
strainer = Context.new.strainer
|
||||
strainer.class.add_filter(PublicMethodOverrideFilter)
|
||||
context = Context.new
|
||||
context.add_filters([PublicMethodOverrideFilter])
|
||||
strainer = context.strainer
|
||||
assert(strainer.class.send(:filter_methods).include?('public_filter'))
|
||||
end
|
||||
end
|
||||
|
@ -6,18 +6,36 @@ class TagUnitTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def test_tag
|
||||
tag = Tag.parse('tag', "", Tokenizer.new(""), ParseContext.new)
|
||||
tag = Tag.parse('tag', "", new_tokenizer, ParseContext.new)
|
||||
assert_equal('liquid::tag', tag.name)
|
||||
assert_equal('', tag.render(Context.new))
|
||||
end
|
||||
|
||||
def test_return_raw_text_of_tag
|
||||
tag = Tag.parse("long_tag", "param1, param2, param3", Tokenizer.new(""), ParseContext.new)
|
||||
tag = Tag.parse("long_tag", "param1, param2, param3", new_tokenizer, ParseContext.new)
|
||||
assert_equal("long_tag param1, param2, param3", tag.raw)
|
||||
end
|
||||
|
||||
def test_tag_name_should_return_name_of_the_tag
|
||||
tag = Tag.parse("some_tag", "", Tokenizer.new(""), ParseContext.new)
|
||||
tag = Tag.parse("some_tag", "", new_tokenizer, ParseContext.new)
|
||||
assert_equal('some_tag', tag.tag_name)
|
||||
end
|
||||
|
||||
class CustomTag < Liquid::Tag
|
||||
def render(_context); end
|
||||
end
|
||||
|
||||
def test_tag_render_to_output_buffer_nil_value
|
||||
custom_tag = CustomTag.parse("some_tag", "", new_tokenizer, ParseContext.new)
|
||||
assert_equal('some string', custom_tag.render_to_output_buffer(Context.new, "some string"))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def new_tokenizer
|
||||
Tokenizer.new(
|
||||
source: "",
|
||||
string_scanner: StringScanner.new(""),
|
||||
)
|
||||
end
|
||||
end
|
||||
|
202
test/unit/tags/comment_tag_unit_test.rb
Normal file
202
test/unit/tags/comment_tag_unit_test.rb
Normal file
@ -0,0 +1,202 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class CommentTagUnitTest < Minitest::Test
|
||||
def test_comment_inside_liquid_tag
|
||||
assert_template_result("", <<~LIQUID.chomp)
|
||||
{% liquid
|
||||
if 1 != 1
|
||||
comment
|
||||
else
|
||||
echo 123
|
||||
endcomment
|
||||
endif
|
||||
%}
|
||||
LIQUID
|
||||
end
|
||||
|
||||
def test_does_not_parse_nodes_inside_a_comment
|
||||
assert_template_result("", <<~LIQUID.chomp)
|
||||
{% comment %}
|
||||
{% if true %}
|
||||
{% if ... %}
|
||||
{%- for ? -%}
|
||||
{% while true %}
|
||||
{%
|
||||
unless if
|
||||
%}
|
||||
{% endcase %}
|
||||
{% endcomment %}
|
||||
LIQUID
|
||||
end
|
||||
|
||||
def test_allows_unclosed_tags
|
||||
assert_template_result('', <<~LIQUID.chomp)
|
||||
{% comment %}
|
||||
{% if true %}
|
||||
{% endcomment %}
|
||||
LIQUID
|
||||
end
|
||||
|
||||
def test_open_tags_in_comment
|
||||
assert_template_result('', <<~LIQUID.chomp)
|
||||
{% comment %}
|
||||
{% assign a = 123 {% comment %}
|
||||
{% endcomment %}
|
||||
LIQUID
|
||||
|
||||
assert_raises(Liquid::SyntaxError) do
|
||||
assert_template_result("", <<~LIQUID.chomp)
|
||||
{% comment %}
|
||||
{% assign foo = "1"
|
||||
{% endcomment %}
|
||||
LIQUID
|
||||
end
|
||||
|
||||
assert_raises(Liquid::SyntaxError) do
|
||||
assert_template_result("", <<~LIQUID.chomp)
|
||||
{% comment %}
|
||||
{% comment %}
|
||||
{% invalid
|
||||
{% endcomment %}
|
||||
{% endcomment %}
|
||||
LIQUID
|
||||
end
|
||||
|
||||
assert_raises(Liquid::SyntaxError) do
|
||||
assert_template_result("", <<~LIQUID.chomp)
|
||||
{% comment %}
|
||||
{% {{ {%- endcomment %}
|
||||
LIQUID
|
||||
end
|
||||
end
|
||||
|
||||
def test_child_comment_tags_need_to_be_closed
|
||||
assert_template_result("", <<~LIQUID.chomp)
|
||||
{% comment %}
|
||||
{% comment %}
|
||||
{% comment %}{% endcomment %}
|
||||
{% endcomment %}
|
||||
{% endcomment %}
|
||||
LIQUID
|
||||
|
||||
assert_raises(Liquid::SyntaxError) do
|
||||
assert_template_result("", <<~LIQUID.chomp)
|
||||
{% comment %}
|
||||
{% comment %}
|
||||
{% comment %}
|
||||
{% endcomment %}
|
||||
{% endcomment %}
|
||||
LIQUID
|
||||
end
|
||||
end
|
||||
|
||||
def test_child_raw_tags_need_to_be_closed
|
||||
assert_template_result("", <<~LIQUID.chomp)
|
||||
{% comment %}
|
||||
{% raw %}
|
||||
{% endcomment %}
|
||||
{% endraw %}
|
||||
{% endcomment %}
|
||||
LIQUID
|
||||
|
||||
assert_raises(Liquid::SyntaxError) do
|
||||
Liquid::Template.parse(<<~LIQUID.chomp)
|
||||
{% comment %}
|
||||
{% raw %}
|
||||
{% endcomment %}
|
||||
{% endcomment %}
|
||||
LIQUID
|
||||
end
|
||||
end
|
||||
|
||||
def test_error_line_number_is_correct
|
||||
template = Liquid::Template.parse(<<~LIQUID.chomp, line_numbers: true)
|
||||
{% comment %}
|
||||
{% if true %}
|
||||
{% endcomment %}
|
||||
{{ errors.standard_error }}
|
||||
LIQUID
|
||||
|
||||
output = template.render('errors' => ErrorDrop.new)
|
||||
expected = <<~TEXT.chomp
|
||||
|
||||
Liquid error (line 4): standard error
|
||||
TEXT
|
||||
|
||||
assert_equal(expected, output)
|
||||
end
|
||||
|
||||
def test_comment_tag_delimiter_with_extra_strings
|
||||
assert_template_result(
|
||||
'',
|
||||
<<~LIQUID.chomp,
|
||||
{% comment %}
|
||||
{% comment %}
|
||||
{% endcomment
|
||||
{% if true %}
|
||||
{% endif %}
|
||||
{% endcomment %}
|
||||
LIQUID
|
||||
)
|
||||
end
|
||||
|
||||
def test_nested_comment_tag_with_extra_strings
|
||||
assert_template_result(
|
||||
'',
|
||||
<<~LIQUID.chomp,
|
||||
{% comment %}
|
||||
{% comment
|
||||
{% assign foo = 1 %}
|
||||
{% endcomment
|
||||
{% assign foo = 1 %}
|
||||
{% endcomment %}
|
||||
LIQUID
|
||||
)
|
||||
end
|
||||
|
||||
def test_ignores_delimiter_with_extra_strings
|
||||
assert_template_result(
|
||||
'',
|
||||
<<~LIQUID.chomp,
|
||||
{% if true %}
|
||||
{% comment %}
|
||||
{% commentXXXXX %}wut{% endcommentXXXXX %}
|
||||
{% endcomment %}
|
||||
{% endif %}
|
||||
LIQUID
|
||||
)
|
||||
end
|
||||
|
||||
def test_delimiter_can_have_extra_strings
|
||||
assert_template_result('', "{% comment %}123{% endcomment xyz %}")
|
||||
assert_template_result('', "{% comment %}123{% endcomment\txyz %}")
|
||||
assert_template_result('', "{% comment %}123{% endcomment\nxyz %}")
|
||||
assert_template_result('', "{% comment %}123{% endcomment\n xyz endcomment %}")
|
||||
assert_template_result('', "{%comment}{% assign a = 1 %}{%endcomment}{% endif %}")
|
||||
end
|
||||
|
||||
def test_with_whitespace_control
|
||||
assert_template_result("Hello!", " {%- comment -%}123{%- endcomment -%}Hello!")
|
||||
assert_template_result("Hello!", "{%- comment -%}123{%- endcomment -%} Hello!")
|
||||
assert_template_result("Hello!", " {%- comment -%}123{%- endcomment -%} Hello!")
|
||||
|
||||
assert_template_result("Hello!", <<~LIQUID.chomp)
|
||||
{%- comment %}Whitespace control!{% endcomment -%}
|
||||
Hello!
|
||||
LIQUID
|
||||
end
|
||||
|
||||
def test_dont_override_liquid_tag_whitespace_control
|
||||
assert_template_result("Hello!World!", <<~LIQUID.chomp)
|
||||
Hello!
|
||||
{%- liquid
|
||||
comment
|
||||
this is inside a liquid tag
|
||||
endcomment
|
||||
-%}
|
||||
World!
|
||||
LIQUID
|
||||
end
|
||||
end
|
184
test/unit/tags/doc_tag_unit_test.rb
Normal file
184
test/unit/tags/doc_tag_unit_test.rb
Normal file
@ -0,0 +1,184 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class DocTagUnitTest < Minitest::Test
|
||||
def test_doc_tag
|
||||
template = <<~LIQUID.chomp
|
||||
{% doc %}
|
||||
Renders loading-spinner.
|
||||
|
||||
@param {string} foo - some foo
|
||||
@param {string} [bar] - optional bar
|
||||
|
||||
@example
|
||||
{% render 'loading-spinner', foo: 'foo' %}
|
||||
{% render 'loading-spinner', foo: 'foo', bar: 'bar' %}
|
||||
{% enddoc %}
|
||||
LIQUID
|
||||
|
||||
assert_template_result('', template)
|
||||
end
|
||||
|
||||
def test_doc_tag_does_not_support_extra_arguments
|
||||
error = assert_raises(Liquid::SyntaxError) do
|
||||
template = <<~LIQUID.chomp
|
||||
{% doc extra %}
|
||||
{% enddoc %}
|
||||
LIQUID
|
||||
|
||||
Liquid::Template.parse(template)
|
||||
end
|
||||
|
||||
exp_error = "Liquid syntax error: Syntax Error in 'doc' - Valid syntax: {% doc %}{% enddoc %}"
|
||||
act_error = error.message
|
||||
|
||||
assert_equal(exp_error, act_error)
|
||||
end
|
||||
|
||||
def test_doc_tag_must_support_valid_tags
|
||||
assert_match_syntax_error("Liquid syntax error (line 1): 'doc' tag was never closed", '{% doc %} foo')
|
||||
assert_match_syntax_error("Liquid syntax error (line 1): Syntax Error in 'doc' - Valid syntax: {% doc %}{% enddoc %}", '{% doc } foo {% enddoc %}')
|
||||
assert_match_syntax_error("Liquid syntax error (line 1): Syntax Error in 'doc' - Valid syntax: {% doc %}{% enddoc %}", '{% doc } foo %}{% enddoc %}')
|
||||
end
|
||||
|
||||
def test_doc_tag_ignores_liquid_nodes
|
||||
template = <<~LIQUID.chomp
|
||||
{% doc %}
|
||||
{% if true %}
|
||||
{% if ... %}
|
||||
{%- for ? -%}
|
||||
{% while true %}
|
||||
{%
|
||||
unless if
|
||||
%}
|
||||
{% endcase %}
|
||||
{% enddoc %}
|
||||
LIQUID
|
||||
|
||||
assert_template_result('', template)
|
||||
end
|
||||
|
||||
def test_doc_tag_ignores_unclosed_liquid_tags
|
||||
template = <<~LIQUID.chomp
|
||||
{% doc %}
|
||||
{% if true %}
|
||||
{% enddoc %}
|
||||
LIQUID
|
||||
|
||||
assert_template_result('', template)
|
||||
end
|
||||
|
||||
def test_doc_tag_does_not_allow_nested_docs
|
||||
error = assert_raises(Liquid::SyntaxError) do
|
||||
template = <<~LIQUID.chomp
|
||||
{% doc %}
|
||||
{% doc %}
|
||||
{% doc %}
|
||||
{% enddoc %}
|
||||
LIQUID
|
||||
|
||||
Liquid::Template.parse(template)
|
||||
end
|
||||
|
||||
exp_error = "Liquid syntax error: Syntax Error in 'doc' - Nested doc tags are not allowed"
|
||||
act_error = error.message
|
||||
|
||||
assert_equal(exp_error, act_error)
|
||||
end
|
||||
|
||||
def test_doc_tag_ignores_nested_raw_tags
|
||||
template = <<~LIQUID.chomp
|
||||
{% doc %}
|
||||
{% raw %}
|
||||
{% enddoc %}
|
||||
LIQUID
|
||||
|
||||
assert_template_result('', template)
|
||||
end
|
||||
|
||||
def test_doc_tag_ignores_unclosed_assign
|
||||
template = <<~LIQUID.chomp
|
||||
{% doc %}
|
||||
{% assign foo = "1"
|
||||
{% enddoc %}
|
||||
LIQUID
|
||||
|
||||
assert_template_result('', template)
|
||||
end
|
||||
|
||||
def test_doc_tag_ignores_malformed_syntax
|
||||
template = <<~LIQUID.chomp
|
||||
{% doc %}
|
||||
{% {{ {%- enddoc %}
|
||||
LIQUID
|
||||
|
||||
assert_template_result('', template)
|
||||
end
|
||||
|
||||
def test_doc_tag_preserves_error_line_numbers
|
||||
template = Liquid::Template.parse(<<~LIQUID.chomp, line_numbers: true)
|
||||
{% doc %}
|
||||
{% if true %}
|
||||
{% enddoc %}
|
||||
{{ errors.standard_error }}
|
||||
LIQUID
|
||||
|
||||
expected = <<~TEXT.chomp
|
||||
|
||||
Liquid error (line 4): standard error
|
||||
TEXT
|
||||
|
||||
assert_equal(expected, template.render('errors' => ErrorDrop.new))
|
||||
end
|
||||
|
||||
def test_doc_tag_whitespace_control
|
||||
# Basic whitespace control
|
||||
assert_template_result("Hello!", " {%- doc -%}123{%- enddoc -%}Hello!")
|
||||
assert_template_result("Hello!", "{%- doc -%}123{%- enddoc -%} Hello!")
|
||||
assert_template_result("Hello!", " {%- doc -%}123{%- enddoc -%} Hello!")
|
||||
assert_template_result("Hello!", <<~LIQUID.chomp)
|
||||
{%- doc %}Whitespace control!{% enddoc -%}
|
||||
Hello!
|
||||
LIQUID
|
||||
end
|
||||
|
||||
def test_doc_tag_delimiter_handling
|
||||
assert_template_result('', <<~LIQUID.chomp)
|
||||
{% if true %}
|
||||
{% doc %}
|
||||
{% docEXTRA %}wut{% enddocEXTRA %}xyz
|
||||
{% enddoc %}
|
||||
{% endif %}
|
||||
LIQUID
|
||||
|
||||
assert_template_result('', "{% doc %}123{% enddoc xyz %}")
|
||||
assert_template_result('', "{% doc %}123{% enddoc\txyz %}")
|
||||
assert_template_result('', "{% doc %}123{% enddoc\nxyz %}")
|
||||
assert_template_result('', "{% doc %}123{% enddoc\n xyz enddoc %}")
|
||||
end
|
||||
|
||||
def test_doc_tag_visitor
|
||||
template_source = '{% doc %}{% enddoc %}'
|
||||
|
||||
assert_equal(
|
||||
[Liquid::Doc],
|
||||
visit(template_source),
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def traversal(template)
|
||||
ParseTreeVisitor
|
||||
.for(Template.parse(template).root)
|
||||
.add_callback_for(Liquid::Doc) do |tag|
|
||||
tag_class = tag.class
|
||||
tag_class
|
||||
end
|
||||
end
|
||||
|
||||
def visit(template)
|
||||
traversal(template).visit.flatten.compact
|
||||
end
|
||||
end
|
@ -20,62 +20,13 @@ class TemplateUnitTest < Minitest::Test
|
||||
assert_equal(fixture("en_locale.yml"), locale.path)
|
||||
end
|
||||
|
||||
def test_with_cache_classes_tags_returns_the_same_class
|
||||
original_cache_setting = Liquid.cache_classes
|
||||
Liquid.cache_classes = true
|
||||
|
||||
original_klass = Class.new
|
||||
Object.send(:const_set, :CustomTag, original_klass)
|
||||
Template.register_tag('custom', CustomTag)
|
||||
|
||||
Object.send(:remove_const, :CustomTag)
|
||||
|
||||
new_klass = Class.new
|
||||
Object.send(:const_set, :CustomTag, new_klass)
|
||||
|
||||
assert(Template.tags['custom'].equal?(original_klass))
|
||||
ensure
|
||||
Object.send(:remove_const, :CustomTag)
|
||||
Template.tags.delete('custom')
|
||||
Liquid.cache_classes = original_cache_setting
|
||||
end
|
||||
|
||||
def test_without_cache_classes_tags_reloads_the_class
|
||||
original_cache_setting = Liquid.cache_classes
|
||||
Liquid.cache_classes = false
|
||||
|
||||
original_klass = Class.new
|
||||
Object.send(:const_set, :CustomTag, original_klass)
|
||||
Template.register_tag('custom', CustomTag)
|
||||
|
||||
Object.send(:remove_const, :CustomTag)
|
||||
|
||||
new_klass = Class.new
|
||||
Object.send(:const_set, :CustomTag, new_klass)
|
||||
|
||||
assert(Template.tags['custom'].equal?(new_klass))
|
||||
ensure
|
||||
Object.send(:remove_const, :CustomTag)
|
||||
Template.tags.delete('custom')
|
||||
Liquid.cache_classes = original_cache_setting
|
||||
end
|
||||
|
||||
class FakeTag; end
|
||||
|
||||
def test_tags_delete
|
||||
Template.register_tag('fake', FakeTag)
|
||||
assert_equal(FakeTag, Template.tags['fake'])
|
||||
|
||||
Template.tags.delete('fake')
|
||||
assert_nil(Template.tags['fake'])
|
||||
end
|
||||
|
||||
def test_tags_can_be_looped_over
|
||||
Template.register_tag('fake', FakeTag)
|
||||
result = Template.tags.map { |name, klass| [name, klass] }
|
||||
assert(result.include?(["fake", "TemplateUnitTest::FakeTag"]))
|
||||
ensure
|
||||
Template.tags.delete('fake')
|
||||
with_custom_tag('fake', FakeTag) do
|
||||
result = Template.tags.map { |name, klass| [name, klass] }
|
||||
assert(result.include?(["fake", TemplateUnitTest::FakeTag]))
|
||||
end
|
||||
end
|
||||
|
||||
class TemplateSubclass < Liquid::Template
|
||||
@ -84,4 +35,15 @@ class TemplateUnitTest < Minitest::Test
|
||||
def test_template_inheritance
|
||||
assert_equal("foo", TemplateSubclass.parse("foo").render)
|
||||
end
|
||||
|
||||
def test_invalid_utf8
|
||||
input = "\xff\x00"
|
||||
error = assert_raises(SyntaxError) do
|
||||
Liquid::Tokenizer.new(source: input, string_scanner: StringScanner.new(input))
|
||||
end
|
||||
assert_equal(
|
||||
'Liquid syntax error: Invalid byte sequence in UTF-8',
|
||||
error.message,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
@ -6,6 +6,7 @@ class TokenizerTest < Minitest::Test
|
||||
def test_tokenize_strings
|
||||
assert_equal([' '], tokenize(' '))
|
||||
assert_equal(['hello world'], tokenize('hello world'))
|
||||
assert_equal(['{}'], tokenize('{}'))
|
||||
end
|
||||
|
||||
def test_tokenize_variables
|
||||
@ -30,6 +31,23 @@ class TokenizerTest < Minitest::Test
|
||||
assert_equal([1, 1, 3], tokenize_line_numbers(" {{\n funk \n}} "))
|
||||
end
|
||||
|
||||
def test_tokenize_with_nil_source_returns_empty_array
|
||||
assert_equal([], tokenize(nil))
|
||||
end
|
||||
|
||||
def test_incomplete_curly_braces
|
||||
assert_equal(["{{.}", " "], tokenize('{{.} '))
|
||||
assert_equal(["{{}", "%}"], tokenize('{{}%}'))
|
||||
assert_equal(["{{}}", "}"], tokenize('{{}}}'))
|
||||
end
|
||||
|
||||
def test_unmatching_start_and_end
|
||||
assert_equal(["{{%}"], tokenize('{{%}'))
|
||||
assert_equal(["{{%%%}}"], tokenize('{{%%%}}'))
|
||||
assert_equal(["{%", "}}"], tokenize('{%}}'))
|
||||
assert_equal(["{%%}", "}"], tokenize('{%%}}'))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def new_tokenizer(source, parse_context: Liquid::ParseContext.new, start_line_number: nil)
|
||||
|
Loading…
x
Reference in New Issue
Block a user